React Native 的 Sheet:模块化 Sheet 框架
Source: Dev.to
请提供您希望翻译的文章正文内容,我会按照要求将其译成简体中文并保留原有的格式、Markdown 语法以及技术术语。谢谢!
Source: …
问题描述
在移动端向用户展示内容的方式有很多,例如弹窗、下拉菜单、底部表单(bottom sheet)和居中表单(center sheet)。在大多数应用中,这些都以 模态层(modal)的形式出现,即位于界面其余部分之上的聚焦层。
在 React Native 中,内置的 Modal 组件实现了这一功能。但它的 API 范围有限,并且与原生的呈现行为紧密耦合。一旦需要自定义行为、复杂的手势系统或多个模态层之间的细致交互时,往往会陷入与框架的“搏斗”。
为填补这一空白,出现了若干库:
gorhom/react-native-bottom-sheetammarahm-ed/react-native-actions-sheetlodev09/react-native-true-sheet
我并未尝试所有可用库,但在公司内部大量使用了 gorhom/react-native-bottom-sheet。在使用过程中,我频繁遇到缺乏文档支撑的 bug,导致难以找到解决方案或替代方案。最常见的问题包括:
| 问题 | 描述 |
|---|---|
| Z‑index 冲突 | 同时打开多个底部表单时,层级顺序常常出现错误。 |
| 动态尺寸 | 自动尺寸在涉及滚动视图时表现不可靠。 |
| 手势抖动 | 拖动表单时偶尔会出现“跳动”。 |
| 键盘处理 | 在不同键盘场景下,焦点和布局的管理不一致。 |
我最初通过为库组件编写包装器来缓解这些问题,但这些只是临时补丁,未能根本解决根因。
最近,我对开源项目投入更多精力,并学习了库的维护与发布流程,于是决定重新审视这个问题。从零开始构建一个新库,设计上强调灵活的 API 并显式处理各种边缘情况。
欢迎前往查看项目(链接略)。
在本文中,我想分享该库背后的思考模型以及它旨在解决的具体问题。
表单系统(Sheet System)
Sheet 是一个由三层独立组成的系统:
-
堆栈项层(Stack Item Layer) – 将当前表单注册到堆栈中。堆栈会根据注册顺序生成唯一的 z‑index,确保在同时打开多个表单时能够按正确顺序显示。
-
呈现层(Presenter Layer) – 管理通用的呈现逻辑。它始终从 0 % 打开到 100 %,从 100 % 关闭到 0 %。正如你可以想象的,这一层可以从底部向上、从左向右,甚至斜向移动。
这就是默认支持动态尺寸的“秘密酱”。因为呈现层只关注过渡本身,不需要预先知道内容的大小;内容可以根据需要自行伸缩。
-
内容层(Content Layer) – 你实际想要展示的内容。它可以是仅包含文字的简单视图,也可以是带有手势和动画的复杂组件,例如底部表单。甚至可以是居中显示的视图,仍然能够完美工作。
Sheet 可用于多种场景,但在本例中,我将重点放在 底部表单——一种常见需求,且在处理所有边缘情况时实现起来尤为困难。
示例设置
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import {
SheetStackProvider,
SheetKeyboardProvider,
BottomSheetRegistryProvider,
} from "react-native-the-sheet";
import { PortalHost, PortalProvider } from "react-native-universe-portal";
export default function App() {
return (
<GestureHandlerRootView>
<SafeAreaProvider>
<SheetKeyboardProvider>
<SheetStackProvider>
<PortalProvider>
{/* Your app content */}
</PortalProvider>
</SheetStackProvider>
</SheetKeyboardProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
| Provider | 作用说明 |
|---|---|
| SafeAreaProvider | 必须用于计算可供底部弹窗使用的真实安全区高度。 |
| SheetKeyboardProvider | 在 Android 上正确处理键盘交互(尤其是非全屏布局、adjustResize 和 adjustPan 场景)。 |
| SheetStackProvider | 管理弹窗堆栈及其相对 z‑index。 |
| PortalProvider | 将内容传送至视图层级中的正确位置。 |
| BottomSheetRegistryProvider | 只需提供弹窗 ID,即可读取内部底部弹窗状态。 |
2. 使用组件构建底部弹出层
在本示例中,我们创建了一个简单的底部弹出层,具备以下特性:
- 动态尺寸
- 背景遮罩
- 把手
- 能够感知平移手势的静态内容(使用
BottomSheetView)
当你拖动把手或内容时,弹出层会跟随你的手指移动。
import { Fragment, useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
import {
Backdrop,
BottomSheet,
BottomSheetHandle,
BottomSheetPresenter,
BottomSheetProvider,
BottomSheetView,
SheetStackItem,
} from "react-native-the-sheet";
import { Portal } from "react-native-universe-portal";
export default function ExampleBottomSheetView() {
const [isOpenA, setIsOpenA] = useState(false);
const renderContent = () => (
<Fragment>
{Array.from({ length: 20 }).map((_, index) => (
<Text key={index}>Item {index + 1}</Text>
))}
</Fragment>
);
return (
<View style={styles.root}>
<Text style={styles.header}>Example Bottom Sheet View</Text>
<Button title="Open Sheet" onPress={() => setIsOpenA(true)} />
<SheetStackItem
isOpen={isOpenA}
onClose={() => setIsOpenA(false)}
waitForFullyExit
testID="sheetA"
>
<BottomSheetProvider>
<Backdrop />
<BottomSheetPresenter>
<BottomSheet>
<BottomSheetHandle />
<BottomSheetView>{renderContent()}</BottomSheetView>
</BottomSheet>
</BottomSheetPresenter>
</BottomSheetProvider>
</SheetStackItem>
</View>
);
}
const styles = StyleSheet.create({
root: { flex: 1, justifyContent: "center", alignItems: "center" },
header: { fontSize: 18, marginBottom: 12 },
});
组件说明
| 组件 | 作用 |
|---|---|
| SheetStackItem | 在堆栈中注册弹出层,控制其打开/关闭状态,并处理 z‑index 排序。 |
| BottomSheetProvider | 为底部弹出层内部提供上下文(例如手势、动画值)。 |
| Backdrop | 出现在弹出层后面的暗色背景。 |
| BottomSheetPresenter | 处理过渡动画(打开/关闭)和定位逻辑。 |
| BottomSheet | 定义弹出层视觉样式的容器(例如背景、圆角)。 |
| BottomSheetHandle | 可拖动的小条,表示弹出层可以被拉起。 |
| BottomSheetView | 包装器,使其子组件响应平移手势并允许动态尺寸。 |
TL;DR
- 三层架构(Stack → Presenter → Content)为你提供可靠的 z‑index 处理、灵活的动画方向以及真正的动态尺寸。
- SheetStackProvider + SheetStackItem 负责多个 sheet 的排序。
- Presenter 抽象了过渡逻辑,使得内容层无需提前知道自己的尺寸。
- Content 层可以是任何 React 组件,从简单的文本列表到完整的手势驱动视图皆可。
尝试一下这个库并告诉我你的感受!
<SheetStackItem
isOpen={isOpenA}
onClose={() => setIsOpenA(false)}
testID="sheetA"
>
<BottomSheetProvider>
<Backdrop />
<BottomSheetPresenter>
<BottomSheet>
<BottomSheetHandle />
<BottomSheetView>{renderContent()}</BottomSheetView>
</BottomSheet>
</BottomSheetPresenter>
</BottomSheetProvider>
</SheetStackItem>
// Styles
const styles = StyleSheet.create({
header: {
fontSize: 20,
fontWeight: "500",
},
root: {
flex: 1,
gap: 8,
padding: 16,
},
});
正如你可能已经注意到的,你不需要向这些组件传递很多属性。大多数功能都封装在组件内部,让你可以自由地混合和布局它们。
当然,也有例外情况,但我鼓励你发挥创意。你可能会发现一些我甚至还没想到的有用模式。
此外,这个示例仅仅触及了库功能的表面。还有许多支持各种特性的组件。查看完整的组件列表及其文档:
目前就这些!希望这能让你对库以及背后的思维模型有一个清晰的了解。如果有任何问题,欢迎在 GitHub 仓库留下评论或打开 issue:
我一直在寻找反馈和对新功能的建议,以在保持 API 简单灵活的同时,提升库的使用体验。