Skyroc Native UI

Picker

滚轮选择器,支持单列、多列与级联

选择器(Picker)用滚轮从若干选项里选值。包含两个组件:PickerView 是内联滚轮,Picker 把它装进底部弹层并额外管理「确定才提交」的语义。滚动跑在 react-native-reanimated 的 UI 线程上,选项的透明度与缩放随距中心的距离衰减。

import { Picker, PickerView } from '@skyroc/native-ui';

Picker 依赖 @gorhom/bottom-sheet,请确保 App 根节点已经包了 GestureHandlerRootViewBottomSheetModalProvider

基础用法

columns 传一维数组即为单列。选中值统一是 string[](单列也是长度为 1 的数组),defaultValue 设置初始值。

PickerBasic.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { PickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

const FRUITS: PickerOption[] = [
  { label: '苹果', value: 'apple' },
  { label: '香蕉', value: 'banana' },
  { label: '橘子', value: 'orange' },
  { label: '葡萄', value: 'grape' },
  { label: '西瓜', value: 'watermelon' },
  { label: '桃子', value: 'peach' },
  { label: '梨', value: 'pear' }
];

const PickerBasic = () => {
  return (
    <View className="bg-background p-4">
      <PickerView
        columns={FRUITS}
        defaultValue={['orange']}
        title="选择水果"
      />
    </View>
  );
};

export { PickerBasic };

何时使用

  • 从一组互斥选项里选值,且选项多到不适合平铺(城市、日期、规格)。
  • 选项少于 5 个、又要一眼看全时用 RadioActionSheet
  • 选日期 / 时间请直接用 DatePicker / TimePicker,它们在 Picker 之上补了范围与格式化。

受控模式

value + onChange 即为受控。外部改值时滚轮会同步滚过去 —— 但用户正在滚动时不抢,避免和手指打架。

PickerControlled.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { Button, PickerView, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const COLORS: PickerOption[] = [
  { label: '红色', value: 'red' },
  { label: '绿色', value: 'green' },
  { label: '蓝色', value: 'blue' },
  { label: '紫色', value: 'purple' }
];

const PickerControlled = () => {
  const [value, setValue] = useState<string[]>(['green']);

  return (
    <View className="bg-background p-4">
      <View className="mb-3 flex-row flex-wrap items-center gap-3">
        <Button
          size="sm"
          variant="tonal"
          onPress={() => setValue(['blue'])}
        >
          外部选中蓝色
        </Button>
        <Text className="text-sm text-muted-foreground">当前 value:{value.join(', ')}</Text>
      </View>
      <PickerView
        columns={COLORS}
        showToolbar={false}
        value={value}
        onChange={setValue}
      />
    </View>
  );
};

export { PickerControlled };

多列选择

columns 传二维数组(PickerOption[][])时各列相互独立,互不联动。

PickerMultiColumn.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { PickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

const YEARS: PickerOption[] = Array.from({ length: 10 }, (_, i) => ({
  label: `${2020 + i} 年`,
  value: `${2020 + i}`
}));

const MONTHS: PickerOption[] = Array.from({ length: 12 }, (_, i) => ({
  label: `${i + 1} 月`,
  value: `${i + 1}`
}));

const DAYS: PickerOption[] = Array.from({ length: 31 }, (_, i) => ({
  label: `${i + 1} 日`,
  value: `${i + 1}`
}));

const PickerMultiColumn = () => {
  return (
    <View className="bg-background p-4">
      <PickerView
        columns={[YEARS, MONTHS, DAYS]}
        defaultValue={['2026', '2', '21']}
        title="选择日期"
      />
    </View>
  );
};

export { PickerMultiColumn };

级联选择

选项带 children 时自动进入级联模式,按当前选中值逐级展开。不传初值也会自动补齐各级 —— 列的展开深度取决于选中值,而选中值合不合法又取决于列,组件会迭代到不动点,因此三级联动首屏就是三列,而不是滚一下才冒出下一列。

PickerCascade.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { PickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 级联示例:省 → 市 → 区 */
const REGIONS: PickerOption[] = [
  {
    label: '浙江',
    value: 'zhejiang',
    children: [
      {
        label: '杭州',
        value: 'hangzhou',
        children: [
          { label: '西湖区', value: 'xihu' },
          { label: '滨江区', value: 'binjiang' },
          { label: '余杭区', value: 'yuhang' }
        ]
      },
      {
        label: '宁波',
        value: 'ningbo',
        children: [
          { label: '海曙区', value: 'haishu' },
          { label: '江北区', value: 'jiangbei' }
        ]
      }
    ]
  },
  {
    label: '江苏',
    value: 'jiangsu',
    children: [
      {
        label: '南京',
        value: 'nanjing',
        children: [
          { label: '玄武区', value: 'xuanwu' },
          { label: '鼓楼区', value: 'gulou' }
        ]
      },
      {
        label: '苏州',
        value: 'suzhou',
        children: [
          { label: '姑苏区', value: 'gusu' },
          { label: '虎丘区', value: 'huqiu' }
        ]
      }
    ]
  }
];

const PickerCascade = () => {
  return (
    <View className="bg-background p-4">
      <PickerView
        columns={REGIONS}
        title="选择地区"
      />
    </View>
  );
};

export { PickerCascade };

改上一级会换掉下一级的整份数据,此时下级会重新落到第一个可用项。

自定义字段

数据里没有 label / value / children 时,用 fieldNames 映射:

字段默认值含义
label'label'显示文本
value'value'选项值
children'children'级联子选项
PickerFieldNames.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { PickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 字段名映射示例:数据里根本没有 label / value / children 这几个 key */
const DEPARTMENTS: PickerOption[] = [
  {
    id: 'tech',
    name: '技术部',
    sub: [
      { id: 'fe', name: '前端组' },
      { id: 'be', name: '后端组' }
    ]
  },
  {
    id: 'design',
    name: '设计部',
    sub: [
      { id: 'ui', name: '视觉组' },
      { id: 'ux', name: '交互组' }
    ]
  }
];

const PickerFieldNames = () => {
  return (
    <View className="bg-background p-4">
      <PickerView
        columns={DEPARTMENTS}
        fieldNames={{ children: 'sub', label: 'name', value: 'id' }}
        title="选择部门"
      />
    </View>
  );
};

export { PickerFieldNames };

禁用选项

选项上的 disabled 让它不可选:滚轮停下时会自动吸附到最近的可用项(先往后找、再往前找),初始值落在禁用项上时同样会被修正。禁用项在距离衰减之上再打 0.4 的透明度折扣。

PickerDisabled.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { PickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 带禁用项的单列示例 */
const SEATS: PickerOption[] = [
  { label: 'A 排(已满)', value: 'a', disabled: true },
  { label: 'B 排', value: 'b' },
  { label: 'C 排(已满)', value: 'c', disabled: true },
  { label: 'D 排(已满)', value: 'd', disabled: true },
  { label: 'E 排', value: 'e' },
  { label: 'F 排', value: 'f' }
];

const PickerDisabled = () => {
  return (
    <View className="bg-background p-4">
      <PickerView
        columns={SEATS}
        showToolbar={false}
      />
    </View>
  );
};

export { PickerDisabled };

滚轮尺寸与反馈

属性含义默认值
itemHeight每个选项的高度(px)48
visibleCount每列可见的选项数,必须是奇数5
haptic滚过一格时触发一次轻触反馈false
PickerWheel.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { PickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

const SIZES: PickerOption[] = [
  { label: 'XS', value: 'xs' },
  { label: 'S', value: 'sm' },
  { label: 'M', value: 'md' },
  { label: 'L', value: 'lg' },
  { label: 'XL', value: 'xl' }
];

const PickerWheel = () => {
  return (
    <View className="bg-background p-4">
      <PickerView
        haptic
        classNames={{ selectedIndicator: 'border-primary/30 bg-primary/5' }}
        columns={SIZES}
        defaultValue={['md']}
        itemHeight={40}
        showToolbar={false}
        visibleCount={3}
      />
    </View>
  );
};

export { PickerWheel };

visibleCount 取偶数时中心格落在两格之间,选中指示线会对不齐。滚轮总高度是 visibleCount × itemHeight,指示线的位置由两者算出。

加载状态

loading 在滚轮区域盖一层半透明遮罩和指示器,工具栏仍然可见可点。

PickerLoading.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { Button, PickerView } from '@skyroc/native-ui';
import { useEffect, useState } from 'react';
import { View } from 'react-native';

const FRUITS: PickerOption[] = [
  { label: '苹果', value: 'apple' },
  { label: '香蕉', value: 'banana' },
  { label: '橘子', value: 'orange' },
  { label: '葡萄', value: 'grape' },
  { label: '西瓜', value: 'watermelon' },
  { label: '桃子', value: 'peach' },
  { label: '梨', value: 'pear' }
];

/** 异步加载的模拟耗时(ms) */
const MOCK_LOADING_DELAY = 1500;

const PickerLoading = () => {
  const [loading, setLoading] = useState(true);

  function reload() {
    setLoading(true);

    setTimeout(() => {
      setLoading(false);
    }, MOCK_LOADING_DELAY);
  }

  useEffect(() => {
    reload();
  }, []);

  return (
    <View className="bg-background p-4">
      <View className="mb-4">
        <Button
          variant="tonal"
          onPress={reload}
        >
          重新加载
        </Button>
      </View>
      <PickerView
        columns={loading ? [] : FRUITS}
        defaultValue={['apple']}
        loading={loading}
        showToolbar={false}
      />
    </View>
  );
};

export { PickerLoading };

工具栏与回调

showToolbar(默认 true)控制顶部工具栏,title / cancelText / confirmText 定制文案。onCancelonConfirm 都会回传当前选中值。

PickerToolbar.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { PickerView, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const PRIORITIES: PickerOption[] = [
  { label: '低', value: 'low' },
  { label: '中', value: 'medium' },
  { label: '高', value: 'high' }
];

const PickerToolbar = () => {
  const [feedback, setFeedback] = useState('点击工具栏按钮查看回调结果');

  function handleCancel(values: string[]) {
    setFeedback(`onCancel:${values.join(', ')}`);
  }

  function handleConfirm(values: string[]) {
    setFeedback(`onConfirm:${values.join(', ')}`);
  }

  return (
    <View className="bg-background p-4">
      <Text className="mb-2 text-sm text-muted-foreground">{feedback}</Text>
      <PickerView
        cancelText="返回"
        columns={PRIORITIES}
        confirmText="选定"
        defaultValue={['medium']}
        title="选择优先级"
        onCancel={handleCancel}
        onConfirm={handleConfirm}
      />
    </View>
  );
};

export { PickerToolbar };

PickerView 里这两个回调只是通知,值早已随滚动提交;真正区分「滚动值」与「已确认值」的是 Picker(见下文)。

样式覆盖

className 追加到滚轮块的根容器上,classNames 按 slot 细粒度覆盖:

slot作用位置
root滚轮块根容器(圆角、背景)
toolbar顶部工具栏容器
cancel取消按钮容器
cancelText取消按钮文字
title工具栏标题
confirm确定按钮容器
confirmText确定按钮文字
columns所有列的横向容器
column单列容器
item单个选项容器
itemText选项文字
selectedIndicator中间的选中指示线
loading加载遮罩
PickerStyles.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { PickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

const LEVELS: PickerOption[] = [
  { label: '入门', value: 'starter' },
  { label: '进阶', value: 'advanced' },
  { label: '专家', value: 'expert' }
];

const PickerStyles = () => {
  return (
    <View className="bg-muted p-4">
      <PickerView
        className="border border-primary/20"
        classNames={{
          itemText: 'text-primary',
          selectedIndicator: 'border-primary/40 bg-primary/5',
          title: 'text-primary'
        }}
        columns={LEVELS}
        defaultValue={['advanced']}
        title="选择级别"
      />
    </View>
  );
};

export { PickerStyles };

弹层提交与关闭

PickerPickerView 之上加了一层「确定才提交」:面板打开期间的滚动只写内部临时值,点确定才写进 value 并触发 onConfirm,取消则整份丢弃。每次打开都会把临时值重置回已确认值,上一次取消掉的滚动不会残留。

PickerPopup.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { Button, Picker, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const FRUITS: PickerOption[] = [
  { label: '苹果', value: 'apple' },
  { label: '香蕉', value: 'banana' },
  { label: '橘子', value: 'orange' },
  { label: '葡萄', value: 'grape' },
  { label: '西瓜', value: 'watermelon' },
  { label: '桃子', value: 'peach' },
  { label: '梨', value: 'pear' }
];

const PickerPopup = () => {
  const [fruitShow, setFruitShow] = useState(false);
  const [fruitValue, setFruitValue] = useState<string[]>(['orange']);
  const [feedback, setFeedback] = useState('尚未操作');

  function handleCancel(values: string[]) {
    setFeedback(`已取消临时值 ${values.join(', ')}`);
  }

  function handleConfirm(values: string[]) {
    setFruitValue(values);
    setFeedback(`已确认 ${values.join(', ')}`);
  }

  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() => setFruitShow(true)}
      >
        打开选择器
      </Button>
      <Text color="muted">当前:{fruitValue.join(', ') || '未选择'}</Text>
      <Text className="w-full text-sm text-muted-foreground">{feedback}</Text>

      <Picker
        enablePanDownToClose
        showHandle
        columns={FRUITS}
        sheetClassName="border border-primary/15"
        sheetClassNames={{ handleBar: 'bg-primary/40' }}
        show={fruitShow}
        title="选择水果"
        value={fruitValue}
        onCancel={handleCancel}
        onConfirm={handleConfirm}
        onUpdateShow={setFruitShow}
      />
    </View>
  );
};

export { PickerPopup };

滚轮需要独占垂直手势,所以 Picker 关掉了面板的内容拖拽(enableContentPanningGesture)—— 否则在列上下拉会拖动整个面板而不是滚动滚轮。下拉关闭的通道因此只剩顶部 handle:enablePanDownToClose 要配合 showHandle 一起传,否则开了也无处可拖。

自定义触发元素与 ref

children 可以是节点(自动包一层 Pressable 打开面板),也可以是渲染函数,参数为 { open, value } —— value已确认值,适合直接显示在触发行上。ref 原样透传给底层的 BottomSheetModal,可以调 dismiss / snapToIndex / expand 这类 show 表达不了的命令式操作。

PickerTrigger.tsx
Loading…
import type { PickerOption } from '@skyroc/native-ui';
import { BottomSheetModal, Button, Cell, Picker } from '@skyroc/native-ui';
import type { ComponentRef } from 'react';
import { useRef, useState } from 'react';
import { View } from 'react-native';

/** 级联示例:省 → 市 → 区 */
const REGIONS: PickerOption[] = [
  {
    label: '浙江',
    value: 'zhejiang',
    children: [
      {
        label: '杭州',
        value: 'hangzhou',
        children: [
          { label: '西湖区', value: 'xihu' },
          { label: '滨江区', value: 'binjiang' },
          { label: '余杭区', value: 'yuhang' }
        ]
      },
      {
        label: '宁波',
        value: 'ningbo',
        children: [
          { label: '海曙区', value: 'haishu' },
          { label: '江北区', value: 'jiangbei' }
        ]
      }
    ]
  },
  {
    label: '江苏',
    value: 'jiangsu',
    children: [
      {
        label: '南京',
        value: 'nanjing',
        children: [
          { label: '玄武区', value: 'xuanwu' },
          { label: '鼓楼区', value: 'gulou' }
        ]
      },
      {
        label: '苏州',
        value: 'suzhou',
        children: [
          { label: '姑苏区', value: 'gusu' },
          { label: '虎丘区', value: 'huqiu' }
        ]
      }
    ]
  }
];

const PickerTrigger = () => {
  const [regionShow, setRegionShow] = useState(false);
  const [regionValue, setRegionValue] = useState<string[]>([]);

  const sheetRef = useRef<ComponentRef<typeof BottomSheetModal>>(null);

  const regionLabel = regionValue.length > 0 ? regionValue.join(' / ') : '请选择';

  /** 先用 show 打开,再通过底层实例执行命令式关闭 */
  function handleOpenAndDismissByRef() {
    setRegionShow(true);
    setTimeout(() => sheetRef.current?.dismiss(), 2500);
  }

  return (
    <View className="bg-background p-4">
      <View className="overflow-hidden rounded-xl border border-border">
        <Picker
          ref={sheetRef}
          columns={REGIONS}
          show={regionShow}
          title="选择地区"
          value={regionValue}
          onConfirm={setRegionValue}
          onUpdateShow={setRegionShow}
        >
          {args => (
            <Cell
              showArrow
              title="所在地区"
              trailing={regionLabel}
              onPress={args.open}
            />
          )}
        </Picker>
      </View>
      <Button
        className="mt-3"
        variant="outline"
        onPress={handleOpenAndDismissByRef}
      >
        打开后用 ref 关闭
      </Button>
    </View>
  );
};

export { PickerTrigger };

API

PickerView

属性说明类型默认值
columns*列数据:一维数组为单列,二维数组为多列,带 children 的一维数组为级联PickerOption[] | PickerOption[][]-
value选中值(受控),每列一个string[]-
defaultValue非受控初始选中值string[][]
onChange任意一列选中值变化时触发(values: string[]) => void-
fieldNames自定义字段名映射PickerFieldNames-
itemHeight每个选项的高度(px)number48
visibleCount每列可见的选项数,必须取奇数number5
haptic滚过一格时触发轻触反馈booleanfalse
loading在滚轮区域显示加载遮罩booleanfalse
showToolbar是否显示顶部工具栏booleantrue
title工具栏标题string-
cancelText取消按钮文字string'取消'
confirmText确定按钮文字string'确定'
onCancel点击取消的回调,回传当前选中值(values: string[]) => void-
onConfirm点击确定的回调,回传当前选中值(values: string[]) => void-
className滚轮块根容器类名string-
classNames各 slot 的类名覆盖,见「样式覆盖」一节SlotClassNames<PickerSlots>-

Picker

Picker 继承 PickerView 的全部属性(columns / value / fieldNames / itemHeight 等),其中 value 表示已确认值:面板内的滚动不会写它,点确定才提交。

属性说明类型默认值
show*是否显示弹层boolean-
onUpdateShow显示状态变化回调(show: boolean) => void-
children触发元素:节点会被包一层 Pressable,渲染函数可拿到 open 与已确认 valueReactNode | ((params: { open: () => void; value: string[] }) => ReactNode)-
closeOnBackdropPress点击遮罩是否关闭booleantrue
showHandle是否显示面板顶部的拖拽指示条booleanfalse
enablePanDownToClose是否允许下拉关闭,需要同时开启 showHandle 才有可拖之处booleanfalse
sheetClassName内部 Sheet 面板本体的类名(className 给的是滚轮那块)string-
sheetClassNames内部 Sheet 各 slot 的类名覆盖SlotClassNames<SheetSlots>-
ref底层 BottomSheetModal 的实例引用,用于 dismiss / snapToIndex / expand 等命令式操作Ref<BottomSheetModal>-

类型

import type { PickerColumnType, PickerFieldNames, PickerOption, PickerProps, PickerSlots, PickerViewProps } from '@skyroc/native-ui';

PickerColumnType

列数据形态,由 columns 的结构自动推断。

'cascade' | 'multiple' | 'single'

PickerSlots

可通过 classNames 覆盖的 slot 名称。

'cancel' | 'cancelText' | 'column' | 'columns' | 'confirm' | 'confirmText' | 'item' | 'itemText' | 'loading' | 'root' | 'selectedIndicator' | 'title' | 'toolbar'

SlotClassNames

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

Partial<Record<Slots, string>>

PickerOption

单个选项。字段名可通过 fieldNames 改写,因此 label / value 都是可选的,且允许携带任意额外字段。

字段类型说明
labelstring显示文本,字段名可由 fieldNames.label 改写。
valuestring选项值,字段名可由 fieldNames.value 改写。
childrenPickerOption[]级联子选项,字段名可由 fieldNames.children 改写。
disabledboolean是否禁用该选项,滚轮会吸附到最近的可用项。

PickerFieldNames

选项对象的字段名映射。

字段类型说明
labelstring显示文本字段名,默认 'label'。
valuestring选项值字段名,默认 'value'。
childrenstring子选项字段名,默认 'children'。

包内还导出了 pickerVariants