React Native 的 Sheet:模块化 Sheet 框架

发布: (2026年5月2日 GMT+8 08:49)
9 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的文章正文内容,我会按照要求将其译成简体中文并保留原有的格式、Markdown 语法以及技术术语。谢谢!

Source:

问题描述

在移动端向用户展示内容的方式有很多,例如弹窗、下拉菜单、底部表单(bottom sheet)和居中表单(center sheet)。在大多数应用中,这些都以 模态层(modal)的形式出现,即位于界面其余部分之上的聚焦层。

在 React Native 中,内置的 Modal 组件实现了这一功能。但它的 API 范围有限,并且与原生的呈现行为紧密耦合。一旦需要自定义行为、复杂的手势系统或多个模态层之间的细致交互时,往往会陷入与框架的“搏斗”。

为填补这一空白,出现了若干库:

  • gorhom/react-native-bottom-sheet
  • ammarahm-ed/react-native-actions-sheet
  • lodev09/react-native-true-sheet

我并未尝试所有可用库,但在公司内部大量使用了 gorhom/react-native-bottom-sheet。在使用过程中,我频繁遇到缺乏文档支撑的 bug,导致难以找到解决方案或替代方案。最常见的问题包括:

问题描述
Z‑index 冲突同时打开多个底部表单时,层级顺序常常出现错误。
动态尺寸自动尺寸在涉及滚动视图时表现不可靠。
手势抖动拖动表单时偶尔会出现“跳动”。
键盘处理在不同键盘场景下,焦点和布局的管理不一致。

我最初通过为库组件编写包装器来缓解这些问题,但这些只是临时补丁,未能根本解决根因。

最近,我对开源项目投入更多精力,并学习了库的维护与发布流程,于是决定重新审视这个问题。从零开始构建一个新库,设计上强调灵活的 API 并显式处理各种边缘情况。

欢迎前往查看项目(链接略)。

在本文中,我想分享该库背后的思考模型以及它旨在解决的具体问题。

表单系统(Sheet System)

Sheet 是一个由三层独立组成的系统:

  1. 堆栈项层(Stack Item Layer) – 将当前表单注册到堆栈中。堆栈会根据注册顺序生成唯一的 z‑index,确保在同时打开多个表单时能够按正确顺序显示。

  2. 呈现层(Presenter Layer) – 管理通用的呈现逻辑。它始终从 0 % 打开到 100 %,从 100 % 关闭到 0 %。正如你可以想象的,这一层可以从底部向上、从左向右,甚至斜向移动。

    这就是默认支持动态尺寸的“秘密酱”。因为呈现层只关注过渡本身,不需要预先知道内容的大小;内容可以根据需要自行伸缩。

  3. 内容层(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 上正确处理键盘交互(尤其是非全屏布局、adjustResizeadjustPan 场景)。
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 简单灵活的同时,提升库的使用体验。

0 浏览
Back to Blog

相关文章

阅读更多 »

Spring Boot 中的 MVC 架构流程

MVC Architecture Flow in Spring Boot 的封面图片 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2F...