Skyroc Native UI

PickerGroup

分组选择器,多个滚轮共用一套工具栏按 tab 逐个填

分组选择器(PickerGroup)把多个 PickerView 组织成 tab,共用一套工具栏,主按钮在非末尾 tab 上是「下一步」、走到最后一个才是「确定」。典型场景是「起始时间 / 结束时间」这类需要分步填写的组合。包含两个组件:PickerGroupView 内联,PickerGroup 装进底部弹层。

import { PickerGroup, PickerGroupView } from '@skyroc/native-ui';

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

基础用法与事件

pickers 声明每个 tab 的标题与列数据,选中值是 string[][](外层按 tab、内层按列)。

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

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}`
}));

/** 两段行程:每个 tab 各是一个独立的多列选择器 */
const TRIP_PICKERS: PickerGroupItem[] = [
  { columns: [MONTHS, DAYS], defaultValue: ['3', '1'], key: 'depart', title: '去程' },
  { columns: [MONTHS, DAYS], defaultValue: ['3', '8'], key: 'return', title: '返程' }
];

const PickerGroupBasic = () => {
  const [liveValues, setLiveValues] = useState<string[][]>([]);
  const [lastEvent, setLastEvent] = useState('尚未触发');

  const liveLabel = liveValues.length > 0 ? JSON.stringify(liveValues) : '尚未滚动';

  function handleCancel() {
    setLastEvent('onCancel');
  }

  function handleChange(values: string[][], pickerIndex: number) {
    setLiveValues(values);
    setLastEvent(`onChange:第 ${pickerIndex + 1} 个选择器`);
  }

  function handleConfirm() {
    setLastEvent('onConfirm');
  }

  function handleTabChange(index: number) {
    setLastEvent(`onTabChange:第 ${index + 1} 个 tab`);
  }

  return (
    <View className="bg-background px-6">
      <View className="mb-8">
        <PickerGroupView
          pickers={TRIP_PICKERS}
          onCancel={handleCancel}
          onChange={handleChange}
          onConfirm={handleConfirm}
          onTabChange={handleTabChange}
        />
        <Text
          className="mt-2"
          color="muted"
        >
          onChange 实时回传:{liveLabel}
        </Text>
        <Text
          className="mt-1"
          color="muted"
        >
          最近事件:{lastEvent}
        </Text>
      </View>
    </View>
  );
};

export { PickerGroupBasic };

四个回调各司其职:onChange 在滚动过程中实时触发并回传变化的 tab 下标,onTabChange 在切 tab 时触发,onCancelonConfirm 分别对应工具栏的两个按钮。

受控模式

activeTab + onTabChange 控制当前 tab,values + onChange 控制所有选中值。两者都可以只用非受控(defaultActiveTab / defaultValues)。

PickerGroupControlled.tsx
Loading…
import type { PickerGroupItem, PickerOption } from '@skyroc/native-ui';
import { Button, PickerGroupView, 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' }
];

const SIZES: PickerOption[] = [
  { label: '小', value: 'sm' },
  { label: '中', value: 'md' },
  { label: '大', value: 'lg' }
];

const CONTROLLED_PICKERS: PickerGroupItem[] = [
  { columns: COLORS, key: 'color', title: '颜色' },
  { columns: SIZES, key: 'size', title: '尺寸' }
];

const PickerGroupControlled = () => {
  const [activeTab, setActiveTab] = useState(0);
  const [values, setValues] = useState<string[][]>([['green'], ['md']]);

  function selectFirstTab() {
    setActiveTab(0);
  }

  function selectSecondTab() {
    setActiveTab(1);
  }

  return (
    <View className="gap-3 bg-background px-4 pb-4">
      <View className="flex-row gap-2">
        <Button
          size="sm"
          variant="outline"
          onPress={selectFirstTab}
        >
          颜色
        </Button>
        <Button
          size="sm"
          variant="outline"
          onPress={selectSecondTab}
        >
          尺寸
        </Button>
      </View>
      <Text color="muted">
        activeTab:{activeTab};values:{JSON.stringify(values)}
      </Text>
      <PickerGroupView
        activeTab={activeTab}
        pickers={CONTROLLED_PICKERS}
        values={values}
        onChange={setValues}
        onTabChange={setActiveTab}
      />
    </View>
  );
};

export { PickerGroupControlled };

defaultValues 缺省时逐个取 pickers[i].defaultValue

显示控制与文案

showTabBar / showToolbar 控制两条栏是否渲染,cancelText / nextStepText / confirmText 分别定制取消、下一步与确定的文案。

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

const OPTIONS: PickerOption[] = [
  { label: '选项一', value: '1' },
  { label: '选项二', value: '2' },
  { label: '选项三', value: '3' }
];

const DISPLAY_PICKERS: PickerGroupItem[] = [
  { columns: OPTIONS, key: 'first', title: '第一步' },
  { columns: OPTIONS, key: 'second', title: '第二步' }
];

type DisplayMode = 'all' | 'no-tab-bar' | 'no-toolbar';

const PickerGroupDisplay = () => {
  const [mode, setMode] = useState<DisplayMode>('all');

  const showTabBar = mode !== 'no-tab-bar';
  const showToolbar = mode !== 'no-toolbar';

  return (
    <View className="gap-3 bg-background px-4 pb-4">
      <View className="flex-row flex-wrap gap-2">
        <Button
          size="sm"
          variant="outline"
          onPress={() => setMode('all')}
        >
          全部显示
        </Button>
        <Button
          size="sm"
          variant="outline"
          onPress={() => setMode('no-tab-bar')}
        >
          隐藏 tab 栏
        </Button>
        <Button
          size="sm"
          variant="outline"
          onPress={() => setMode('no-toolbar')}
        >
          隐藏工具栏
        </Button>
      </View>
      <Text color="muted">当前模式:{mode}</Text>
      <PickerGroupView
        cancelText="返回"
        confirmText="提交"
        nextStepText="继续"
        pickers={DISPLAY_PICKERS}
        showTabBar={showTabBar}
        showToolbar={showToolbar}
      />
    </View>
  );
};

export { PickerGroupDisplay };

弹层用法

PickerGroupPickerGroupView 之上加了一层「确定才提交」:面板打开期间的滚动与切 tab 都只写内部临时值,在最后一个 tab 点确定才写进 values 并触发 onConfirm。每次打开都会把临时值重置回已确认值、tab 拨回第一个。

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

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}`
}));

/** 两段行程:每个 tab 各是一个独立的多列选择器 */
const TRIP_PICKERS: PickerGroupItem[] = [
  { columns: [MONTHS, DAYS], defaultValue: ['3', '1'], key: 'depart', title: '去程' },
  { columns: [MONTHS, DAYS], defaultValue: ['3', '8'], key: 'return', title: '返程' }
];

const PickerGroupPopup = () => {
  const [tripShow, setTripShow] = useState(false);
  const [tripValues, setTripValues] = useState<string[][]>([
    ['3', '1'],
    ['3', '8']
  ]);

  const tripLabel = tripValues.map(values => values.join('/')).join('  →  ');

  return (
    <View className="bg-background px-6">
      <View className="mb-8 flex-row flex-wrap items-center gap-3">
        <Button
          variant="tonal"
          onPress={() => setTripShow(true)}
        >
          选择行程
        </Button>
        <Text color="muted">当前:{tripLabel}</Text>

        <PickerGroup
          enablePanDownToClose
          pickers={TRIP_PICKERS}
          sheetClassName="bg-secondary"
          sheetClassNames={{ handleBar: 'bg-primary' }}
          show={tripShow}
          showHandle
          values={tripValues}
          onConfirm={setTripValues}
          onUpdateShow={setTripShow}
        />
      </View>
    </View>
  );
};

export { PickerGroupPopup };

因为面板每次打开都要回到第一个 tab,activeTab / defaultActiveTabPickerGroup 上不开放 —— 受控的 tab 与这条规则冲突,接了也会被立刻覆盖。

滚轮需要独占垂直手势,所以内部关掉了面板的内容拖拽,下拉关闭只剩顶部 handle:enablePanDownToClose 要配合 showHandle 一起传。

选择器配置

pickers 里的每一项都是独立的 PickerGroupItem,可以各自配置 columns(单列 / 多列 / 级联)、fieldNameshapticitemHeightvisibleCountloading 以及该滚轮的 classNames

PickerGroupMixed.tsx
Loading…
import type { PickerGroupItem, PickerOption } from '@skyroc/native-ui';
import { Cell, PickerGroup } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const HOURS: PickerOption[] = Array.from({ length: 24 }, (_, i) => ({
  label: `${i} 时`,
  value: `${i}`
}));

const MINUTES: PickerOption[] = Array.from({ length: 60 }, (_, i) => ({
  label: `${i} 分`,
  value: `${i}`
}));

/** 使用 id / name / sub 字段的级联数据 */
const REGIONS: PickerOption[] = [
  {
    id: 'zhejiang',
    name: '浙江',
    sub: [
      { id: 'hangzhou', name: '杭州' },
      { id: 'ningbo', name: '宁波' }
    ]
  },
  {
    id: 'jiangsu',
    name: '江苏',
    sub: [
      { id: 'nanjing', name: '南京' },
      { id: 'suzhou', name: '苏州' }
    ]
  }
];

/** 混合形态:第一个 tab 是级联,第二个 tab 是多列 */
const APPOINTMENT_PICKERS: PickerGroupItem[] = [
  {
    classNames: { itemText: 'text-primary' },
    columns: REGIONS,
    fieldNames: { children: 'sub', label: 'name', value: 'id' },
    haptic: true,
    key: 'region',
    title: '地区',
    visibleCount: 5
  },
  { columns: [HOURS, MINUTES], defaultValue: ['9', '30'], itemHeight: 52, key: 'time', title: '时间' }
];

const PickerGroupMixed = () => {
  const [appointmentShow, setAppointmentShow] = useState(false);
  const [appointmentValues, setAppointmentValues] = useState<string[][]>([]);

  const appointmentLabel =
    appointmentValues.length > 0 ? appointmentValues.map(values => values.join(' ')).join(',') : '请选择';

  return (
    <View className="bg-background px-6">
      <View className="mb-8">
        <PickerGroup
          nextStepText="选时间"
          pickers={APPOINTMENT_PICKERS}
          show={appointmentShow}
          values={appointmentValues}
          onConfirm={setAppointmentValues}
          onUpdateShow={setAppointmentShow}
        >
          {args => (
            <Cell
              showArrow
              title="预约"
              trailing={appointmentLabel}
              onPress={args.open}
            />
          )}
        </PickerGroup>
      </View>
    </View>
  );
};

export { PickerGroupMixed };

切 tab 时对应的滚轮会整块重建(key 随 tab 变化):两个 tab 的列数与级联结构可能完全不同,复用同一棵滚轮反而要多做一轮修正。给每项传稳定的 key 可以让这个身份更明确。

单个选择器

pickers 只有一项时 tab 栏自动隐藏(showTabBar 也管不着),主按钮直接显示「确定」。

PickerGroupSingle.tsx
Loading…
import type { PickerGroupItem, PickerOption } from '@skyroc/native-ui';
import { Button, PickerGroup } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const HOURS: PickerOption[] = Array.from({ length: 24 }, (_, i) => ({
  label: `${i} 时`,
  value: `${i}`
}));

const MINUTES: PickerOption[] = Array.from({ length: 60 }, (_, i) => ({
  label: `${i} 分`,
  value: `${i}`
}));

/** 只有一个 picker 时 tab 栏会自动隐藏 */
const SINGLE_PICKERS: PickerGroupItem[] = [{ columns: [HOURS, MINUTES], key: 'time', title: '时间' }];

const PickerGroupSingle = () => {
  const [singleShow, setSingleShow] = useState(false);

  return (
    <View className="bg-background px-6">
      <View className="mb-8 flex-row flex-wrap items-center gap-3">
        <Button
          variant="tonal"
          onPress={() => setSingleShow(true)}
        >
          选择时间
        </Button>

        <PickerGroup
          pickers={SINGLE_PICKERS}
          show={singleShow}
          onUpdateShow={setSingleShow}
        />
      </View>
    </View>
  );
};

export { PickerGroupSingle };

样式覆盖

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

slot作用位置
root根容器
toolbar顶部工具栏容器
cancel取消按钮容器
cancelText取消按钮文字
confirm确定 / 下一步按钮容器
confirmText确定 / 下一步按钮文字
tabBartab 栏容器
tab单个 tab
tabTexttab 文字(激活态会加粗变主题色)
activeIndicator激活 tab 底部的指示条
PickerGroupStyles.tsx
Loading…
import type { PickerGroupItem, PickerOption } from '@skyroc/native-ui';
import { PickerGroupView } from '@skyroc/native-ui';
import { View } from 'react-native';

const LEVELS: PickerOption[] = [
  { label: '初级', value: 'junior' },
  { label: '中级', value: 'middle' },
  { label: '高级', value: 'senior' }
];

const STYLE_PICKERS: PickerGroupItem[] = [
  { columns: LEVELS, key: 'current', title: '当前等级' },
  { columns: LEVELS, key: 'target', title: '目标等级' }
];

const PickerGroupStyles = () => {
  return (
    <View className="bg-background px-4 pb-4">
      <PickerGroupView
        className="overflow-hidden rounded-xl border border-primary/30"
        classNames={{
          activeIndicator: 'h-1 rounded-full',
          confirmText: 'font-semibold text-success',
          tabBar: 'bg-primary/5',
          tabText: 'text-base'
        }}
        pickers={STYLE_PICKERS}
      />
    </View>
  );
};

export { PickerGroupStyles };

滚轮本身的 slot 不在这里,走 pickers[i].classNames(取值为 PickerSlots)。

API

PickerGroupView

属性说明类型默认值
pickers*各个选择器的配置PickerGroupItem[]-
values所有选择器的选中值(受控),外层按 tab、内层按列string[][]-
defaultValues所有选择器的非受控初始值;缺省时逐个取 pickers[i].defaultValuestring[][]-
onChange任意选择器滚动时实时触发,回传全部值与变化的 tab 下标(values: string[][], pickerIndex: number) => void-
activeTab当前激活的 tab 下标(受控)number-
defaultActiveTab非受控初始 tab 下标number0
onTabChange激活 tab 变化的回调(index: number) => void-
onConfirm在最后一个 tab 点击确定时触发,回传所有选中值(values: string[][]) => void-
onCancel点击取消的回调,回传所有选中值(values: string[][]) => void-
showToolbar是否显示顶部工具栏booleantrue
showTabBar是否显示 tab 栏;只有一个选择器时始终不显示booleantrue
cancelText取消按钮文字string'取消'
confirmText最后一个 tab 上主按钮的文字string'确定'
nextStepText非末尾 tab 上主按钮的文字string'下一步'
className根容器类名,合并在 classNames.root 之后string-
classNames各 slot 的类名覆盖,见「样式覆盖」一节SlotClassNames<PickerGroupSlots>-

PickerGroup

PickerGroup 继承 PickerGroupView 的属性,但不含 activeTab / defaultActiveTabvalues 表示已确认值,面板内的滚动不会写它。

属性说明类型默认值
show*是否显示弹层boolean-
onUpdateShow显示状态变化回调(show: boolean) => void-
children触发元素:节点会被包一层 Pressable,渲染函数可拿到 open 与已确认 valuesReactNode | ((params: { open: () => void; values: 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 { PickerGroupItem, PickerGroupProps, PickerGroupSlots, PickerGroupViewProps } from '@skyroc/native-ui';

PickerGroupSlots

可通过 classNames 覆盖的 slot 名称;滚轮本身的 slot 走 pickers[i].classNames。

'activeIndicator' | 'cancel' | 'cancelText' | 'confirm' | 'confirmText' | 'root' | 'tab' | 'tabBar' | 'tabText' | 'toolbar'

SlotClassNames

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

Partial<Record<Slots, string>>

PickerGroupItem

组内单个选择器的配置,字段语义与 PickerView 的同名属性一致。

字段类型说明
title*stringtab 标签文字。
columns*PickerOption[] | PickerOption[][]列数据:单列、多列或带 children 的级联。
keystring该 tab 的唯一标识,缺省时退回下标。
defaultValuestring[]该选择器的默认选中值。
fieldNamesPickerFieldNames自定义字段名映射。
itemHeightnumber每个选项的高度(px)。
visibleCountnumber每列可见的选项数,必须取奇数。
hapticboolean滚过一格时是否触发轻触反馈。
loadingboolean是否显示加载遮罩。
classNamesSlotClassNames<PickerSlots>覆盖该滚轮各 slot 的类名。

包内还导出了 pickerGroupVariants