React Native 鸿蒙跨平台开发:实现一个计时器工具

一、核心原理:计时器的设计与实现

1.1 计时器的设计理念

计时器是一个实用的时间管理工具,主要用于:

  • 倒计时:设置特定时间后进行提醒
  • 秒表:记录时间间隔
  • 定时提醒:在特定时间点提醒用户
  • 时间追踪:记录任务耗时

1.2 计时器的核心要素

一个完整的计时器需要考虑:

  1. 时间显示:显示小时、分钟、秒
  2. 时间设置:允许用户设置时间
  3. 开始/暂停:控制计时器的运行状态
  4. 重置功能:将计时器重置到初始状态
  5. 完成提醒:时间到达时的提醒
  6. 状态管理:管理计时器的运行状态
  7. 格式化显示:将秒数格式化为 HH:MM:SS 格式

1.3 实现原理

计时器的核心实现原理:

  • 使用 useState 管理剩余时间
  • 使用 useEffect 和 setInterval 实现倒计时
  • 使用 useRef 存储定时器引用
  • 将秒数格式化为 HH:MM:SS 格式
  • 使用 TouchableOpacity 实现按钮交互
  • 使用 ScrollView 确保页面可滚动

二、基础计时器实现

2.1 组件结构

计时器组件包含以下部分:

  1. 时间显示:显示剩余时间
  2. 时间设置:设置小时、分钟、秒
  3. 控制按钮:开始、暂停、重置按钮
  4. 进度条:显示剩余时间的进度

2.2 完整代码实现

javascript 复制代码
import React, { useState, useEffect, useRef, memo, useCallback } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  SafeAreaView,
  ScrollView,
} from 'react-native';

// 计时器组件
const Timer = memo(() => {
  const [totalSeconds, setTotalSeconds] = useState(300); // 总时间(秒)
  const [remainingSeconds, setRemainingSeconds] = useState(300); // 剩余时间(秒)
  const [isRunning, setIsRunning] = useState(false); // 是否运行中
  const [hours, setHours] = useState(0); // 设置的小时
  const [minutes, setMinutes] = useState(5); // 设置的分钟
  const [seconds, setSeconds] = useState(0); // 设置的秒

  const timerRef = useRef<NodeJS.Timeout | null>(null);

  // 格式化时间显示
  const formatTime = useCallback((totalSeconds: number): string => {
    const h = Math.floor(totalSeconds / 3600);
    const m = Math.floor((totalSeconds % 3600) / 60);
    const s = totalSeconds % 60;
    return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
  }, []);

  // 开始计时
  const startTimer = useCallback(() => {
    if (isRunning || remainingSeconds <= 0) return;
    setIsRunning(true);
  }, [isRunning, remainingSeconds]);

  // 暂停计时
  const pauseTimer = useCallback(() => {
    setIsRunning(false);
  }, []);

  // 重置计时
  const resetTimer = useCallback(() => {
    setIsRunning(false);
    const newTotal = hours * 3600 + minutes * 60 + seconds;
    setTotalSeconds(newTotal);
    setRemainingSeconds(newTotal);
  }, [hours, minutes, seconds]);

  // 应用设置的时间
  const applySettings = useCallback(() => {
    const newTotal = hours * 3600 + minutes * 60 + seconds;
    if (newTotal > 0) {
      setIsRunning(false);
      setTotalSeconds(newTotal);
      setRemainingSeconds(newTotal);
    }
  }, [hours, minutes, seconds]);

  // 倒计时逻辑
  useEffect(() => {
    if (isRunning && remainingSeconds > 0) {
      timerRef.current = setInterval(() => {
        setRemainingSeconds(prev => {
          if (prev <= 1) {
            setIsRunning(false);
            return 0;
          }
          return prev - 1;
        });
      }, 1000);
    } else {
      if (timerRef.current) {
        clearInterval(timerRef.current);
        timerRef.current = null;
      }
    }

    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current);
        timerRef.current = null;
      }
    };
  }, [isRunning, remainingSeconds]);

  // 计算进度百分比
  const progress = totalSeconds > 0 ? ((totalSeconds - remainingSeconds) / totalSeconds) * 100 : 0;

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView style={styles.scrollView}>
        {/* 标题区域 */}
        <View style={styles.header}>
          <Text style={styles.title}>React Native for Harmony</Text>
          <Text style={styles.subtitle}>计时器工具</Text>
        </View>

        {/* 计时器主体 */}
        <View style={styles.timerContainer}>
          {/* 时间显示 */}
          <View style={styles.timeDisplay}>
            <Text style={styles.timeText}>{formatTime(remainingSeconds)}</Text>
          </View>

          {/* 进度条 */}
          <View style={styles.progressContainer}>
            <View style={styles.progressBar}>
              <View style={[styles.progressFill, { width: `${progress}%` }]} />
            </View>
          </View>

          {/* 控制按钮 */}
          <View style={styles.controlButtons}>
            <TouchableOpacity
              style={[styles.controlButton, isRunning ? styles.pauseButton : styles.startButton]}
              onPress={isRunning ? pauseTimer : startTimer}
              disabled={remainingSeconds <= 0}
            >
              <Text style={styles.controlButtonText}>
                {isRunning ? '暂停' : '开始'}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity
              style={styles.controlButton}
              onPress={resetTimer}
            >
              <Text style={styles.controlButtonText}>重置</Text>
            </TouchableOpacity>
          </View>

          {/* 时间设置 */}
          <View style={styles.settingsContainer}>
            <Text style={styles.settingsTitle}>设置时间</Text>
            <View style={styles.timeInputs}>
              <View style={styles.timeInput}>
                <Text style={styles.timeInputLabel}>小时</Text>
                <TouchableOpacity
                  style={styles.timeInputButton}
                  onPress={() => setHours(prev => Math.min(prev + 1, 23))}
                >
                  <Text style={styles.timeInputButtonText}>+</Text>
                </TouchableOpacity>
                <Text style={styles.timeInputValue}>{hours}</Text>
                <TouchableOpacity
                  style={styles.timeInputButton}
                  onPress={() => setHours(prev => Math.max(prev - 1, 0))}
                >
                  <Text style={styles.timeInputButtonText}>-</Text>
                </TouchableOpacity>
              </View>

              <View style={styles.timeInput}>
                <Text style={styles.timeInputLabel}>分钟</Text>
                <TouchableOpacity
                  style={styles.timeInputButton}
                  onPress={() => setMinutes(prev => Math.min(prev + 1, 59))}
                >
                  <Text style={styles.timeInputButtonText}>+</Text>
                </TouchableOpacity>
                <Text style={styles.timeInputValue}>{minutes}</Text>
                <TouchableOpacity
                  style={styles.timeInputButton}
                  onPress={() => setMinutes(prev => Math.max(prev - 1, 0))}
                >
                  <Text style={styles.timeInputButtonText}>-</Text>
                </TouchableOpacity>
              </View>

              <View style={styles.timeInput}>
                <Text style={styles.timeInputLabel}>秒</Text>
                <TouchableOpacity
                  style={styles.timeInputButton}
                  onPress={() => setSeconds(prev => Math.min(prev + 1, 59))}
                >
                  <Text style={styles.timeInputButtonText}>+</Text>
                </TouchableOpacity>
                <Text style={styles.timeInputValue}>{seconds}</Text>
                <TouchableOpacity
                  style={styles.timeInputButton}
                  onPress={() => setSeconds(prev => Math.max(prev - 1, 0))}
                >
                  <Text style={styles.timeInputButtonText}>-</Text>
                </TouchableOpacity>
              </View>
            </View>

            <TouchableOpacity
              style={styles.applyButton}
              onPress={applySettings}
            >
              <Text style={styles.applyButtonText}>应用设置</Text>
            </TouchableOpacity>
          </View>
        </View>

        {/* 说明区域 */}
        <View style={styles.infoCard}>
          <Text style={styles.infoTitle}>💡 功能说明</Text>
          <Text style={styles.infoText}>• 倒计时:设置时间后自动倒计时</Text>
          <Text style={styles.infoText}>• 暂停/继续:随时暂停和恢复计时</Text>
          <Text style={styles.infoText}>• 重置功能:一键重置到初始时间</Text>
          <Text style={styles.infoText}>• 进度显示:实时显示剩余时间进度</Text>
          <Text style={styles.infoText}>• 自定义时间:支持设置小时、分钟、秒</Text>
          <Text style={styles.infoText}>• 鸿蒙端完美兼容,运行稳定</Text>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
});

Timer.displayName = 'Timer';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F7FA',
  },
  scrollView: {
    flex: 1,
  },

  // ======== 标题区域 ========
  header: {
    padding: 20,
    backgroundColor: '#FFFFFF',
    borderBottomWidth: 1,
    borderBottomColor: '#EBEEF5',
  },
  title: {
    fontSize: 24,
    fontWeight: '700',
    color: '#303133',
    textAlign: 'center',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 16,
    fontWeight: '500',
    color: '#909399',
    textAlign: 'center',
  },

  // ======== 计时器容器 ========
  timerContainer: {
    backgroundColor: '#FFFFFF',
    margin: 16,
    borderRadius: 16,
    padding: 20,
    shadowColor: '#000000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 8,
    elevation: 4,
  },

  // ======== 时间显示 ========
  timeDisplay: {
    backgroundColor: '#F5F7FA',
    borderRadius: 12,
    padding: 30,
    alignItems: 'center',
    marginBottom: 20,
  },
  timeText: {
    fontSize: 48,
    fontWeight: '700',
    color: '#303133',
    fontFamily: 'monospace',
  },

  // ======== 进度条 ========
  progressContainer: {
    marginBottom: 20,
  },
  progressBar: {
    height: 8,
    backgroundColor: '#EBEEF5',
    borderRadius: 4,
    overflow: 'hidden',
  },
  progressFill: {
    height: '100%',
    backgroundColor: '#409EFF',
    borderRadius: 4,
  },

  // ======== 控制按钮 ========
  controlButtons: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginBottom: 30,
  },
  controlButton: {
    backgroundColor: '#F5F7FA',
    paddingHorizontal: 24,
    paddingVertical: 12,
    borderRadius: 8,
    marginHorizontal: 8,
  },
  startButton: {
    backgroundColor: '#67C23A',
  },
  pauseButton: {
    backgroundColor: '#E6A23C',
  },
  controlButtonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#303133',
  },

  // ======== 时间设置 ========
  settingsContainer: {
    borderTopWidth: 1,
    borderTopColor: '#EBEEF5',
    paddingTop: 20,
  },
  settingsTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#303133',
    marginBottom: 16,
    textAlign: 'center',
  },
  timeInputs: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginBottom: 20,
  },
  timeInput: {
    alignItems: 'center',
  },
  timeInputLabel: {
    fontSize: 14,
    color: '#909399',
    marginBottom: 8,
  },
  timeInputButton: {
    width: 40,
    height: 40,
    backgroundColor: '#F5F7FA',
    borderRadius: 8,
    justifyContent: 'center',
    alignItems: 'center',
    marginBottom: 8,
  },
  timeInputButtonText: {
    fontSize: 20,
    fontWeight: '600',
    color: '#409EFF',
  },
  timeInputValue: {
    fontSize: 24,
    fontWeight: '700',
    color: '#303133',
    marginBottom: 8,
  },
  applyButton: {
    backgroundColor: '#409EFF',
    paddingVertical: 12,
    borderRadius: 8,
    alignItems: 'center',
  },
  applyButtonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#FFFFFF',
  },

  // ======== 信息卡片 ========
  infoCard: {
    backgroundColor: '#FFFFFF',
    borderRadius: 12,
    padding: 16,
    margin: 16,
    marginTop: 0,
    shadowColor: '#000000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 8,
    elevation: 4,
  },
  infoTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#303133',
    marginBottom: 12,
  },
  infoText: {
    fontSize: 14,
    color: '#606266',
    lineHeight: 22,
    marginBottom: 6,
  },
});

export default Timer;

三、核心实现要点

3.1 时间格式化

将秒数格式化为 HH:MM:SS 格式:

javascript 复制代码
const formatTime = useCallback((totalSeconds: number): string => {
  const h = Math.floor(totalSeconds / 3600);
  const m = Math.floor((totalSeconds % 3600) / 60);
  const s = totalSeconds % 60;
  return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}, []);

格式化要点:

  • 使用 Math.floor 计算小时、分钟、秒
  • 使用 padStart(2, '0') 确保两位数显示
  • 返回 HH:MM:SS 格式的字符串

3.2 倒计时逻辑

使用 useEffect 和 setInterval 实现倒计时:

javascript 复制代码
useEffect(() => {
  if (isRunning && remainingSeconds > 0) {
    timerRef.current = setInterval(() => {
      setRemainingSeconds(prev => {
        if (prev <= 1) {
          setIsRunning(false);
          return 0;
        }
        return prev - 1;
      });
    }, 1000);
  } else {
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  }

  return () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  };
}, [isRunning, remainingSeconds]);

倒计时要点:

  • 使用 useRef 存储定时器引用
  • 每秒减少剩余时间
  • 时间到达 0 时自动停止
  • 组件卸载时清除定时器

3.3 时间设置

允许用户设置小时、分钟、秒:

javascript 复制代码
const [hours, setHours] = useState(0);
const [minutes, setMinutes] = useState(5);
const [seconds, setSeconds] = useState(0);

const applySettings = useCallback(() => {
  const newTotal = hours * 3600 + minutes * 60 + seconds;
  if (newTotal > 0) {
    setIsRunning(false);
    setTotalSeconds(newTotal);
    setRemainingSeconds(newTotal);
  }
}, [hours, minutes, seconds]);

设置要点:

  • 分别管理小时、分钟、秒
  • 使用 + 和 - 按钮调整时间
  • 应用设置时计算总秒数
  • 限制时间范围(小时 0-23,分钟和秒 0-59)

3.4 进度条显示

显示剩余时间的进度:

javascript 复制代码
const progress = totalSeconds > 0 ? ((totalSeconds - remainingSeconds) / totalSeconds) * 100 : 0;

<View style={styles.progressBar}>
  <View style={[styles.progressFill, { width: `${progress}%` }]} />
</View>

进度条要点:

  • 计算已过时间的百分比
  • 使用 width 属性控制进度条长度
  • 使用百分比字符串设置宽度

四、性能优化

4.1 使用 useCallback 优化

使用 useCallback 缓存回调函数:

javascript 复制代码
const formatTime = useCallback((totalSeconds: number): string => {
  const h = Math.floor(totalSeconds / 3600);
  const m = Math.floor((totalSeconds % 3600) / 60);
  const s = totalSeconds % 60;
  return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}, []);

const startTimer = useCallback(() => {
  if (isRunning || remainingSeconds <= 0) return;
  setIsRunning(true);
}, [isRunning, remainingSeconds]);

为什么使用 useCallback?

  • 避免每次渲染都创建新函数
  • 减少子组件的重新渲染
  • 提升整体性能

4.2 使用 useRef 存储定时器

使用 useRef 存储定时器引用:

javascript 复制代码
const timerRef = useRef<NodeJS.Timeout | null>(null);

timerRef.current = setInterval(() => {
  setRemainingSeconds(prev => prev - 1);
}, 1000);

为什么使用 useRef?

  • 避免定时器引用在重新渲染时丢失
  • 确保能够正确清除定时器
  • 避免内存泄漏

4.3 使用 memo 优化

使用 memo 包装组件:

javascript 复制代码
const Timer = memo(() => {
  // ...
});

为什么使用 memo?

  • 避免不必要的重新渲染
  • 提升整体性能
  • 在复杂应用中效果更明显

五、常见问题与解决方案

5.1 定时器不停止

问题现象: 组件卸载后定时器仍在运行

可能原因:

  1. 没有清除定时器
  2. 清除定时器的逻辑不正确

解决方案:

javascript 复制代码
useEffect(() => {
  if (isRunning && remainingSeconds > 0) {
    timerRef.current = setInterval(() => {
      setRemainingSeconds(prev => prev - 1);
    }, 1000);
  }

  return () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  };
}, [isRunning, remainingSeconds]);

5.2 时间显示不正确

问题现象: 时间显示格式不正确

可能原因:

  1. 格式化逻辑错误
  2. 没有使用 padStart

解决方案:

javascript 复制代码
const formatTime = useCallback((totalSeconds: number): string => {
  const h = Math.floor(totalSeconds / 3600);
  const m = Math.floor((totalSeconds % 3600) / 60);
  const s = totalSeconds % 60;
  return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}, []);

5.3 进度条不更新

问题现象: 进度条不随时间变化

可能原因:

  1. 进度计算逻辑错误
  2. 没有正确更新状态

解决方案:

javascript 复制代码
const progress = totalSeconds > 0 ? ((totalSeconds - remainingSeconds) / totalSeconds) * 100 : 0;

<View style={styles.progressBar}>
  <View style={[styles.progressFill, { width: `${progress}%` }]} />
</View>

六、扩展用法

6.1 添加音效提醒

添加时间到达时的音效提醒:

javascript 复制代码
import { Sound } from 'react-native-sound';

const playAlarm = useCallback(() => {
  const sound = new Sound('alarm.mp3', Sound.MAIN_BUNDLE, (error) => {
    if (error) {
      console.log('Failed to load sound', error);
      return;
    }
    sound.play();
  });
}, []);

useEffect(() => {
  if (remainingSeconds === 0) {
    playAlarm();
  }
}, [remainingSeconds, playAlarm]);

6.2 添加震动提醒

添加时间到达时的震动提醒:

javascript 复制代码
import { Vibration } from 'react-native';

useEffect(() => {
  if (remainingSeconds === 0) {
    Vibration.vibrate([500, 200, 500]); // 震动模式
  }
}, [remainingSeconds]);

6.3 添加预设时间

添加常用的时间预设:

javascript 复制代码
const presets = [
  { label: '1分钟', seconds: 60 },
  { label: '5分钟', seconds: 300 },
  { label: '10分钟', seconds: 600 },
  { label: '15分钟', seconds: 900 },
  { label: '30分钟', seconds: 1800 },
];

<View style={styles.presetsContainer}>
  {presets.map(preset => (
    <TouchableOpacity
      key={preset.label}
      style={styles.presetButton}
      onPress={() => {
        setTotalSeconds(preset.seconds);
        setRemainingSeconds(preset.seconds);
        setIsRunning(false);
      }}
    >
      <Text style={styles.presetButtonText}>{preset.label}</Text>
    </TouchableOpacity>
  ))}
</View>

七、总结

计时器是一个实用的时间管理工具,通过本篇文章,我们学习了:

  1. 时间格式化:将秒数格式化为 HH:MM:SS 格式
  2. 倒计时逻辑:使用 useEffect 和 setInterval 实现倒计时
  3. 时间设置:允许用户设置小时、分钟、秒
  4. 进度显示:实时显示剩余时间的进度
  5. 性能优化:使用 useCallback、useRef、memo 优化性能
  6. 错误处理:正确处理定时器的清除

计时器组件在 React Native for Harmony 中表现良好,运行稳定,是一个很好的学习案例。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
Dragon Wu2 小时前
ReactNative MMKV和React Native Keychain存储本地数据
javascript·react native·react.js·前端框架
Never_Satisfied2 小时前
在JavaScript / HTML中,cloneNode()方法详细指南
开发语言·javascript·html
摘星编程2 小时前
React Native for OpenHarmony 实战:Camera 相机组件详解
数码相机·react native·react.js
—Qeyser2 小时前
Flutter组件 - BottomNavigationBar 底部导航栏
开发语言·javascript·flutter
hxjhnct2 小时前
CSS 伪类和伪元素
前端·javascript·css
❆VE❆2 小时前
【css】打造倾斜异形按钮:CSS radial-gradient 与抗锯齿实战解析
前端·javascript·css
pas1362 小时前
33-mini-vue 更新element的children-双端对比diff算法
javascript·vue.js·算法
靓仔建2 小时前
用tdesign-vue-next的t-tree-select做个下拉单选框
javascript·vue.js·tdesign
美酒没故事°12 小时前
vue3拖拽+粘贴的综合上传器
前端·javascript·typescript