在React Native中实现一个时间选择器,你可以使用第三方库比如react-native-datetimepicker来在鸿蒙(HarmonyOS)设备上实现时间选择功能。鸿蒙操作系统基于Android系统,因此大多数Android的React Native组件和库都可以在鸿蒙设备上运行,包括时间选择器。

步骤 1: 安装 react-native-datetimepicker

首先,你需要安装react-native-datetimepicker库。在你的React Native项目中,打开终端并运行以下命令:

npm install @react-native-community/datetimepicker

或者,如果你使用yarn:

yarn add @react-native-community/datetimepicker

步骤 2: 导入并使用 DateTimePicker

在你的React Native组件中,你可以这样使用DateTimePicker

import React, { useState } from 'react';
import { View, Button, Platform } from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker';

const TimePickerExample = () => {
  const [date, setDate] = useState(new Date(1598051730000)); // 默认时间
  const [mode, setMode] = useState('date'); // 'date' 或 'time'
  const [show, setShow] = useState(false);

  const onChange = (event, selectedDate) => {
    const currentDate = selectedDate || date;
    setShow(Platform.OS === 'ios'); // 只在iOS上显示,因为安卓原生组件存在问题
    setDate(currentDate);
  };

  const showMode = (currentMode) => {
    setShow(true);
    setMode(currentMode);
  };

  const showDatepicker = () => {
    showMode('date');
  };

  const showTimepicker = () => {
    showMode('time');
  };

  return (
    <View>
      <Button title="Show date picker" onPress={showDatepicker} />
      <Button title="Show time picker" onPress={showTimepicker} />
      {show && (
        <DateTimePicker
          testID="dateTimePicker"
          value={date}
          mode={mode}
          is24Hour={true} // 24小时制
          display="default" // 默认显示模式,还有其他如'spinner', 'inline'等模式可选
          onChange={onChange}
        />
      )}
    </View>
  );
};

export default TimePickerExample;

注意事项:

  1. 平台兼容性:虽然大多数React Native组件在鸿蒙(HarmonyOS)上可以工作,但最好还是检查每个组件的文档,确保其在鸿蒙设备上的兼容性。对于日期和时间选择器,通常没有问题。
  2. 显示问题:在Android设备上,特别是在鸿蒙操作系统上,有时原生组件的显示可能会有所不同或存在问题。在这种情况下,可以考虑使用第三方库如react-native-datetimepicker。在上述代码中,我们通过Platform.OS === 'ios'来避免在Android上显示时间选择器,因为原生组件可能存在问题。对于Android,我们使用第三方库来提供一致的用户体验。
  3. 权限:确保你的应用有访问时间和日期的权限。通常,这些权限在Android应用中是自动授予的,但在某些情况下,你可能需要在应用的AndroidManifest.xml中明确请求它们。对于React Native应用,通常不需要手动添加这些权限。

通过以上步骤,你应该能够在React Native应用中为鸿蒙设备实现一个功能齐全的时间选择器。


真实组件案例演示:请添加图片描述

import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Modal, Platform } from 'react-native';

// Simple Icon Component using Unicode symbols
interface IconProps {
  name: string;
  size?: number;
  color?: string;
  style?: object;
}

const Icon: React.FC<IconProps> = ({ 
  name, 
  size = 24, 
  color = '#333333',
  style 
}) => {
  const getIconSymbol = () => {
    switch (name) {
      case 'clock': return '⏰';
      case 'calendar': return '📅';
      case 'time': return '⏱️';
      case 'confirm': return '✅';
      case 'cancel': return '❌';
      case 'up': return '▲';
      case 'down': return '▼';
      default: return '●';
    }
  };

  return (
    <View style={[{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }, style]}>
      <Text style={{ fontSize: size * 0.8, color, includeFontPadding: false, textAlign: 'center' }}>
        {getIconSymbol()}
      </Text>
    </View>
  );
};

// DatetimePicker Component
interface DatetimePickerProps {
  mode?: 'date' | 'time' | 'datetime';
  value?: Date;
  onChange: (date: Date) => void;
  placeholder?: string;
}

const DatetimePicker: React.FC<DatetimePickerProps> = ({ 
  mode = 'datetime',
  value = new Date(),
  onChange,
  placeholder = '请选择时间'
}) => {
  const [visible, setVisible] = useState(false);
  const [tempDate, setTempDate] = useState(value);
  
  const openPicker = () => {
    setTempDate(value);
    setVisible(true);
  };
  
  const closePicker = () => {
    setVisible(false);
  };
  
  const confirmSelection = () => {
    onChange(tempDate);
    closePicker();
  };
  
  const formatDate = (date: Date): string => {
    if (mode === 'date') {
      return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`;
    } else if (mode === 'time') {
      return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
    } else {
      return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
    }
  };
  
  const getDisplayText = (): string => {
    return value ? formatDate(value) : placeholder;
  };
  
  // Number picker component for datetime selection
  const NumberPicker = ({ 
    value, 
    min, 
    max, 
    onChange,
    label
  }: {
    value: number;
    min: number;
    max: number;
    onChange: (value: number) => void;
    label: string;
  }) => {
    const increment = () => {
      if (value < max) {
        onChange(value + 1);
      }
    };
    
    const decrement = () => {
      if (value > min) {
        onChange(value - 1);
      }
    };
    
    return (
      <View style={styles.numberPickerContainer}>
        <TouchableOpacity 
          style={styles.pickerButton} 
          onPress={increment}
          disabled={value >= max}
        >
          <Icon name="up" size={16} color={value >= max ? '#ccc' : '#333'} />
        </TouchableOpacity>
        
        <View style={styles.pickerValueContainer}>
          <Text style={styles.pickerValue}>{value.toString().padStart(2, '0')}</Text>
          <Text style={styles.pickerLabel}>{label}</Text>
        </View>
        
        <TouchableOpacity 
          style={styles.pickerButton} 
          onPress={decrement}
          disabled={value <= min}
        >
          <Icon name="down" size={16} color={value <= min ? '#ccc' : '#333'} />
        </TouchableOpacity>
      </View>
    );
  };
  
  // Year picker
  const YearPicker = () => (
    <NumberPicker 
      value={tempDate.getFullYear()}
      min={1900}
      max={2100}
      onChange={(year) => setTempDate(new Date(year, tempDate.getMonth(), tempDate.getDate(), tempDate.getHours(), tempDate.getMinutes()))}
      label="年"
    />
  );
  
  // Month picker
  const MonthPicker = () => (
    <NumberPicker 
      value={tempDate.getMonth() + 1}
      min={1}
      max={12}
      onChange={(month) => setTempDate(new Date(tempDate.getFullYear(), month - 1, tempDate.getDate(), tempDate.getHours(), tempDate.getMinutes()))}
      label="月"
    />
  );
  
  // Day picker
  const DayPicker = () => {
    const daysInMonth = new Date(tempDate.getFullYear(), tempDate.getMonth() + 1, 0).getDate();
    return (
      <NumberPicker 
        value={tempDate.getDate()}
        min={1}
        max={daysInMonth}
        onChange={(day) => setTempDate(new Date(tempDate.getFullYear(), tempDate.getMonth(), day, tempDate.getHours(), tempDate.getMinutes()))}
        label="日"
      />
    );
  };
  
  // Hour picker
  const HourPicker = () => (
    <NumberPicker 
      value={tempDate.getHours()}
      min={0}
      max={23}
      onChange={(hour) => setTempDate(new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(), hour, tempDate.getMinutes()))}
      label="时"
    />
  );
  
  // Minute picker
  const MinutePicker = () => (
    <NumberPicker 
      value={tempDate.getMinutes()}
      min={0}
      max={59}
      onChange={(minute) => setTempDate(new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(), tempDate.getHours(), minute))}
      label="分"
    />
  );
  
  return (
    <>
      <TouchableOpacity style={styles.datetimeTrigger} onPress={openPicker}>
        <Icon name={mode === 'time' ? 'clock' : 'calendar'} size={20} color="#666" style={styles.triggerIcon} />
        <Text style={styles.datetimeText}>{getDisplayText()}</Text>
        <Icon name="down" size={16} color="#999" />
      </TouchableOpacity>

      <Modal
        animationType="slide"
        transparent={true}
        visible={visible}
        onRequestClose={closePicker}
      >
        <View style={styles.datetimeOverlay}>
          <View style={styles.datetimeContainer}>
            <View style={styles.datetimeHeader}>
              <Text style={styles.datetimeTitle}>
                {mode === 'date' ? '选择日期' : mode === 'time' ? '选择时间' : '选择日期和时间'}
              </Text>
              <TouchableOpacity onPress={closePicker}>
                <Icon name="cancel" size={20} color="#999" />
              </TouchableOpacity>
            </View>
            
            <View style={styles.datetimePickers}>
              {mode !== 'time' && (
                <>
                  <YearPicker />
                  <MonthPicker />
                  <DayPicker />
                </>
              )}
              
              {mode !== 'date' && (
                <>
                  <HourPicker />
                  <MinutePicker />
                </>
              )}
            </View>
            
            <View style={styles.datetimeActions}>
              <TouchableOpacity style={styles.actionButton} onPress={closePicker}>
                <Text style={styles.actionText}>取消</Text>
              </TouchableOpacity>
              <TouchableOpacity style={[styles.actionButton, styles.confirmButton]} onPress={confirmSelection}>
                <Text style={[styles.actionText, styles.confirmText]}>确定</Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </Modal>
    </>
  );
};

// Main App Component
const DatetimePickerComponentApp = () => {
  const [selectedDate, setSelectedDate] = useState(new Date());
  const [selectedTime, setSelectedTime] = useState(new Date());
  const [selectedDateTime, setSelectedDateTime] = useState(new Date());

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>时间选择器</Text>
        <Text style={styles.headerSubtitle}>美观实用的时间选择组件</Text>
      </View>
      
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>日期选择</Text>
        <View style={styles.pickerWrapper}>
          <DatetimePicker
            mode="date"
            value={selectedDate}
            onChange={setSelectedDate}
            placeholder="请选择日期"
          />
        </View>
        <View style={styles.selectedValueCard}>
          <Text style={styles.selectedValueText}>选中日期: {selectedDate.toLocaleDateString('zh-CN')}</Text>
        </View>
      </View>
      
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>时间选择</Text>
        <View style={styles.pickerWrapper}>
          <DatetimePicker
            mode="time"
            value={selectedTime}
            onChange={setSelectedTime}
            placeholder="请选择时间"
          />
        </View>
        <View style={styles.selectedValueCard}>
          <Text style={styles.selectedValueText}>选中时间: {selectedTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}</Text>
        </View>
      </View>
      
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>日期时间选择</Text>
        <View style={styles.pickerWrapper}>
          <DatetimePicker
            mode="datetime"
            value={selectedDateTime}
            onChange={setSelectedDateTime}
            placeholder="请选择日期和时间"
          />
        </View>
        <View style={styles.selectedValueCard}>
          <Text style={styles.selectedValueText}>选中时间: {selectedDateTime.toLocaleString('zh-CN')}</Text>
        </View>
      </View>
      
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>功能演示</Text>
        <View style={styles.demosContainer}>
          <View style={styles.demoItem}>
            <Icon name="calendar" size={24} color="#1890ff" style={styles.demoIcon} />
            <View>
              <Text style={styles.demoTitle}>日期选择</Text>
              <Text style={styles.demoDesc}>单独选择年月日</Text>
            </View>
          </View>
          
          <View style={styles.demoItem}>
            <Icon name="clock" size={24} color="#52c41a" style={styles.demoIcon} />
            <View>
              <Text style={styles.demoTitle}>时间选择</Text>
              <Text style={styles.demoDesc}>单独选择时分</Text>
            </View>
          </View>
          
          <View style={styles.demoItem}>
            <Icon name="time" size={24} color="#722ed1" style={styles.demoIcon} />
            <View>
              <Text style={styles.demoTitle}>日期时间</Text>
              <Text style={styles.demoDesc}>同时选择日期和时间</Text>
            </View>
          </View>
        </View>
      </View>
      
      <View style={styles.usageSection}>
        <Text style={styles.sectionTitle}>使用方法</Text>
        <View style={styles.codeBlock}>
          <Text style={styles.codeText}>{'<DatetimePicker'}</Text>
          <Text style={styles.codeText}>  mode="datetime"</Text>
          <Text style={styles.codeText}>  value={'{selectedDate}'}</Text>
          <Text style={styles.codeText}>  onChange={'{setSelectedDate}'}{'\n'}/></Text>
        </View>
        <Text style={styles.description}>
          DatetimePicker组件提供了完整的日期时间选择功能,支持日期、时间或日期时间选择模式。
          通过mode属性设置选择模式,value属性传递初始值,onChange回调处理选择结果。
        </Text>
      </View>
      
      <View style={styles.featuresSection}>
        <Text style={styles.sectionTitle}>功能特性</Text>
        <View style={styles.featuresList}>
          <View style={styles.featureItem}>
            <Icon name="calendar" size={20} color="#1890ff" style={styles.featureIcon} />
            <Text style={styles.featureText}>三种选择模式</Text>
          </View>
          <View style={styles.featureItem}>
            <Icon name="clock" size={20} color="#52c41a" style={styles.featureIcon} />
            <Text style={styles.featureText}>数字滚动选择</Text>
          </View>
          <View style={styles.featureItem}>
            <Icon name="confirm" size={20} color="#722ed1" style={styles.featureIcon} />
            <Text style={styles.featureText}>确认/取消操作</Text>
          </View>
          <View style={styles.featureItem}>
            <Icon name="time" size={20} color="#fa8c16" style={styles.featureIcon} />
            <Text style={styles.featureText}>自定义格式显示</Text>
          </View>
        </View>
      </View>
      
      <View style={styles.footer}>
        <Text style={styles.footerText}>© 2023 时间选择器 | 现代化UI组件库</Text>
      </View>
    </ScrollView>
  );
};

const { width, height } = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f0f5ff',
  },
  header: {
    backgroundColor: '#ffffff',
    paddingVertical: 30,
    paddingHorizontal: 20,
    marginBottom: 10,
    borderBottomWidth: 1,
    borderBottomColor: '#e8e8e8',
  },
  headerTitle: {
    fontSize: 28,
    fontWeight: '700',
    color: '#1d39c4',
    textAlign: 'center',
    marginBottom: 5,
  },
  headerSubtitle: {
    fontSize: 16,
    color: '#8c8c8c',
    textAlign: 'center',
  },
  section: {
    marginBottom: 25,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: '700',
    color: '#1d39c4',
    paddingHorizontal: 20,
    paddingBottom: 15,
  },
  pickerWrapper: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 12,
    padding: 15,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  selectedValueCard: {
    backgroundColor: '#f0f5ff',
    marginHorizontal: 15,
    borderRadius: 12,
    padding: 15,
    marginTop: 10,
    borderWidth: 1,
    borderColor: '#adc6ff',
  },
  selectedValueText: {
    fontSize: 16,
    color: '#1d39c4',
    fontWeight: '500',
    textAlign: 'center',
  },
  demosContainer: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  demoItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 20,
  },
  demoItemLast: {
    marginBottom: 0,
  },
  demoIcon: {
    marginRight: 15,
  },
  demoTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#262626',
    marginBottom: 3,
  },
  demoDesc: {
    fontSize: 14,
    color: '#8c8c8c',
  },
  usageSection: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    marginBottom: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  codeBlock: {
    backgroundColor: '#1d1f21',
    borderRadius: 8,
    padding: 15,
    marginBottom: 15,
  },
  codeText: {
    fontFamily: 'monospace',
    color: '#c5c8c6',
    fontSize: 14,
    lineHeight: 22,
  },
  description: {
    fontSize: 15,
    color: '#595959',
    lineHeight: 22,
  },
  featuresSection: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    marginBottom: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  featuresList: {
    paddingLeft: 10,
  },
  featureItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 15,
  },
  featureIcon: {
    marginRight: 15,
  },
  featureText: {
    fontSize: 16,
    color: '#262626',
  },
  footer: {
    paddingVertical: 20,
    alignItems: 'center',
  },
  footerText: {
    color: '#bfbfbf',
    fontSize: 14,
  },
  // DatetimePicker Styles
  datetimeTrigger: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#ffffff',
    borderWidth: 1,
    borderColor: '#d9d9d9',
    borderRadius: 6,
    paddingHorizontal: 15,
    paddingVertical: 12,
    justifyContent: 'space-between',
  },
  triggerIcon: {
    marginRight: 10,
  },
  datetimeText: {
    flex: 1,
    fontSize: 16,
    color: '#595959',
  },
  datetimeOverlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'flex-end',
  },
  datetimeContainer: {
    backgroundColor: '#ffffff',
    height: height * 0.5,
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
  },
  datetimeHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  datetimeTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#262626',
  },
  datetimePickers: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    alignItems: 'center',
    flex: 1,
    paddingVertical: 20,
  },
  numberPickerContainer: {
    alignItems: 'center',
  },
  pickerButton: {
    padding: 10,
  },
  pickerValueContainer: {
    alignItems: 'center',
    marginVertical: 10,
  },
  pickerValue: {
    fontSize: 24,
    fontWeight: '600',
    color: '#262626',
  },
  pickerLabel: {
    fontSize: 14,
    color: '#8c8c8c',
    marginTop: 5,
  },
  datetimeActions: {
    flexDirection: 'row',
    padding: 20,
    borderTopWidth: 1,
    borderTopColor: '#f0f0f0',
  },
  actionButton: {
    flex: 1,
    paddingVertical: 12,
    alignItems: 'center',
    borderRadius: 6,
    borderWidth: 1,
    borderColor: '#d9d9d9',
    marginHorizontal: 10,
  },
  confirmButton: {
    backgroundColor: '#1890ff',
    borderColor: '#1890ff',
  },
  actionText: {
    fontSize: 16,
    color: '#595959',
  },
  confirmText: {
    color: '#ffffff',
  },
});

export default DatetimePickerComponentApp;

这段代码展示了 React Native 在鸿蒙系统上的两种日期选择器实现方案,涉及重要的跨平台兼容性技术考量。

第三方库方案的技术实现‌

代码使用了 @react-native-community/datetimepicker 库,这是 React Native 生态中处理日期时间选择的推荐方案。在鸿蒙系统上,该库通过鸿蒙的 ACE 引擎桥接层调用系统原生日期选择组件。Platform.OS === ‘ios’ 这一条件判断体现了对鸿蒙系统特性的深度理解——由于鸿蒙基于 Android 开源项目,其 Platform.OS 返回值通常为 android,因此鸿蒙设备会遵循 Android 平台的显示逻辑。

状态管理的技术细节‌

组件通过三个 useState 钩子管理核心状态:date 存储当前选择的日期时间值,采用 Unix 时间戳初始化确保跨平台一致性;mode 控制选择器的工作模式;show 管理组件的显示状态。onChange 回调函数的参数结构 (event, selectedDate) 遵循了 React Native 社区标准。

鸿蒙系统的渲染机制‌

在鸿蒙系统上,DateTimePicker 组件通过鸿蒙的 ArkUI 框架进行渲染。当用户触发选择操作时,event 对象包含 type 属性标识操作类型,selectedDate 提供用户选择的时间值。条件渲染逻辑 {show && …} 确保了组件只在需要时挂载到渲染树中。

自定义组件的架构设计‌

第二种方案采用了完全自定义的组件架构。Icon 组件通过 Unicode 符号映射机制实现图标功能,这种设计避免了在鸿蒙系统上可能存在的图标资源加载问题。getIconSymbol 方法通过 switch-case 语句将语义化的图标名称转换为对应的 Unicode 字符。

日期处理的核心算法‌

formatDate 函数根据不同的 mode 参数实现了条件格式化逻辑。对于日期模式返回年月日格式,时间模式返回时分格式,完整模式则组合显示。这种算法设计确保了在不同使用场景下的输出一致性。

在这里插入图片描述

数值选择器的边界控制‌

NumberPicker 组件实现了完整的数值边界检查机制。increment 和 decrement 方法通过比较当前值与最大最小值,防止用户选择超出有效范围的数值。

鸿蒙系统的权限管理‌

代码注释中提到的权限问题在鸿蒙系统上有特殊考量。虽然 React Native 应用通常不需要手动声明权限,但在鸿蒙的分布式架构中,访问系统时间可能需要通过 ohos.permission.GET_BUNDLE_INFO 或相关权限,这取决于鸿蒙系统的具体安全策略。

平台特定的渲染优化‌

在鸿蒙系统上,自定义日期选择器的渲染性能需要特别关注。由于鸿蒙采用方舟编译器的 AOT 优化,JavaScript 代码在鸿蒙上的执行效率可能不同于传统 Android 系统,这需要在组件设计时考虑渲染性能的优化。


打包

接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

在这里插入图片描述

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

在这里插入图片描述

最后运行效果图如下显示:请添加图片描述

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐