关于 truesheet-usage
truesheet-usage 是面向 React Native TrueSheet v4 使用者的编码指南,帮助开发者选择集成方式并正确使用其公开 API。内容涵盖工作表布局、控制模式、停靠点和常见交互场景。
主要功能
- 01说明工作表控制模式的选择
- 02介绍停靠点与内容布局
- 03涵盖滚动视图及页眉页脚
- 04提供背景、键盘与关闭处理指引
适用场景
- 01在 React Native 应用中集成 TrueSheet
- 02实现可调整高度的内容面板
- 03为导航流程配置工作表界面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lodev09/ react-native-true-sheet --skill truesheet-usage安装后在 Claude Code 中输入/truesheet-usage确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/advanced-patterns.mdapi.mdconfiguration.mdtroubleshooting.md详细介绍
TrueSheet 消费者指南
使用此技能可为使用 @lodev09/react-native-true-sheet(v4)的应用编写正确、符合惯例的代码。本指南涵盖如何选择合适的集成模式、正确使用公共 API,以及如何避免平台特有的陷阱。
需要 React Native >= 0.82(Expo SDK 55+)和新架构。
快速开始
最简单的 Sheet:一个 ref、一个按钮和一些内容。
import { useRef } from 'react'
import { Button, Text, View } from 'react-native'
import { TrueSheet } from '@lodev09/react-native-true-sheet'
export function App() {
const sheet = useRef<TrueSheet>(null)
return (
<View>
<Button title="Open" onPress={() => sheet.current?.present()} />
<TrueSheet ref={sheet} detents={['auto']} cornerRadius={24} grabber>
<View style={{ padding: 16 }}>
<Text>Hello from the sheet</Text>
<Button title="Close" onPress={() => sheet.current?.dismiss()} />
</View>
</TrueSheet>
</View>
)
}
内容布局(v4)
Sheet 的内容会自然布局——像常规视图或 React Navigation 屏幕一样,按子元素的高度自适应。默认情况下,内容不会填满 Sheet。由此会产生以下情况:
- 如果内容未填满 Sheet,
flex: 1子元素的高度会收缩为零。 - 若要按每个 detent 的可见高度填满 Sheet(用于间隔元素、居中内容、有界滚动视图),请通过 Sheet 的
style属性传入flex: 1:
<TrueSheet style={{ flex: 1 }} detents={[0.5, 1]}>
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>Centered</Text>
</View>
</TrueSheet>
内容会按每个 detent 的 Sheet 可见高度调整大小,并在拖动时实时跟随,因此弹性布局会逐帧贴合 Sheet 的边缘。
选择合适的控制模式
根据触发器相对于 Sheet 的位置以及目标平台选择一种模式。
| 模式 | 适用场景 | 平台 |
|---|---|---|
| Ref | 触发器和 Sheet 位于同一组件中 | 全部 |
| 命名 Sheet + 全局方法 | 触发器远离 Sheet(位于不同屏幕或组件树深处) | 仅原生平台 |
TrueSheetProvider + useTrueSheet() | 需要支持 Web,或希望通过 hook 控制 | 全部(Web 上必需) |
createTrueSheetNavigator() / Expo Router Sheet | Sheet 属于导航流程的一部分 | 全部 |
ReanimatedTrueSheet | 需要与 Sheet 位置同步的动画值 | 全部 |
基于 Ref
快速开始中已展示。通过 ref 调用 present()、dismiss()、resize(index)。
使用全局方法控制命名 Sheet(仅原生平台)
当触发器远离 Sheet 的渲染位置时:
// Somewhere in the tree
<TrueSheet name="profile" detents={['auto', 1]}>
<ProfileContent />
</TrueSheet>
// Anywhere else (native only)
await TrueSheet.present('profile')
await TrueSheet.dismiss('profile')
await TrueSheet.resize('profile', 1)
await TrueSheet.dismissAll()
每个 name 都必须唯一。静态方法在 Web 上不可用——请改用 Provider 模式。
使用 Provider 控制 Web
使用 TrueSheetProvider 包装应用(在原生平台上,它只是一个开销为零的透传组件)。Web 还需要安装 @radix-ui/react-dialog 和 @radix-ui/react-presence(可选 peer 依赖):
import { TrueSheet, TrueSheetProvider, useTrueSheet } from '@lodev09/react-native-true-sheet'
function Toolbar() {
const { present, dismiss } = useTrueSheet()
return <Button title="Open" onPress={() => present('settings')} />
}
export function App() {
return (
<TrueSheetProvider>
<Toolbar />
<TrueSheet name="settings" detents={[0.5, 1]}>
<SettingsContent />
</TrueSheet>
</TrueSheetProvider>
)
}
导航(React Navigation / Expo Router)
完整配置请参阅高级模式参考,内容包括 createTrueSheetNavigator、静态 API(createTrueSheetScreen)、来自 /navigation/expo-router 的 Expo Router Sheet 布局、屏幕选项(包括通过 setOptions 设置的 scrollableRef)、以堆叠 Sheet 屏幕实现的多步骤流程,以及 useTrueSheetNavigation。
Reanimated
有关 ReanimatedTrueSheet、ReanimatedTrueSheetProvider 和动画值(animatedPosition、animatedIndex、animatedDetent),请参阅高级模式参考。
Detent
Detent 定义 Sheet 可吸附到的高度。最多可设置 3 个 detent,并按从小到大排序。默认值:[0.5, 1]。
| 值 | 含义 |
|---|---|
'auto' | 调整尺寸以适应内容(iOS 16+、Android、Web)。支持滚动容器——尺寸会适应滚动内容,并随内容增长或缩小而调整 |
'peek' | 折叠高度由测量得到的 header、绝对定位的 footer 以及截至 TrueSheetPeek 标记的内容共同决定(iOS 16+、Android、Web)。未提供这些内容时回退为 150 |
0 – 1 | 屏幕高度的比例 |
// Content-sized sheet
<TrueSheet detents={['auto']} />
// Half and full screen
<TrueSheet detents={[0.5, 1]} />
// Collapsed summary that expands to near-full
<TrueSheet detents={['peek', 0.9]} />
常用方案
可滚动内容
通过 scrollableRef 将工作表指向你的滚动视图。对于固定停靠点,在工作表的 style 上设置 flex: 1,以约束滚动视图:
const scrollableRef = useRef<ScrollView>(null)
<TrueSheet scrollableRef={scrollableRef} style={{ flex: 1 }} detents={[0.5, 1]} grabber>
<ScrollView ref={scrollableRef}>
{items.map(item => <ItemRow key={item.id} item={item} />)}
</ScrollView>
</TrueSheet>
- 使用
detents={['auto']}时,无需设置flex: 1——工作表会根据滚动内容调整大小,并自动约束视口 - 例外:会用
flex: 1包裹滚动视图的列表(例如FlashList),在'auto'模式下会渲染为空白,除非工作表设置了style={{ flex: 1 }}——工作表仍会根据列表调整大小 - 滚动视图不必是直接子元素——可以用
View包裹 - 键盘处理、嵌套滚动(Android)和底部安全区域内边距会自动配置
- 滚动到顶部会展开到下一个停靠点——使用
scrollableOptions={{ scrollingExpandsSheet: false }}可禁用此行为(YouTube 风格的评论) - 底部安全区域内边距(以及绝对定位页脚的高度)会在内容可滚动时由原生端应用——不要手动添加内边距,也可以通过
scrollableOptions={{ contentInsetAdjustment: 'never' }}关闭('safe-area'/'footer'会保留其中一项)
固定页眉和页脚
<TrueSheet
detents={[0.5, 1]}
scrollableRef={scrollableRef}
style={{ flex: 1 }}
header={
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>Title</Text>
</View>
}
footer={<BottomActions />}
>
<ScrollView ref={scrollableRef}>{/* ... */}</ScrollView>
</TrueSheet>
- 使用
header和footer属性——它们会在原生容器视图中渲染,因此布局计算会自动处理。不要用绝对定位模拟。 - 默认情况下,两者都是相对定位:页眉占据内容上方的空间,页脚占据内容下方的空间,并且两者都会计入
'auto'停靠点。 - 页脚会由原生端吸收底部安全区域内边距——从页脚内容中移除手动设置的
paddingBottom: insets.bottom,否则会重复叠加。通过footerStyle设置页脚背景,使其填满安全区域内边距。 - 相对定位的页脚会布局在内容下方——对于固定停靠点,将内容限制在
flex: 1(工作表的style)内,这样页脚才能保持可见。 - 若要让它们浮在内容上方(不计入
'auto'高度),请使用headerOptions={{ position: 'absolute' }}/footerOptions={{ position: 'absolute' }}。绝对定位的页脚会升到键盘上方(通过avoidKeyboard: false关闭此行为);相对定位的页脚则会在键盘后方保持在布局流中。 - 绝对定位的页脚会遮住已接入的滚动视图底部——滚动内容会自动按测得的页脚高度增加内边距(默认
contentInsetAdjustment: 'automatic'),且页脚会计入'auto',因此不要硬编码paddingBottom: FOOTER_HEIGHT。使用'safe-area'可让内容滚动到页脚下方。 - 设置
avoidKeyboard: false时,只有键盘上方的页脚部分会增加滚动内边距,并计入展开后的'auto'高度。必须启用页脚内边距调整。
Peek(地图样式的折叠工作表)
import { TrueSheet, TrueSheetPeek } from '@lodev09/react-native-true-sheet'
<TrueSheet detents={['peek', 0.9]} initialDetentIndex={0} header={<Summary />}>
<TrueSheetPeek>
<QuickActions />
</TrueSheetPeek>
<FullDetails />
</TrueSheet>
折叠时,工作表会显示从顶部到 TrueSheetPeek 底边的所有内容;下方内容会一直隐藏,直到工作表展开。空的 <TrueSheetPeek /> 可用作现有视图之间的折叠标记。每个工作表只能有一个 peek 组件。绝对定位的页脚会计入 peek 高度;相对定位的页脚不会(peek 状态下会被推到屏幕外)。
工作表上方的 Toast 和对话框
import { TrueSheet, TrueSheetOverlay } from '@lodev09/react-native-true-sheet'
<TrueSheetOverlay>
{toast && <Toast message={toast} />}
</TrueSheetOverlay>
在所有已呈现的工作表上方的原生图层中渲染子元素。通过有条件地渲染子元素来显示/隐藏。填满整个窗口;未落在子元素上的触摸事件会穿透。此后不要再使用 RN Modal/FullWindowOverlay。不要在其中放置 react-native-screens 容器(原生堆栈、标签页、抽屉)——Android 会崩溃。
不可取消的确认
<TrueSheet
ref={sheet}
detents={['auto']}
dismissible={false}
draggable={false}
dimmed
grabber={false}
>
<View style={{ padding: 24 }}>
<Text>Are you sure?</Text>
<Button title="Confirm" onPress={handleConfirm} />
<Button title="Cancel" onPress={() => sheet.current?.dismiss()} />
</View>
</TrueSheet>
取消前确认(未保存的更改)
<TrueSheet
ref={sheet}
dismissible={!isDirty}
onDismissAttempt={() =>
Alert.alert('Discard changes?', undefined, [
{ text: 'Keep editing', style: 'cancel' },
{ text: 'Discard', style: 'destructive', onPress: () => sheet.current?.dismiss() },
])
}
>
{/* form */}
</TrueSheet>
不要试图通过回调来阻止取消——原生端无法等待 JS。通过状态切换 dismissible;被阻止的拖动以及返回/转义操作会触发 onDismissAttempt(点击遮罩不会),然后以编程方式调用 dismiss()。
背景
<TrueSheet backgroundBlur="system-material" /> {/* iOS-only blur, wins over backgroundColor */}
<TrueSheet backgroundStyle={{ backgroundColor: 'rgba(0, 122, 255, 0.25)' }} /> {/* tint glass/blur */}
<TrueSheet backgroundStyle={{ backgroundColor: '#18202b' }} /> {/* exact opaque color */}
<TrueSheet backgroundColor="transparent" /> {/* clear, no glass on supported iOS 26.1+ */}
<TrueSheet background={<BlurView intensity={40} style={StyleSheet.absoluteFill} />} /> {/* custom node */}
<TrueSheet detentBackgrounds={[null, { blur: 'system-material' }, '#18202b']} /> {/* per detent */}
backgroundStyle.backgroundColor 会覆盖玻璃或模糊效果。background 会在工作表效果上方渲染任意节点;包装器会忽略触摸事件。
v4 中已移除 blurOptions。如需自定义强度,请在 background 中使用第三方模糊效果。
有关图层、按停靠点过渡和 iOS 回退方案,请参阅 背景。
挂载时呈现
<TrueSheet detents={['auto', 1]} initialDetentIndex={0} initialDetentAnimated>
<WelcomeContent />
</TrueSheet>
为 'auto' 停靠点预先挂载内容
默认情况下,内容会在首次调用 present() 时挂载。如果内容需要异步加载完成(例如获取数据、加载图像),'auto' 会过早测量。设置 lazy={false} 可预先挂载内容而不呈现,然后在准备就绪后再呈现(仅限原生端):
<TrueSheet ref={sheet} lazy={false} detents={['auto']}>
<AsyncContent onReady={() => sheet.current?.present()} />
</TrueSheet>
控制变暗效果
// No dimming (allows background interaction)
<TrueSheet dimmed={false} detents={['auto']} />
// Dim only above a certain detent
<TrueSheet detents={['auto', 0.7, 1]} dimmedDetentIndex={1} />
// Custom dim color and opacity
<TrueSheet detents={['auto']} dimColor="#1b0a3c" dimOpacity={0.3} />
// Invisible dim that still blocks touches and dismisses on tap
<TrueSheet detents={['auto']} dimOpacity={0} />
以编程方式调整大小
resize() 接受停靠点索引,而非数值:
const sheet = useRef<TrueSheet>(null)
// detents={[0.3, 0.6, 1]}
await sheet.current?.resize(2) // expands to full (index 2)
有助于节省调试时间的规则
- 最多 3 个停靠点,按从小到大排序。
- 内容默认不会填满工作表——如果子元素使用
flex: 1,请在工作表上设置style={{ flex: 1 }},否则它们会以零高度渲染。 scrollableRef取代了 v3 的scrollable属性——传入 ScrollView/FlatList 的 ref。对于固定停靠点,在工作表的style上设置flex: 1以约束它;'auto'不需要 flex,但FlashList和其他包装在flex: 1中的列表除外。resize()接受索引,而非比例。resize(1)表示“切换到第二个停靠点”。- 整个应用中的工作表名称必须唯一。
- 静态方法仅限原生端——在 Web 上使用
useTrueSheet()。 - 不要为页脚或插入的滚动内容添加手动安全区域内边距——页脚会吸收底部内边距,滚动区域会在原生端获得该内边距。重复添加是 v4 最常见的布局问题。
autoFocus仅在挂载时触发——默认的延迟工作表可以正常使用(键盘会随工作表升起),但使用lazy={false}时,它只会在首次呈现时聚焦。要在每次呈现时聚焦,请在onWillPresent(键盘随工作表升起)或onDidPresent(工作表显示后)中执行:<TrueSheet lazy={false} onWillPresent={() => inputRef.current?.focus()}>- 在 Android 上,工作表内的
GestureHandlerRootView应使用flexGrow: 1(而非flex: 1)——除非工作表本身设置了style={{ flex: 1 }}。 - 在 iOS 上,关闭 Modal 前先关闭工作表——React Native 存在一个 bug:工作表仍可见时关闭 Modal 会导致屏幕空白。
- 使用
header/footer属性设置固定界面元素——不要使用绝对定位。需要让它们覆盖内容时,可通过headerOptions/footerOptions设置position: 'absolute'使其浮动。 - 背景效果:在受支持的 iOS 26.1+ 版本上,模糊效果优先于颜色,其次是默认的 Liquid Glass。要实现透明效果,请使用
backgroundColor="transparent"。可通过backgroundStyle.backgroundColor为玻璃/模糊效果添加色调,或绘制指定颜色。缺少效果设置器或在 iOS 26 上启用设计兼容模式时,会使用回退方案。iOS 27+ 会忽略UIDesignRequiresCompatibility。如需自定义强度,请在background中使用第三方模糊效果;blurOptions已移除。 - 原生堆栈应放在基础屏幕中,而不是工作表屏幕中(Sheet Navigator / Expo Router)。第一个屏幕是常规视图,可以承载
createNativeStackNavigator(这是包装应用的方式)。工作表屏幕无法承载它——Android 会因ScreenContainer is not attached under ReactRootView而崩溃。对于多步骤流程,请将每个步骤单独设置为工作表屏幕。
各平台差异一览
| 功能 | iOS | Android | Web |
|---|---|---|---|
'auto' detent | iOS 16+ | 是 | 是 |
'peek' detent / TrueSheetPeek | iOS 16+ | 是 | 是 |
TrueSheetOverlay | 是 | 是 | 是 |
detentBackgrounds | 在支持的 26.1+ 版本上使用原生效果,否则使用中点交叉淡化 | 颜色交叉淡化 | 颜色交叉淡化 |
backgroundBlur | 是 | 否 | 否 |
background / backgroundStyle | 是 | 是 | 是 |
| Liquid Glass | iOS 26+ | 否 | 否 |
| 静态全局方法 | 是 | 是 | 否(使用 provider) |
scrollableRef | 是 | 是 | 是 |
| 滚动边缘效果 | iOS 26+ | 否 | 否 |
grabberOptions | 是 | 是 | 否 |
dimColor / dimOpacity | 自定义变暗图层会替代系统变暗效果;堆叠的每个 sheet 都会各自添加一个 | 是 | 是 |
placement / 侧边 sheet | 边距由系统控制;在 iOS 27+ 上,'automatic' 与 'center' 的效果不同 | placementOffset 属性 | placementOffset 属性 |
presentation | iOS 17+(iPad) | 不适用 | 横屏/平板 |
foldOptions | iOS 27.1+ 可折叠设备(桌面模式) | 否 | 否 |
detached 模式 | 否 | 否 | 是 |
insetAdjustment | 是 | 是 | 否 |
dismissThreshold | 否(由系统控制) | 是 | 是 |
lazy={false}(预先挂载内容) | 是 | 是 | 否 |
| 延伸至边缘 | 不适用 | 自动检测 | 不适用 |
| 键盘处理 | 内置 | 内置 | 在软键盘上方适配 |
事件
最常用的事件:
| 事件 | 触发时机 | 载荷 |
|---|---|---|
onMount | 内容已挂载并就绪 | — |
onDidPresent | Sheet 展示完毕 | { index, position, detent } |
onDidDismiss | Sheet 关闭完毕 | — |
onDismissAttempt | 用户在 dismissible={false} 时尝试关闭 | — |
onDetentChange | 用户拖动或 resize() 更改 detent | { index, position, detent } |
onPositionChange | 拖动/动画期间持续更新位置 | { index, position, detent, realtime } |
完整事件列表(拖动事件、焦点/失焦事件、will/did 生命周期事件对、onBackPress),请参阅 API 参考。
方法
在 ref 上:
present(index?, animated?)— 显示 sheetdismiss(animated?)— 隐藏 sheet 及其所有子级dismissStack(animated?)— 仅隐藏堆叠在上方的 sheetresize(index)— 按索引吸附到一个 detent
全局(仅原生平台):
TrueSheet.present(name, index?, animated?)TrueSheet.dismiss(name, animated?)TrueSheet.dismissStack(name, animated?)TrueSheet.resize(name, index)TrueSheet.dismissAll(animated?)
Web hook:
const { present, dismiss, dismissStack, resize, dismissAll } = useTrueSheet()
堆叠 Sheet
当另一个 sheet 可见时展示新 sheet,第一个 sheet 会自动隐藏。关闭顶部 sheet 后,前一个 sheet 会重新显示。这是内置功能,无需额外配置。
dismiss()会级联关闭:关闭当前 sheet 以及堆叠在其上方的所有 sheetdismissStack()仅关闭上方的 sheet,保留当前 sheet 可见- 使用
onDidFocus/onDidBlur响应 sheet 获得或失去顶部位置
深入参考
需要了解完整信息时,请查看以下参考文件:
| 参考文档 | 内容 |
|---|---|
| 配置 | 每个属性的类型、默认值、平台支持情况和说明 |
| API | 完整的事件和方法参考,以及载荷类型 |
| 高级模式 | 导航、Reanimated、Web、侧边 sheet、背景、Liquid Glass、Jest 模拟、从 v3 迁移到 v4 |
| 故障排除 | 各平台常见问题及解决方法 |