Skyroc Native UI

DatePicker

年月日滚轮选择器,列范围随可选区间与已选值级联

日期选择器(DatePicker)用滚轮选择年、月、日。列的选项不是写死的:它由 columnsTypeminDate / maxDate 推导,并随已选值级联收窄 —— 选到 2 月时日列只有 28 / 29 天,选到区间的首尾年份时月列也会跟着裁掉越界的月份。

组件分内联与弹层两种形态:DatePickerView 直接把滚轮渲染在当前布局里(底层是 PickerView);DatePicker 把它装进底部面板(底层是 Sheet / @gorhom/bottom-sheet),并额外管理「滚动只改临时值、确定才写回」的提交语义。

import { DatePicker, DatePickerView } from '@skyroc/native-ui';

使用 DatePicker 时应用根节点需要包一层 GestureHandlerRootViewBottomSheetModalProvider,缺了面板不会出现;DatePickerView 没有这个要求。

基础用法

不传任何属性时显示年、月、日三列,选中值落在今天,可选区间是今天往前后各 10 年。

DatePickerBasic.tsx
Loading…
import { DatePickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

const DatePickerBasic = () => {
  return (
    <View className="bg-background p-4">
      <DatePickerView showToolbar={false} />
    </View>
  );
};

export { DatePickerBasic };

选中值是一个字符串数组,顺序与 columnsType 一一对应,数字都补零:['2026', '08', '19']。它不是 Date 对象 —— 需要日期实例时自己拼,例如 new Date(Number(y), Number(m) - 1, Number(d))

何时使用

  • 需要选择一个具体日期,且不需要看到「这一天是星期几」「这个月的排布」时,用滚轮比日历更省空间、更快。
  • 需要在月视图上圈选区间、标注状态时用 Calendar
  • 只选时间(时 / 分 / 秒)用 TimePicker;选的不是日期而是普通选项列表用 Picker
  • 表单里的日期字段推荐用 DatePicker + 自定义触发元素(见下方「自定义触发元素」),而不是把滚轮常驻在表单中间。

受控模式

value + onChange 组成受控模式,defaultValue 用于非受控。DatePickerViewonChange 在每一次滚动停下后触发。

DatePickerControlled.tsx
Loading…
import { DatePickerView, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const INITIAL_DATE = ['2026', '08', '19'];

const DatePickerControlled = () => {
  const [value, setValue] = useState(INITIAL_DATE);

  return (
    <View className="bg-background p-4">
      <Text className="mb-2 text-sm text-muted-foreground">当前 value:{value.join('-')}</Text>
      <DatePickerView
        showToolbar={false}
        value={value}
        onChange={setValue}
      />
    </View>
  );
};

export { DatePickerControlled };

受控值会被组件钳回可选范围内,并且修正结果会回流成一次 onChange:如果你传进来的 value 越界(比如 maxDate 在过去),组件不会一边显示钳位后的值、一边让你手里留着那个越界的原值。父组件不接这次修正时值不变,也不会来回震荡。

可选区间

minDate / maxDate 界定可选范围,缺省是今天往前后各 10 年。收窄是分级发生的:年列直接由区间决定,月列只在停在首尾年份上时才被裁,日列还要再看是否落在首尾月份上。

DatePickerRange.tsx
Loading…
import { DatePickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

const CURRENT_YEAR = new Date().getFullYear();

const DatePickerRange = () => {
  return (
    <View className="bg-background p-4">
      <DatePickerView
        showToolbar={false}
        maxDate={new Date(CURRENT_YEAR, 11, 20)}
        minDate={new Date(CURRENT_YEAR, 0, 10)}
      />
    </View>
  );
};

export { DatePickerRange };

选中值落在区间之外时会被钳到数值上最接近的可选项,而不是简单取第一项或最后一项 —— 2000 年在 2016–2036 的区间里会变成 2016,被 filter 挖空的中间值则取左右最近的那个。钳位是迭代到不动点的:年被钳住之后月列才知道自己该怎么收窄,日列还要再跟一轮。

列组合

columnsType 决定显示哪几列以及它们的顺序,默认 ['year', 'month', 'day']。可以只保留其中的一部分,比如做「每年的纪念日」只需要月、日两列。

DatePickerColumns.tsx
Loading…
import { DatePickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

const DatePickerColumns = () => {
  return (
    <View className="bg-background p-4">
      <DatePickerView
        columnsType={['month', 'day']}
        defaultValue={['08', '19']}
        showToolbar={false}
      />
    </View>
  );
};

export { DatePickerColumns };

缺少年列时,月列的上下界没法从选中值里读出来,组件用今天的年份补位。所以 ['month', 'day'] 搭配跨年的 minDate / maxDate 时,实际可选范围以今年为准。

格式化与过滤

formatter(type, option) 只改显示文本,返回的 value 应保持不变(它是选中值的载体);filter(columnType, options, values) 剔除的是候选项本身。

DatePickerFormatter.tsx
Loading…
import { DatePickerView } from '@skyroc/native-ui';
import type { DatePickerFilter, DatePickerFormatter as DatePickerFormatterFn } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 各列的中文单位 */
const COLUMN_UNITS = { day: '日', month: '月', year: '年' };

/** 给数字列补上中文单位 */
const CN_FORMATTER: DatePickerFormatterFn = (type, option) => ({
  ...option,
  label: `${option.label}${COLUMN_UNITS[type]}`
});

/** 只保留双数日,用来验证 filter 把中间值挖空后选中值会落到最近的可选项 */
const EVEN_DAY_FILTER: DatePickerFilter = (columnType, options) => {
  if (columnType !== 'day') return options;

  return options.filter(option => Number.parseInt(option.value ?? '0', 10) % 2 === 0);
};

const DatePickerFormatter = () => {
  return (
    <View className="bg-background p-4">
      <DatePickerView
        filter={EVEN_DAY_FILTER}
        formatter={CN_FORMATTER}
        showToolbar={false}
      />
    </View>
  );
};

export { DatePickerFormatter };

两者的调用时机不同:formatter 先对生成的选项逐个加工,filter 再对整列做筛选。filter 的第三个参数是当前各列选中值,可以据此做联动过滤(比如只在某个月里跳过周末)。被 filter 挖空的当前值会被钳到最近的可选项,所以不必担心筛完之后选中值悬空。

两个函数都建议提到组件外或用 useCallback 包住:它们是重算列数据的 memo 依赖,每次渲染都换一个新函数会让缓存整体失效。

滚轮外观与反馈

itemHeight(默认 48)与 visibleCount(默认 5)决定滚轮密度,列区域的高度就是两者相乘;haptic 打开后每滚过一格触发一次轻触反馈。

DatePickerWheel.tsx
Loading…
import { DatePickerView } from '@skyroc/native-ui';
import { View } from 'react-native';

const DatePickerWheel = () => {
  return (
    <View className="bg-background p-4">
      <DatePickerView
        haptic
        classNames={{
          itemText: 'text-primary',
          selectedIndicator: 'border-primary/30 bg-primary/5'
        }}
        defaultValue={['2026', '08', '19']}
        itemHeight={40}
        showToolbar={false}
        visibleCount={3}
      />
    </View>
  );
};

export { DatePickerWheel };

visibleCount 要取奇数 —— 偶数时中间的选中指示线落不到正中。指示线是绝对定位在滚轮之上的一层,已经关掉了 pointerEvents,不会截走滚动手势。

加载状态

loading 在滚轮区域盖一层加载遮罩,组件尺寸保持不变,不会因为数据没来就把布局撑塌。

DatePickerLoading.tsx
Loading…
import { Button, DatePickerView } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

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

  function toggleLoading() {
    setLoading(current => !current);
  }

  return (
    <View className="bg-background p-4">
      <Button
        className="self-start"
        size="sm"
        variant="tonal"
        onPress={toggleLoading}
      >
        {loading ? '结束加载' : '重新加载'}
      </Button>
      <DatePickerView
        loading={loading}
        showToolbar={false}
      />
    </View>
  );
};

export { DatePickerLoading };

工具栏与回调

showToolbarDatePickerView 默认 true)显示顶部工具栏,title / cancelText / confirmText 定制文案,onCancel / onConfirm 回传当前选中值。

DatePickerToolbar.tsx
Loading…
import { DatePickerView, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const DatePickerToolbar = () => {
  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>
      <DatePickerView
        cancelText="返回"
        confirmText="选定"
        defaultValue={['2026', '08', '19']}
        title="选择日期"
        onCancel={handleCancel}
        onConfirm={handleConfirm}
      />
    </View>
  );
};

export { DatePickerToolbar };

内联形态下工具栏按钮只是回调,不会改变任何状态:值早在滚动时就通过 onChange 提交过了,onCancel 要不要回滚由你自己决定。

弹层提交与关闭

DatePickershow + onUpdateShow 控制面板显示。面板里的滚动只改临时值,只有点「确定」才会写回已确认值并触发 onChange;取消或关闭面板会丢弃这次滚动,下次打开重新从已确认值开始。

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

const DatePickerPopup = () => {
  const [show, setShow] = useState(false);
  const [checkIn, setCheckIn] = useState<string[]>([]);
  const [feedback, setFeedback] = useState('尚未提交');

  const checkInLabel = checkIn.length > 0 ? checkIn.join('-') : '请选择';

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

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

  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() => setShow(true)}
      >
        选择入住日期
      </Button>
      <Text color="muted">当前:{checkInLabel}</Text>
      <Text className="w-full text-sm text-muted-foreground">{feedback}</Text>

      <DatePicker
        enablePanDownToClose
        showHandle
        show={show}
        title="入住日期"
        value={checkIn}
        minDate={new Date()}
        onCancel={handleCancel}
        onConfirm={handleConfirm}
        onUpdateShow={setShow}
      />
    </View>
  );
};

export { DatePickerPopup };

这是 DatePickerDatePickerView 语义上最大的差别:内联形态的 onChange 是「滚到哪了」,弹层形态的 onChange 是「用户确认了什么」。

关闭路径上有一个约束:滚轮要独占垂直手势,所以面板的内容拖拽(enableContentPanningGesture)是关掉的,下拉通道只剩顶部的 handle,而 handle 默认不显示。enablePanDownToClose 要和 showHandle 一起传,否则开了也无处可拖。

自定义触发元素

children 传节点时会自动包一层 Pressable,点击即打开面板;传函数则由你自己画触发元素,参数里带着 open 与当前已确认的值。

DatePickerTrigger.tsx
Loading…
import { Cell, DatePicker } from '@skyroc/native-ui';
import type { DatePickerFormatter } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

/** 各列的中文单位 */
const COLUMN_UNITS = { day: '日', month: '月', year: '年' };

/** 给数字列补上中文单位 */
const CN_FORMATTER: DatePickerFormatter = (type, option) => ({
  ...option,
  label: `${option.label}${COLUMN_UNITS[type]}`
});

const DatePickerTrigger = () => {
  const [show, setShow] = useState(false);
  const [birthday, setBirthday] = useState<string[]>(['1998', '06', '15']);

  const birthdayLabel = birthday.length > 0 ? birthday.join('-') : '请选择';

  return (
    <View className="bg-background p-4">
      <DatePicker
        formatter={CN_FORMATTER}
        show={show}
        title="选择生日"
        value={birthday}
        maxDate={new Date()}
        minDate={new Date(1950, 0, 1)}
        onConfirm={setBirthday}
        onUpdateShow={setShow}
      >
        {args => (
          <Cell
            showArrow
            title="出生日期"
            trailing={birthdayLabel}
            onPress={args.open}
          />
        )}
      </DatePicker>
    </View>
  );
};

export { DatePickerTrigger };

面板还没被打开过时,children 拿到的就是初值 —— 它同样过了一遍钳位,不会把越界日期显示在触发元素上。

API

DatePickerView

内联滚轮,继承 PickerView 的属性(columnsfieldNames 除外 —— 列由 columnsType 与日期区间推导,不对外开放)。

属性说明类型默认值
columnsType显示哪几列及其顺序DatePickerColumnType[]['year', 'month', 'day']
minDate可选的最小日期Date今天往前 10 年
maxDate可选的最大日期Date今天往后 10 年
value选中值(受控),顺序与 columnsType 对应,数字补零string[]-
defaultValue默认选中值(非受控),缺省为今天string[]-
onChange选中值变化回调,滚动停下即触发(values: string[]) => void-
formatter定制选项显示文本,不应改动 valueDatePickerFormatter-
filter剔除某一列中的部分选项DatePickerFilter-
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>-

DatePicker

弹层形态,除下表外拥有 DatePickerView 的全部属性(onChange 的语义不同,见下)。

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

类型

import type {
  DatePickerColumnType,
  DatePickerFilter,
  DatePickerFormatter,
  DatePickerProps,
  DatePickerViewProps
} from '@skyroc/native-ui';

DatePickerColumnType

日期列的类型标识,columnsType 数组里的成员。

'year' | 'month' | 'day'

DatePickerFormatter

定制选项显示文本,逐个选项调用;返回的 value 应保持不变,它是选中值的载体。

DatePickerFilter

剔除某一列中的部分选项,在 formatter 之后、按列调用;第三个参数是当前各列选中值,可用于联动过滤。

(columnType: DatePickerColumnType, options: PickerOption[], values: string[]) => PickerOption[]

PickerSlots

滚轮部分可通过 classNames 覆盖的 slot 名称,来自底层的 PickerView。

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

SheetSlots

内部 Sheet 可通过 sheetClassNames 覆盖的 slot 名称,仅 DatePicker 用得到。

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

PickerOption

formatter / filter 拿到与返回的选项对象,日期列的 label 与 value 都是补零后的数字字符串。

字段类型说明
labelstring显示文本,formatter 改的就是它。
valuestring选项值,进入 value / onChange 的就是它。
disabledboolean是否禁用该选项。
childrenPickerOption[]级联模式下的子选项,日期列不会用到。