React Native for OpenHarmony:DatePicker 日期选择器组件详解

🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

- [React Native for OpenHarmony:DatePicker 日期选择器组件详解](#React Native for OpenHarmony:DatePicker 日期选择器组件详解)
description: 深入解析在 OpenHarmony 6.0.0 平台上使用 React Native DatePicker 组件的技术方案,包含平台适配、事件处理、样式定制
tags:
- react-native
- openharmony
- datetimepicker
- date-selection
category: 移动开发
概述
DatePicker 是移动应用中用于选择日期和时间的标准 UI 组件,广泛应用于表单填写、日程安排、数据筛选等场景。本文将详细讲解在 OpenHarmony 6.0.0 (API 20) 平台上使用 DatePicker 组件的技术要点。
组件架构
工作原理
┌─────────────────────────────────────────────────────┐
│ React Native JavaScript 层 │
│ (组件状态、事件处理、日期格式化、UI 逻辑) │
└─────────────────────┬───────────────────────────────┘
│ JSI (JavaScript Interface)
│ 属性序列化 / 事件反序列化
┌─────────────────────┴───────────────────────────────┐
│ @react-native-oh/react-native-harmony │
│ (桥接适配层) │
└─────────────────────┬───────────────────────────────┘
│ Native Module Bridge
┌─────────────────────┴───────────────────────────────┐
│ OpenHarmony Native 层 │
│ (@ohos.datepicker 模块、ETS 桥接代码) │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────┴───────────────────────────────┐
│ ArkUI 组件系统 │
│ (渲染引擎、原生 UI) │
└─────────────────────────────────────────────────────┘
平台差异对比
| 特性 | iOS | Android | OpenHarmony 6.0.0 |
|---|---|---|---|
| 原生组件 | UIDatePicker | DatePicker | @ohos.datepicker |
| 模式支持 | date/time/datetime | date/time/datetime | date/time |
| 时区处理 | 自动 | 需配置 | 需手动设置 |
| 样式定制 | 有限 | 中等 | 有限 |
| 事件格式 | Date 对象 | Date 对象 | Date 对象 |
核心实现
类型定义
typescript
// types/datepicker.ts
export type DatePickerMode = 'date' | 'time' | 'datetime';
export type AndroidEvent = {
type: 'set' | 'dismissed';
utc?: boolean;
};
export interface DatePickerProps {
value: Date;
mode?: DatePickerMode;
onChange?: (date: Date) => void;
onDismiss?: () => void;
minimumDate?: Date;
maximumDate?: Date;
minuteInterval?: 1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 15 | 20 | 30;
timeZoneOffsetInMinutes?: number;
disabled?: boolean;
style?: object;
}
export interface DatePickerState {
visible: boolean;
currentValue: Date;
currentMode: DatePickerMode;
}
日期格式化工具
typescript
// utils/dateFormatter.ts
export class DateFormatter {
/**
* 格式化日期为 YYYY-MM-DD
*/
static formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 格式化时间为 HH:mm
*/
static formatTime(date: Date): string {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
/**
* 格式化日期时间为 YYYY-MM-DD HH:mm
*/
static formatDateTime(date: Date): string {
return `${this.formatDate(date)} ${this.formatTime(date)}`;
}
/**
* 获取时区偏移(分钟)
*/
static getTimezoneOffset(date: Date): number {
return date.getTimezoneOffset();
}
/**
* 转换为 UTC 日期
*/
static toUTC(date: Date): Date {
return new Date(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes()
);
}
/**
* 从 UTC 转换为本地日期
*/
static fromUTC(utcDate: Date): Date {
return new Date(
utcDate.getUTCFullYear(),
utcDate.getUTCMonth(),
utcDate.getUTCDate(),
utcDate.getUTCHours(),
utcDate.getUTCMinutes()
);
}
/**
* 本地化格式化
*/
static localize(date: Date, locale: string = 'zh-CN'): string {
return date.toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
}
DatePicker 组件封装
typescript
// components/DatePicker.tsx
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Modal,
Platform,
Alert,
} from 'react-native';
import { DateFormatter } from '../utils/dateFormatter';
import { DatePickerProps, DatePickerMode } from '../types/datepicker';
export const DatePicker: React.FC<DatePickerProps> = ({
value,
mode = 'date',
onChange,
onDismiss,
minimumDate,
maximumDate,
minuteInterval = 1,
timeZoneOffsetInMinutes,
disabled = false,
style,
}) => {
const [visible, setVisible] = useState(false);
const [currentMode, setCurrentMode] = useState<DatePickerMode>(mode);
// 使用 ref 避免闭包问题
const onChangeRef = useRef(onChange);
const onDismissRef = useRef(onDismiss);
useEffect(() => {
onChangeRef.current = onChange;
onDismissRef.current = onDismiss;
}, [onChange, onDismiss]);
// 显示选择器
const show = useCallback(() => {
if (disabled) return;
setVisible(true);
setCurrentMode(mode);
}, [disabled, mode]);
// 隐藏选择器
const hide = useCallback(() => {
setVisible(false);
onDismissRef.current?.();
}, []);
// 处理日期变更
const handleDateChange = useCallback((event: any, selectedDate?: Date) => {
if (Platform.OS === 'ios') {
if (selectedDate) {
onChangeRef.current?.(selectedDate);
}
} else {
// Android / OpenHarmony
if (event?.type === 'set' && selectedDate) {
onChangeRef.current?.(selectedDate);
hide();
} else {
hide();
}
}
}, [hide]);
// 获取显示文本
const displayText = useMemo(() => {
switch (currentMode) {
case 'date':
return DateFormatter.formatDate(value);
case 'time':
return DateFormatter.formatTime(value);
case 'datetime':
return DateFormatter.formatDateTime(value);
default:
return '';
}
}, [value, currentMode]);
return (
<>
<TouchableOpacity
style={[styles.trigger, style]}
onPress={show}
disabled={disabled}
activeOpacity={0.7}
>
<Text
style={[
styles.triggerText,
disabled && styles.triggerTextDisabled,
]}
>
{displayText}
</Text>
</TouchableOpacity>
{/* 这里使用原生组件 */}
{/* 实际项目中根据平台使用 @react-native-community/datetimepicker */}
</>
);
};
const styles = StyleSheet.create({
trigger: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 12,
borderWidth: 1,
borderColor: '#ddd',
},
triggerText: {
fontSize: 16,
color: '#333',
},
triggerTextDisabled: {
color: '#999',
},
});
滚轮选择器实现(纯 JS)
由于 OpenHarmony 6.0.0 对 datetime 模式支持有限,以下提供纯 JS 实现的滚轮选择器:
typescript
// components/WheelDatePicker.tsx
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Dimensions,
} from 'react-native';
import { DateFormatter } from '../utils/dateFormatter';
interface WheelDatePickerProps {
value: Date;
mode: 'date' | 'time' | 'datetime';
onChange: (date: Date) => void;
minDate?: Date;
maxDate?: Date;
themeColor?: string;
}
const SCREEN_WIDTH = Dimensions.get('window').width;
const ITEM_HEIGHT = 36;
const VISIBLE_ITEMS = 5;
const CONTAINER_HEIGHT = ITEM_HEIGHT * VISIBLE_ITEMS;
export const WheelDatePicker: React.FC<WheelDatePickerProps> = ({
value,
mode,
onChange,
minDate,
maxDate,
themeColor = '#F44336',
}) => {
const [currentValue, setCurrentValue] = useState(value);
const yearScrollRef = useRef<ScrollView>(null);
const monthScrollRef = useRef<ScrollView>(null);
const dayScrollRef = useRef<ScrollView>(null);
const hourScrollRef = useRef<ScrollView>(null);
const minuteScrollRef = useRef<ScrollView>(null);
// 生成年份列表
const years = useMemo(() => {
const currentYear = new Date().getFullYear();
const startYear = (minDate?.getFullYear() ?? currentYear - 50);
const endYear = (maxDate?.getFullYear() ?? currentYear + 50);
return Array.from(
{ length: endYear - startYear + 1 },
(_, i) => startYear + i
);
}, [minDate, maxDate]);
// 月份列表
const months = useMemo(() => {
return Array.from({ length: 12 }, (_, i) => i + 1);
}, []);
// 日期列表
const days = useMemo(() => {
const year = currentValue.getFullYear();
const month = currentValue.getMonth() + 1;
const daysInMonth = new Date(year, month, 0).getDate();
return Array.from({ length: daysInMonth }, (_, i) => i + 1);
}, [currentValue]);
// 小时列表
const hours = useMemo(() => {
return Array.from({ length: 24 }, (_, i) => i);
}, []);
// 分钟列表
const minutes = useMemo(() => {
return Array.from({ length: 60 }, (_, i) => i);
}, []);
// 滚动到指定位置
const scrollToIndex = useCallback((
scrollViewRef: React.RefObject<ScrollView>,
index: number,
animated = true
) => {
if (scrollViewRef.current) {
const offset = (CONTAINER_HEIGHT - ITEM_HEIGHT) / 2 + index * ITEM_HEIGHT;
scrollViewRef.current.scrollTo({ y: offset, animated });
}
}, []);
// 初始化滚动位置
useEffect(() => {
const timer = setTimeout(() => {
scrollToIndex(yearScrollRef, years.indexOf(currentValue.getFullYear()), false);
scrollToIndex(monthScrollRef, currentValue.getMonth(), false);
scrollToIndex(dayScrollRef, currentValue.getDate() - 1, false);
scrollToIndex(hourScrollRef, currentValue.getHours(), false);
scrollToIndex(minuteScrollRef, currentValue.getMinutes(), false);
}, 100);
return () => clearTimeout(timer);
}, []);
// 更新日期
const updateDate = useCallback((part: 'year' | 'month' | 'day' | 'hour' | 'minute', value: number) => {
const newDate = new Date(currentValue);
switch (part) {
case 'year':
newDate.setFullYear(value);
break;
case 'month':
newDate.setMonth(value);
break;
case 'day':
newDate.setDate(value);
break;
case 'hour':
newDate.setHours(value);
break;
case 'minute':
newDate.setMinutes(value);
break;
}
setCurrentValue(newDate);
onChange(newDate);
}, [currentValue, onChange]);
// 渲染滚轮选择器
const renderWheel = useCallback(<T extends number>(
data: T[],
selectedValue: T,
onSelect: (value: T) => void,
scrollViewRef: React.RefObject<ScrollView>,
label: string
) => {
const selectedIndex = data.indexOf(selectedValue);
return (
<View style={styles.wheelContainer}>
<Text style={styles.wheelLabel}>{label}</Text>
<View style={styles.wheelWrapper}>
{/* 选中区域遮罩 */}
<View style={[styles.selectionMask, { borderColor: themeColor }]} />
<ScrollView
ref={scrollViewRef}
style={styles.wheelScroll}
contentContainerStyle={styles.wheelContent}
showsVerticalScrollIndicator={false}
snapToInterval={ITEM_HEIGHT}
decelerationRate="fast"
scrollEventThrottle={16}
onMomentumScrollEnd={(event) => {
const offsetY = event.nativeEvent.contentOffset.y;
const topPadding = (CONTAINER_HEIGHT - ITEM_HEIGHT) / 2;
const index = Math.round((offsetY - topPadding) / ITEM_HEIGHT);
if (index >= 0 && index < data.length) {
onSelect(data[index]);
}
}}
>
{/* 上方占位 */}
<View style={{ height: (CONTAINER_HEIGHT - ITEM_HEIGHT) / 2 }} />
{data.map((item, index) => (
<TouchableOpacity
key={index}
style={[
styles.wheelItem,
index === selectedIndex && styles.wheelItemSelected,
]}
onPress={() => {
onSelect(item);
scrollToIndex(scrollViewRef, index);
}}
activeOpacity={0.7}
>
<Text
style={[
styles.wheelItemText,
index === selectedIndex && {
color: themeColor,
fontWeight: '700',
},
]}
>
{String(item).padStart(2, '0')}
</Text>
</TouchableOpacity>
))}
{/* 下方占位 */}
<View style={{ height: (CONTAINER_HEIGHT - ITEM_HEIGHT) / 2 }} />
</ScrollView>
</View>
</View>
);
}, [themeColor, scrollToIndex]);
return (
<View style={styles.container}>
{mode === 'date' || mode === 'datetime' ? (
<View style={styles.wheelRow}>
{renderWheel(
years,
currentValue.getFullYear(),
(y) => updateDate('year', y),
yearScrollRef,
'年'
)}
{renderWheel(
months,
currentValue.getMonth() + 1,
(m) => updateDate('month', m - 1),
monthScrollRef,
'月'
)}
{renderWheel(
days,
currentValue.getDate(),
(d) => updateDate('day', d),
dayScrollRef,
'日'
)}
</View>
) : null}
{mode === 'time' || mode === 'datetime' ? (
<View style={styles.wheelRow}>
{renderWheel(
hours,
currentValue.getHours(),
(h) => updateDate('hour', h),
hourScrollRef,
'时'
)}
{renderWheel(
minutes,
currentValue.getMinutes(),
(m) => updateDate('minute', m),
minuteScrollRef,
'分'
)}
</View>
) : null}
{/* 显示当前值 */}
<View style={styles.valueDisplay}>
<Text style={[styles.valueText, { color: themeColor }]}>
{mode === 'date' && DateFormatter.formatDate(currentValue)}
{mode === 'time' && DateFormatter.formatTime(currentValue)}
{mode === 'datetime' && DateFormatter.formatDateTime(currentValue)}
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 16,
backgroundColor: '#fff',
borderRadius: 12,
},
wheelRow: {
flexDirection: 'row',
justifyContent: 'center',
marginBottom: 12,
},
wheelContainer: {
alignItems: 'center',
marginHorizontal: 4,
},
wheelLabel: {
fontSize: 12,
color: '#999',
marginBottom: 8,
},
wheelWrapper: {
position: 'relative',
width: 70,
height: CONTAINER_HEIGHT,
backgroundColor: '#f5f5f5',
borderRadius: 8,
overflow: 'hidden',
},
selectionMask: {
position: 'absolute',
top: (CONTAINER_HEIGHT - ITEM_HEIGHT) / 2,
left: 0,
right: 0,
height: ITEM_HEIGHT,
borderTopWidth: 1,
borderBottomWidth: 1,
backgroundColor: 'rgba(244, 67, 54, 0.05)',
pointerEvents: 'none',
},
wheelScroll: {
flex: 1,
},
wheelContent: {
paddingTop: (CONTAINER_HEIGHT - ITEM_HEIGHT) / 2,
paddingBottom: (CONTAINER_HEIGHT - ITEM_HEIGHT) / 2,
},
wheelItem: {
height: ITEM_HEIGHT,
justifyContent: 'center',
alignItems: 'center',
},
wheelItemSelected: {
backgroundColor: 'transparent',
},
wheelItemText: {
fontSize: 16,
color: '#999',
},
valueDisplay: {
alignItems: 'center',
paddingVertical: 16,
borderTopWidth: 1,
borderTopColor: '#f0f0f0',
marginTop: 8,
},
valueText: {
fontSize: 20,
fontWeight: '700',
},
});
使用示例
typescript
// Example.tsx
import React, { useState } from 'react';
import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
import { WheelDatePicker } from './components/WheelDatePicker';
const Example: React.FC = () => {
const [selectedDate, setSelectedDate] = useState(new Date());
const [mode, setMode] = useState<'date' | 'time' | 'datetime'>('date');
return (
<View style={styles.container}>
{/* 模式切换 */}
<View style={styles.modeSelector}>
{(['date', 'time', 'datetime'] as const).map((m) => (
<TouchableOpacity
key={m}
style={[
styles.modeButton,
mode === m && styles.modeButtonActive,
]}
onPress={() => setMode(m)}
>
<Text
style={[
styles.modeButtonText,
mode === m && styles.modeButtonTextActive,
]}
>
{m === 'date' ? '日期' : m === 'time' ? '时间' : '日期时间'}
</Text>
</TouchableOpacity>
))}
</View>
{/* 选择器 */}
<WheelDatePicker
value={selectedDate}
mode={mode}
onChange={setSelectedDate}
themeColor="#F44336"
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#f5f5f5',
},
modeSelector: {
flexDirection: 'row',
backgroundColor: '#fff',
borderRadius: 8,
padding: 4,
marginBottom: 16,
},
modeButton: {
flex: 1,
paddingVertical: 10,
alignItems: 'center',
borderRadius: 6,
},
modeButtonActive: {
backgroundColor: '#F44336',
},
modeButtonText: {
fontSize: 14,
color: '#666',
fontWeight: '600',
},
modeButtonTextActive: {
color: '#fff',
},
});
export default Example;
OpenHarmony 适配要点
API 兼容性
-
模式限制
- API 20 仅支持
date和time模式 datetime模式需要组合两个选择器或使用自定义实现
- API 20 仅支持
-
时区处理
- 需要手动设置
timeZoneOffsetInMinutes - 建议统一使用 UTC 时间
- 需要手动设置
-
事件处理
- Android/OpenHarmony 需处理
event.type - iOS 直接返回日期对象
- Android/OpenHarmony 需处理
性能优化
-
减少重渲染
typescript// 使用 React.memo 包裹组件 const MemoizedDatePicker = React.memo(DatePicker); // 稳定回调函数 const handleChange = useCallback((date: Date) => { // 处理逻辑 }, []); -
延迟加载
typescript// 使用 React.lazy 按需加载 const DatePicker = React.lazy(() => import('./components/DatePicker')); -
防抖处理
typescript// 对频繁的日期变更进行防抖 const debouncedOnChange = useMemo( () => debounce(onChange, 300), [onChange] );
总结
本文介绍了在 OpenHarmony 平台上使用 DatePicker 组件的技术要点,包括组件架构、纯 JS 实现、平台适配等内容。
相关资源
- 完整项目 Demo
- OpenHarmony 跨平台社区
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
📕个人领域 :Linux/C++/java/AI
🚀 个人主页 :有点流鼻涕 · CSDN
💬 座右铭 : "向光而行,沐光而生。"
