Skyroc Native UI

Sheet

底部面板,支持吸附档位与动态高度

底部面板(Sheet)基于 @gorhom/bottom-sheet 封装:支持下拉关闭、吸附档位、按内容动态撑高,标题栏是固定不滚的顶部区。PickerPickerGroupTimePicker 的弹层形态都建立在它之上。

import { Sheet, BottomSheetView } from '@skyroc/native-ui';

使用前需要在 App 根节点挂上 GestureHandlerRootViewBottomSheetModalProvider(后者由本包再导出)。

基础用法

show 控制显隐,onUpdateShow必填的 —— 下拉或点遮罩关闭后没人把 show 置为 false,面板会卡死。不传 snapPoints 时走动态尺寸,高度由内容撑开。

SheetBasic.tsx
Loading…
import { BottomSheetView, Button, Sheet, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const SheetBasic = () => {
  const [show, setShow] = useState(false);

  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() => setShow(true)}
      >
        打开面板
      </Button>

      <Sheet
        show={show}
        title="基础面板"
        onUpdateShow={setShow}
      >
        <BottomSheetView className="gap-3 px-6 pb-safe-offset-6">
          <Text color="muted">下拉、点遮罩、点右上角关闭按钮都能收起面板</Text>
          <Button
            variant="outline"
            onPress={() => setShow(false)}
          >
            关闭
          </Button>
        </BottomSheetView>
      </Sheet>
    </View>
  );
};

export { SheetBasic };

内容必须自带 gorhom 的容器组件:普通内容用 BottomSheetView,长列表用 BottomSheetFlatList / BottomSheetScrollView / BottomSheetSectionList。Sheet 不代为包裹 —— 外层若再套一层 BottomSheetView,它的 effect 会在子组件之后把 scrollable 注册覆盖成 VIEW 类型,内层列表的滚动手势就被吞掉了。

这些容器都由本包再导出:

import {
  BottomSheetFlatList,
  BottomSheetModalProvider,
  BottomSheetScrollView,
  BottomSheetSectionList,
  BottomSheetTextInput,
  BottomSheetView
} from '@skyroc/native-ui';

何时使用

  • 需要在当前页面之上补充操作或信息,且内容较多、可能需要滚动。
  • 一句话确认用 Dialog;一列动作选项用 ActionSheet;完全自定义的覆盖层用 Popup

标题与描述

title / description 组成顶部固定区,closeable(默认开启)在右上角渲染关闭按钮。

SheetHeader.tsx
Loading…
import { BottomSheetView, Button, Sheet, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const SheetHeader = () => {
  const insets = useSafeAreaInsets();

  const [titleShow, setTitleShow] = useState(false);
  const [noTitleShow, setNoTitleShow] = useState(false);
  const [plainShow, setPlainShow] = useState(false);

  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() => setTitleShow(true)}
      >
        标题 + 描述
      </Button>

      <Sheet
        description="描述显示在标题下方,同属顶部固定区"
        show={titleShow}
        title="标题与描述"
        onUpdateShow={setTitleShow}
      >
        <BottomSheetView
          className="gap-3 px-6"
          style={{ paddingBottom: insets.bottom + 24 }}
        >
          <Text color="muted">标题居中,关闭按钮绝对定位在右侧,不参与居中计算</Text>
        </BottomSheetView>
      </Sheet>

      <Button
        variant="tonal"
        onPress={() => setNoTitleShow(true)}
      >
        只有关闭按钮
      </Button>

      {/* 没有 title 时 header 只剩关闭按钮,验证它不会压到下面的内容上 */}
      <Sheet
        show={noTitleShow}
        onUpdateShow={setNoTitleShow}
      >
        <BottomSheetView
          className="gap-3 px-6"
          style={{ paddingBottom: insets.bottom + 24 }}
        >
          <Text color="muted">没有标题,但 header 仍然撑出高度,关闭按钮不会盖住这行字</Text>
        </BottomSheetView>
      </Sheet>

      <Button
        variant="tonal"
        onPress={() => setPlainShow(true)}
      >
        无顶部区
      </Button>

      {/* handle / 标题 / 描述全空时,Sheet 会给 gorhom 传 handleComponent={null} */}
      <Sheet
        closeable={false}
        show={plainShow}
        showHandle={false}
        onUpdateShow={setPlainShow}
      >
        <BottomSheetView
          className="gap-3 px-6 pt-6"
          style={{ paddingBottom: insets.bottom + 24 }}
        >
          <Text color="muted">顶部固定区整块不渲染,handleHeight 直接置 0</Text>
          <Button
            variant="outline"
            onPress={() => setPlainShow(false)}
          >
            关闭
          </Button>
        </BottomSheetView>
      </Sheet>
    </View>
  );
};

export { SheetHeader };

顶部区走的是 gorhom 的 handleComponent 而不是塞进内容里:gorhom 会单独量它的高度并计入动态档位,又从内容区高度里扣掉,这样内容区完全留给你的容器,标题也不会跟着列表滚。

吸附高度

snapPoints(如 ['25%', '50%'])后高度固定下来,可以在几档之间拖动;此时动态尺寸自动关闭。

SheetSnapPoints.tsx
Loading…
import { BottomSheetView, Button, Sheet, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const SheetSnapPoints = () => {
  const [show, setShow] = useState(false);

  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() => setShow(true)}
      >
        40% / 75%
      </Button>

      <Sheet
        show={show}
        snapPoints={['40%', '75%']}
        title="吸附高度"
        onUpdateShow={setShow}
      >
        <BottomSheetView className="gap-3 px-6">
          <Text color="muted">往上拖到 75%,再往下拖回 40%,继续下拉才会关闭</Text>
        </BottomSheetView>
      </Sheet>
    </View>
  );
};

export { SheetSnapPoints };

需要在档位间程序化切换时用 ref,它指向底层的 BottomSheetModal,可调 snapToIndex / expand / collapse 这类 show 表达不了的操作。

列表内容

长列表直接用 BottomSheetFlatList 当内容。底部安全区留白要写进 contentContainerStyle 才会跟着滚动 —— 写在外层容器上只会压缩滚动视口。

SheetList.tsx
Loading…
import { BottomSheetFlatList, Button, Cell, Sheet, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

/** 条数刻意多于一屏,用来验证面板内部滚动 */
const CITIES = [
  '北京',
  '上海',
  '广州',
  '深圳',
  '杭州',
  '成都',
  '武汉',
  '西安',
  '南京',
  '重庆',
  '苏州',
  '天津',
  '长沙',
  '青岛',
  '厦门',
  '合肥'
];

const SheetList = () => {
  const insets = useSafeAreaInsets();

  const [show, setShow] = useState(false);
  const [picked, setPicked] = useState('');

  function handlePick(city: string) {
    setPicked(city);
    setShow(false);
  }

  function renderCity(info: { item: string }) {
    return (
      <Cell
        showArrow
        title={info.item}
        onPress={() => handlePick(info.item)}
      />
    );
  }

  return (
    <View className="gap-2 bg-background p-4">
      <View className="flex-row flex-wrap items-center gap-3">
        <Button
          variant="tonal"
          onPress={() => setShow(true)}
        >
          选择城市
        </Button>

        <Sheet
          show={show}
          snapPoints={['60%']}
          title="选择城市"
          onUpdateShow={setShow}
        >
          <BottomSheetFlatList
            contentContainerStyle={{ paddingBottom: insets.bottom }}
            data={CITIES}
            keyExtractor={city => city}
            renderItem={renderCity}
          />
        </Sheet>
      </View>

      <Text color="muted">{picked ? `已选择:${picked}` : '还没选'}</Text>
    </View>
  );
};

export { SheetList };

关闭行为

closeOnBackdropPress(默认 true)与 enablePanDownToClose(默认 true)分别控制点遮罩与下拉关闭。都关掉后就只剩右上角的关闭按钮这一条出口。

SheetCloseBehavior.tsx
Loading…
import { BottomSheetView, Button, Sheet, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const SheetCloseBehavior = () => {
  const insets = useSafeAreaInsets();

  const [show, setShow] = useState(false);

  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() => setShow(true)}
      >
        只能按按钮关闭
      </Button>

      <Sheet
        closeOnBackdropPress={false}
        enablePanDownToClose={false}
        show={show}
        title="锁定关闭"
        onUpdateShow={setShow}
      >
        <BottomSheetView
          className="gap-3 px-6"
          style={{ paddingBottom: insets.bottom + 24 }}
        >
          <Text color="muted">点遮罩没反应,下拉也拉不走;Android 返回键仍然可以关闭</Text>
        </BottomSheetView>
      </Sheet>
    </View>
  );
};

export { SheetCloseBehavior };

Android 硬件返回键在面板打开时先关闭面板,不返回上一页。

动态尺寸 + 滚动

不传 snapPoints 也能滚:列表自己上报内容高度,长到屏幕上限后就在面板内部滚动。

SheetDynamicHeight.tsx
Loading…
import { BottomSheetFlatList, Button, Cell, Sheet } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

/** 条数刻意多于一屏,用来验证动态尺寸下的内部滚动 */
const CITIES = [
  '北京',
  '上海',
  '广州',
  '深圳',
  '杭州',
  '成都',
  '武汉',
  '西安',
  '南京',
  '重庆',
  '苏州',
  '天津',
  '长沙',
  '青岛',
  '厦门',
  '合肥'
];

/** 列表项只用来撑高内容,不接管点击 */
function renderCity(info: { item: string }) {
  return (
    <Cell
      showArrow
      title={info.item}
    />
  );
}

const SheetDynamicHeight = () => {
  const insets = useSafeAreaInsets();

  const [show, setShow] = useState(false);

  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() => setShow(true)}
      >
        很长的内容
      </Button>

      <Sheet
        show={show}
        title="超长内容"
        onUpdateShow={setShow}
      >
        <BottomSheetFlatList
          contentContainerStyle={{ paddingBottom: insets.bottom }}
          data={CITIES}
          keyExtractor={city => city}
          renderItem={renderCity}
        />
      </Sheet>
    </View>
  );
};

export { SheetDynamicHeight };

样式覆盖

classNameclassNames.background 都作用于面板本体(背景 + 圆角),classNames 覆盖顶部区的各 slot:

slot作用位置
background面板本体的底色与圆角
chrome顶部固定区整体(handle + header + description)
handle拖拽指示条的容器
handleBar拖拽指示条本身
header标题行容器
title标题文字
description描述文字
close关闭按钮容器
closeIcon关闭图标的 colorClassName,只接受 accent-* 颜色类

面板底色与圆角走 gorhom 自带的 backgroundStyle(组件用 useResolveClassNames 把类名解析成 style),而不是自定义 backgroundComponent —— 自定义组件每次渲染都是新类型,背景层会跟着卸载重挂,还会丢掉默认背景自带的无障碍属性。background 的类名里 rounded-b-none 是必须的:gorhom 默认背景自带 borderRadius: 15,只覆盖上圆角的话,贴着屏幕底边的两个下角会留出缺口透出遮罩。

API

Sheet

除下表外,Sheet 透传 BottomSheetModal 的属性(indexenableContentPanningGesturekeyboardBehaviorstackBehavior 等)。以下属性由组件接管,不可透传:snapPoints(用同名属性)、enableDynamicSizinghandleComponentbackdropComponentbackgroundComponentbackgroundStyleonDismisschildrenref

属性说明类型默认值
show*是否显示面板boolean-
onUpdateShow*显示状态变化回调;必填,否则关闭后 show 无人置为 false,面板会卡死(show: boolean) => void-
children面板内容,必须自带 BottomSheetView / BottomSheetFlatList 等容器ReactNode-
title面板标题,string / number 会被包裹为 TextReactNode-
description描述信息,显示在标题下方ReactNode-
closeable是否显示右上角关闭按钮booleantrue
showHandle是否显示拖拽指示条booleantrue
snapPoints吸附点,如 ['25%', '50%'];不传则按内容高度动态撑开BottomSheetModalProps["snapPoints"]-
enablePanDownToClose是否允许下拉关闭booleantrue
closeOnBackdropPress是否允许点击遮罩关闭booleantrue
className面板本体(背景 + 圆角)的类名string-
classNames各 slot 的类名覆盖,见「样式覆盖」一节SlotClassNames<SheetSlots>-
ref底层 BottomSheetModal 的实例引用,用于 snapToIndex / expand / collapseRef<BottomSheetModal>-

类型

import type { SheetProps, SheetSlots } from '@skyroc/native-ui';

SheetSlots

可通过 classNames 覆盖的 slot 名称。

'background' | 'chrome' | 'close' | 'closeIcon' | 'description' | 'handle' | 'handleBar' | 'header' | 'title'

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 SheetSlots。

Partial<Record<Slots, string>>

包内还导出了 sheetVariants,以及从 @gorhom/bottom-sheet 再导出的 BottomSheetModalBottomSheetModalProviderBottomSheetViewBottomSheetScrollViewBottomSheetFlatListBottomSheetSectionListBottomSheetTextInput 与调试用的 enableLogging