【HarmonyOS】DAY23:React Native for OpenHarmony:DatePicker 日期选择器组件详解


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


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

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 兼容性

  1. 模式限制

    • API 20 仅支持 datetime 模式
    • datetime 模式需要组合两个选择器或使用自定义实现
  2. 时区处理

    • 需要手动设置 timeZoneOffsetInMinutes
    • 建议统一使用 UTC 时间
  3. 事件处理

    • Android/OpenHarmony 需处理 event.type
    • iOS 直接返回日期对象

性能优化

  1. 减少重渲染

    typescript 复制代码
    // 使用 React.memo 包裹组件
    const MemoizedDatePicker = React.memo(DatePicker);
    
    // 稳定回调函数
    const handleChange = useCallback((date: Date) => {
      // 处理逻辑
    }, []);
  2. 延迟加载

    typescript 复制代码
    // 使用 React.lazy 按需加载
    const DatePicker = React.lazy(() => import('./components/DatePicker'));
  3. 防抖处理

    typescript 复制代码
    // 对频繁的日期变更进行防抖
    const debouncedOnChange = useMemo(
      () => debounce(onChange, 300),
      [onChange]
    );

总结

本文介绍了在 OpenHarmony 平台上使用 DatePicker 组件的技术要点,包括组件架构、纯 JS 实现、平台适配等内容。


相关资源

📕个人领域 :Linux/C++/java/AI

🚀 个人主页有点流鼻涕 · CSDN

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
胖鱼罐头16 小时前
RNGH:指令式 vs JSX 形式深度对比
前端·react native
麟听科技17 小时前
HarmonyOS 6.0+ APP智能种植监测系统开发实战:农业传感器联动与AI种植指导落地
人工智能·分布式·学习·华为·harmonyos
前端不太难17 小时前
HarmonyOS PC 焦点系统重建
华为·状态模式·harmonyos
空白诗18 小时前
基础入门 Flutter for Harmony:Text 组件详解
javascript·flutter·harmonyos
lbb 小魔仙19 小时前
【HarmonyOS】React Native实战+Popover内容自适应
react native·华为·harmonyos
motosheep19 小时前
鸿蒙开发(四)播放 Lottie 动画实战(Canvas 渲染 + 资源加载踩坑总结)
华为·harmonyos
左手厨刀右手茼蒿20 小时前
Flutter for OpenHarmony 实战:Barcode — 纯 Dart 条形码与二维码生成全指南
android·flutter·ui·华为·harmonyos
lbb 小魔仙20 小时前
【HarmonyOS】React Native of HarmonyOS实战:手势组合与协同
react native·华为·harmonyos
果粒蹬i21 小时前
【HarmonyOS】React Native实战项目+NativeStack原生导航
react native·华为·harmonyos
waeng_luo21 小时前
HarmonyOS 应用开发 Skills
华为·harmonyos