Picker
滚轮选择器,支持单列、多列与级联
选择器(Picker)用滚轮从若干选项里选值。包含两个组件:PickerView 是内联滚轮,Picker 把它装进底部弹层并额外管理「确定才提交」的语义。滚动跑在 react-native-reanimated 的 UI 线程上,选项的透明度与缩放随距中心的距离衰减。
import { Picker, PickerView } from '@skyroc/native-ui';Picker 依赖 @gorhom/bottom-sheet,请确保 App 根节点已经包了 GestureHandlerRootView 与 BottomSheetModalProvider。
基础用法
columns 传一维数组即为单列。选中值统一是 string[](单列也是长度为 1 的数组),defaultValue 设置初始值。
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 个、又要一眼看全时用
Radio或ActionSheet。 - 选日期 / 时间请直接用
DatePicker/TimePicker,它们在 Picker 之上补了范围与格式化。
受控模式
传 value + onChange 即为受控。外部改值时滚轮会同步滚过去 —— 但用户正在滚动时不抢,避免和手指打架。
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[][])时各列相互独立,互不联动。
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 时自动进入级联模式,按当前选中值逐级展开。不传初值也会自动补齐各级 —— 列的展开深度取决于选中值,而选中值合不合法又取决于列,组件会迭代到不动点,因此三级联动首屏就是三列,而不是滚一下才冒出下一列。
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' | 级联子选项 |
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 的透明度折扣。
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 |
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 在滚轮区域盖一层半透明遮罩和指示器,工具栏仍然可见可点。
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 定制文案。onCancel 与 onConfirm 都会回传当前选中值。
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 | 加载遮罩 |
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 };弹层提交与关闭
Picker 在 PickerView 之上加了一层「确定才提交」:面板打开期间的滚动只写内部临时值,点确定才写进 value 并触发 onConfirm,取消则整份丢弃。每次打开都会把临时值重置回已确认值,上一次取消掉的滚动不会残留。
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 表达不了的命令式操作。
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) | number | 48 |
| visibleCount | 每列可见的选项数,必须取奇数 | number | 5 |
| haptic | 滚过一格时触发轻触反馈 | boolean | false |
| loading | 在滚轮区域显示加载遮罩 | boolean | false |
| showToolbar | 是否显示顶部工具栏 | boolean | true |
| 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 与已确认 value | ReactNode | ((params: { open: () => void; value: 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 { PickerColumnType, PickerFieldNames, PickerOption, PickerProps, PickerSlots, PickerViewProps } from '@skyroc/native-ui';PickerColumnType
列数据形态,由 columns 的结构自动推断。
PickerSlots
可通过 classNames 覆盖的 slot 名称。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 PickerSlots。
PickerOption
单个选项。字段名可通过 fieldNames 改写,因此 label / value 都是可选的,且允许携带任意额外字段。
| 字段 | 类型 | 说明 |
|---|---|---|
| label | string | 显示文本,字段名可由 fieldNames.label 改写。 |
| value | string | 选项值,字段名可由 fieldNames.value 改写。 |
| children | PickerOption[] | 级联子选项,字段名可由 fieldNames.children 改写。 |
| disabled | boolean | 是否禁用该选项,滚轮会吸附到最近的可用项。 |
PickerFieldNames
选项对象的字段名映射。
| 字段 | 类型 | 说明 |
|---|---|---|
| label | string | 显示文本字段名,默认 'label'。 |
| value | string | 选项值字段名,默认 'value'。 |
| children | string | 子选项字段名,默认 'children'。 |
包内还导出了 pickerVariants。