PickerGroup
分组选择器,多个滚轮共用一套工具栏按 tab 逐个填
分组选择器(PickerGroup)把多个 PickerView 组织成 tab,共用一套工具栏,主按钮在非末尾 tab 上是「下一步」、走到最后一个才是「确定」。典型场景是「起始时间 / 结束时间」这类需要分步填写的组合。包含两个组件:PickerGroupView 内联,PickerGroup 装进底部弹层。
import { PickerGroup, PickerGroupView } from '@skyroc/native-ui';PickerGroup 依赖 @gorhom/bottom-sheet,请确保 App 根节点已经包了 GestureHandlerRootView 与 BottomSheetModalProvider。
基础用法与事件
pickers 声明每个 tab 的标题与列数据,选中值是 string[][](外层按 tab、内层按列)。
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 时触发,onCancel 与 onConfirm 分别对应工具栏的两个按钮。
受控模式
activeTab + onTabChange 控制当前 tab,values + onChange 控制所有选中值。两者都可以只用非受控(defaultActiveTab / defaultValues)。
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 分别定制取消、下一步与确定的文案。
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 };弹层用法
PickerGroup 在 PickerGroupView 之上加了一层「确定才提交」:面板打开期间的滚动与切 tab 都只写内部临时值,在最后一个 tab 点确定才写进 values 并触发 onConfirm。每次打开都会把临时值重置回已确认值、tab 拨回第一个。
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 / defaultActiveTab 在 PickerGroup 上不开放 —— 受控的 tab 与这条规则冲突,接了也会被立刻覆盖。
滚轮需要独占垂直手势,所以内部关掉了面板的内容拖拽,下拉关闭只剩顶部 handle:enablePanDownToClose 要配合 showHandle 一起传。
选择器配置
pickers 里的每一项都是独立的 PickerGroupItem,可以各自配置 columns(单列 / 多列 / 级联)、fieldNames、haptic、itemHeight、visibleCount、loading 以及该滚轮的 classNames。
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 也管不着),主按钮直接显示「确定」。
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 | 确定 / 下一步按钮文字 |
tabBar | tab 栏容器 |
tab | 单个 tab |
tabText | tab 文字(激活态会加粗变主题色) |
activeIndicator | 激活 tab 底部的指示条 |
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].defaultValue | string[][] | - |
| onChange | 任意选择器滚动时实时触发,回传全部值与变化的 tab 下标 | (values: string[][], pickerIndex: number) => void | - |
| activeTab | 当前激活的 tab 下标(受控) | number | - |
| defaultActiveTab | 非受控初始 tab 下标 | number | 0 |
| onTabChange | 激活 tab 变化的回调 | (index: number) => void | - |
| onConfirm | 在最后一个 tab 点击确定时触发,回传所有选中值 | (values: string[][]) => void | - |
| onCancel | 点击取消的回调,回传所有选中值 | (values: string[][]) => void | - |
| showToolbar | 是否显示顶部工具栏 | boolean | true |
| showTabBar | 是否显示 tab 栏;只有一个选择器时始终不显示 | boolean | true |
| cancelText | 取消按钮文字 | string | '取消' |
| confirmText | 最后一个 tab 上主按钮的文字 | string | '确定' |
| nextStepText | 非末尾 tab 上主按钮的文字 | string | '下一步' |
| className | 根容器类名,合并在 classNames.root 之后 | string | - |
| classNames | 各 slot 的类名覆盖,见「样式覆盖」一节 | SlotClassNames<PickerGroupSlots> | - |
PickerGroup
PickerGroup 继承 PickerGroupView 的属性,但不含 activeTab / defaultActiveTab;values 表示已确认值,面板内的滚动不会写它。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| show* | 是否显示弹层 | boolean | - |
| onUpdateShow | 显示状态变化回调 | (show: boolean) => void | - |
| children | 触发元素:节点会被包一层 Pressable,渲染函数可拿到 open 与已确认 values | ReactNode | ((params: { open: () => void; values: string[][] }) => ReactNode) | - |
| closeOnBackdropPress | 点击遮罩是否关闭 | boolean | true |
| showHandle | 是否显示面板顶部的拖拽指示条 | boolean | false |
| enablePanDownToClose | 是否允许下拉关闭,需要同时开启 showHandle 才有可拖之处 | boolean | false |
| 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。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 PickerGroupSlots。
PickerGroupItem
组内单个选择器的配置,字段语义与 PickerView 的同名属性一致。
| 字段 | 类型 | 说明 |
|---|---|---|
| title* | string | tab 标签文字。 |
| columns* | PickerOption[] | PickerOption[][] | 列数据:单列、多列或带 children 的级联。 |
| key | string | 该 tab 的唯一标识,缺省时退回下标。 |
| defaultValue | string[] | 该选择器的默认选中值。 |
| fieldNames | PickerFieldNames | 自定义字段名映射。 |
| itemHeight | number | 每个选项的高度(px)。 |
| visibleCount | number | 每列可见的选项数,必须取奇数。 |
| haptic | boolean | 滚过一格时是否触发轻触反馈。 |
| loading | boolean | 是否显示加载遮罩。 |
| classNames | SlotClassNames<PickerSlots> | 覆盖该滚轮各 slot 的类名。 |
包内还导出了 pickerGroupVariants。