React Native for OpenHarmony 实战:待办事项实现

今天我们用 React Native 实现一个待办事项工具,支持添加、完成、删除任务,以及筛选功能。

数据结构

tsx 复制代码
import React, { useState, useRef } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';

interface Todo {
  id: number;
  text: string;
  done: boolean;
  anim: Animated.Value;
}

待办事项的数据结构。

四个属性

  • id:唯一标识,用时间戳生成
  • text:任务内容
  • done:是否完成
  • anim:动画值,控制出现和消失动画

为什么每个任务都有动画值?因为每个任务的动画是独立的。添加任务时,新任务从 0 缩放到 1。删除任务时,该任务从 1 缩放到 0。如果用全局动画值,所有任务会同时动画,效果不好。

状态设计

tsx 复制代码
export const TodoList: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [input, setInput] = useState('');
  const [filter, setFilter] = useState<'all' | 'active' | 'done'>('all');
  
  const addBtnAnim = useRef(new Animated.Value(1)).current;
  const inputAnim = useRef(new Animated.Value(1)).current;

状态设计包含任务列表、输入框、筛选器、动画值。

任务列表todosTodo[] 数组,存储所有任务。

输入框input 是字符串,存储用户输入的任务内容。

筛选器filter 有三个值:

  • 'all':显示全部任务
  • 'active':显示未完成任务
  • 'done':显示已完成任务

两个动画值

  • addBtnAnim:添加按钮的缩放动画
  • inputAnim:输入框的缩放动画(虽然代码中没用到)

添加任务

tsx 复制代码
  const addTodo = () => {
    if (!input.trim()) return;
    
    Animated.sequence([
      Animated.timing(addBtnAnim, { toValue: 0.8, duration: 100, useNativeDriver: true }),
      Animated.spring(addBtnAnim, { toValue: 1, friction: 3, useNativeDriver: true }),
    ]).start();
    
    const newTodo: Todo = { id: Date.now(), text: input.trim(), done: false, anim: new Animated.Value(0) };
    setTodos([...todos, newTodo]);
    setInput('');
    
    setTimeout(() => {
      Animated.spring(newTodo.anim, { toValue: 1, friction: 5, useNativeDriver: true }).start();
    }, 50);
  };

添加按钮点击时,创建新任务,触发动画。

验证输入if (!input.trim()) return; 如果输入为空或只有空格,直接返回。

按钮动画

  • 先缩小到 0.8,持续 100ms
  • 再弹回 1,弹簧效果

创建新任务

  • id:用 Date.now() 生成时间戳,保证唯一
  • text:用 trim() 去除首尾空格
  • done:初始值 false
  • anim:初始值 0

更新状态 :用展开运算符 [...todos, newTodo] 创建新数组,添加新任务。

清空输入框setInput('')

延迟动画 :用 setTimeout() 延迟 50ms,让新任务先渲染,再触发动画。动画从 0 弹到 1,弹簧效果。

为什么要延迟?因为如果立即触发动画,新任务还没渲染,动画不会生效。延迟 50ms 让 React 先完成渲染,再启动动画。

切换完成状态

tsx 复制代码
  const toggleTodo = (id: number) => {
    setTodos(todos.map(t => {
      if (t.id === id) {
        Animated.sequence([
          Animated.timing(t.anim, { toValue: 1.1, duration: 100, useNativeDriver: true }),
          Animated.spring(t.anim, { toValue: 1, friction: 3, useNativeDriver: true }),
        ]).start();
        return { ...t, done: !t.done };
      }
      return t;
    }));
  };

点击复选框时,切换任务的完成状态。

遍历任务 :用 map() 遍历所有任务。

找到目标任务if (t.id === id) 判断是否是点击的任务。

动画

  • 先放大到 1.1,持续 100ms
  • 再弹回 1,弹簧效果

切换状态done: !t.done 取反。如果是 false 变成 true,如果是 true 变成 false

返回新对象 :用展开运算符 { ...t, done: !t.done } 创建新对象,保持不可变性。

其他任务 :直接返回 t,不修改。

为什么用 map 而不是 forEach ?因为 map() 返回新数组,符合 React 的不可变性原则。forEach() 不返回值,无法更新状态。

删除任务

tsx 复制代码
  const deleteTodo = (id: number) => {
    const todo = todos.find(t => t.id === id);
    if (todo) {
      Animated.timing(todo.anim, { toValue: 0, duration: 200, useNativeDriver: true }).start(() => {
        setTodos(todos.filter(t => t.id !== id));
      });
    }
  };

点击删除按钮时,删除任务。

找到任务 :用 find() 找到要删除的任务。

动画:把动画值从 1 缩小到 0,持续 200ms。

动画完成回调 :在 start() 的回调函数中删除任务。用 filter() 过滤掉 id 匹配的任务。

为什么在回调中删除?因为要等动画完成后再删除。如果立即删除,任务会突然消失,没有动画效果。

清除已完成

tsx 复制代码
  const clearDone = () => {
    setTodos(todos.filter(t => !t.done));
  };

清除所有已完成的任务。用 filter() 过滤掉 donetrue 的任务。

筛选和统计

tsx 复制代码
  const filtered = todos.filter(t => {
    if (filter === 'active') return !t.done;
    if (filter === 'done') return t.done;
    return true;
  });

  const activeCount = todos.filter(t => !t.done).length;

根据筛选器过滤任务,统计未完成任务数量。

筛选逻辑

  • 'active':返回 !t.done,只显示未完成任务
  • 'done':返回 t.done,只显示已完成任务
  • 'all':返回 true,显示全部任务

统计未完成 :用 filter() 过滤出未完成任务,再用 .length 获取数量。

界面渲染:头部和输入

tsx 复制代码
  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerIcon}>✅</Text>
        <Text style={styles.headerTitle}>待办事项</Text>
      </View>

      <View style={styles.inputRow}>
        <Animated.View style={[styles.inputWrapper, { transform: [{ scale: inputAnim }] }]}>
          <TextInput style={styles.input} value={input} onChangeText={setInput} placeholder="添加待办事项..." placeholderTextColor="#666" onSubmitEditing={addTodo} />
        </Animated.View>
        <Animated.View style={{ transform: [{ scale: addBtnAnim }] }}>
          <TouchableOpacity style={styles.addBtn} onPress={addTodo} activeOpacity={0.8}>
            <Text style={styles.addBtnText}>+</Text>
          </TouchableOpacity>
        </Animated.View>
      </View>

头部显示标题,输入区域包含输入框和添加按钮。

头部

  • 图标:✅ 对勾
  • 标题:待办事项

输入区域

  • 横向布局
  • 输入框:占满剩余空间,占位符"添加待办事项..."
  • onSubmitEditing={addTodo}:按回车键时添加任务
  • 添加按钮:圆形,蓝色背景,"+" 号

为什么用圆形按钮?因为圆形按钮更符合移动端的设计规范。而且 "+" 号是通用的添加符号,用户一看就懂。

筛选按钮

tsx 复制代码
      <View style={styles.filters}>
        {(['all', 'active', 'done'] as const).map(f => (
          <TouchableOpacity key={f} style={[styles.filterBtn, filter === f && styles.filterBtnActive]} onPress={() => setFilter(f)} activeOpacity={0.7}>
            <Text style={[styles.filterText, filter === f && styles.filterTextActive]}>
              {f === 'all' ? '📋 全部' : f === 'active' ? '⏳ 未完成' : '✅ 已完成'}
            </Text>
          </TouchableOpacity>
        ))}
      </View>

三个筛选按钮:全部、未完成、已完成。

遍历筛选器 :用 map() 遍历 ['all', 'active', 'done'] 数组。

激活状态filter === f && styles.filterBtnActive 如果当前筛选器等于按钮的值,应用激活样式。

按钮文字

  • 'all':📋 全部
  • 'active':⏳ 未完成
  • 'done':✅ 已完成

为什么用图标?因为图标能快速传达含义。📋 代表列表,⏳ 代表进行中,✅ 代表完成。

任务列表

tsx 复制代码
      <ScrollView style={styles.list}>
        {filtered.map(todo => (
          <Animated.View key={todo.id} style={{
            transform: [{ scale: todo.anim }],
            opacity: todo.anim,
          }}>
            <View style={styles.todoItem}>
              <TouchableOpacity style={[styles.checkbox, todo.done && styles.checkboxDone]} onPress={() => toggleTodo(todo.id)} activeOpacity={0.7}>
                {todo.done && <Text style={styles.checkmark}>✓</Text>}
              </TouchableOpacity>
              <Text style={[styles.todoText, todo.done && styles.todoTextDone]}>{todo.text}</Text>
              <TouchableOpacity style={styles.deleteBtn} onPress={() => deleteTodo(todo.id)}>
                <Text style={styles.deleteText}>×</Text>
              </TouchableOpacity>
            </View>
          </Animated.View>
        ))}
      </ScrollView>

滚动列表显示所有任务。

遍历任务 :用 map() 遍历 filtered 数组(已筛选的任务)。

任务动画

  • 缩放:scale: todo.anim
  • 透明度:opacity: todo.anim

任务卡片

  • 复选框:圆形,点击切换完成状态
  • 如果已完成,显示 ✓ 号,背景变蓝色
  • 任务文字:占满剩余空间
  • 如果已完成,文字变灰色,加删除线
  • 删除按钮:× 号,红色

为什么用删除线?因为删除线是通用的"已完成"标记。用户看到删除线,就知道任务已完成。

底部统计

tsx 复制代码
      <View style={styles.footer}>
        <Text style={styles.count}>📝 {activeCount} 项未完成</Text>
        <TouchableOpacity onPress={clearDone}>
          <Text style={styles.clearText}>🗑️ 清除已完成</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
};

底部显示未完成任务数量和清除按钮。

未完成数量:📝 图标 + 数量 + "项未完成"。

清除按钮:🗑️ 图标 + "清除已完成",红色文字。

鸿蒙 ArkTS 对比:任务管理

typescript 复制代码
@State todos: Todo[] = []
@State input: string = ''
@State filter: 'all' | 'active' | 'done' = 'all'

addTodo() {
  if (!this.input.trim()) return
  
  const newTodo: Todo = {
    id: Date.now(),
    text: this.input.trim(),
    done: false,
    anim: 0
  }
  this.todos.push(newTodo)
  this.input = ''
  
  animateTo({ duration: 300 }, () => {
    newTodo.anim = 1
  })
}

toggleTodo(id: number) {
  const index = this.todos.findIndex(t => t.id === id)
  if (index !== -1) {
    this.todos[index].done = !this.todos[index].done
  }
}

deleteTodo(id: number) {
  const index = this.todos.findIndex(t => t.id === id)
  if (index !== -1) {
    animateTo({ duration: 200 }, () => {
      this.todos[index].anim = 0
    })
    setTimeout(() => {
      this.todos.splice(index, 1)
    }, 200)
  }
}

clearDone() {
  this.todos = this.todos.filter(t => !t.done)
}

ArkTS 中的任务管理逻辑完全一样。核心是数组操作:push() 添加、map() 更新、filter() 删除。Date.now() 生成唯一 ID,trim() 去除空格,都是标准 JavaScript 语法。

动画差异 :ArkTS 用 animateTo() 触发动画,React Native 用 Animated.spring()。但动画效果一样:缩放和透明度。

样式定义

tsx 复制代码
const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0f0f23', padding: 16 },
  header: { alignItems: 'center', marginBottom: 20 },
  headerIcon: { fontSize: 40, marginBottom: 4 },
  headerTitle: { fontSize: 24, fontWeight: '700', color: '#fff' },
  inputRow: { flexDirection: 'row', marginBottom: 16 },
  inputWrapper: { flex: 1, backgroundColor: '#1a1a3e', borderRadius: 12, marginRight: 10, borderWidth: 1, borderColor: '#3a3a6a' },
  input: { padding: 14, fontSize: 16, color: '#fff' },
  addBtn: { width: 54, height: 54, backgroundColor: '#4A90D9', borderRadius: 27, justifyContent: 'center', alignItems: 'center' },
  addBtnText: { color: '#fff', fontSize: 32, fontWeight: '300' },
  filters: { flexDirection: 'row', marginBottom: 16 },
  filterBtn: { flex: 1, padding: 12, backgroundColor: '#1a1a3e', marginHorizontal: 4, borderRadius: 10, alignItems: 'center', borderWidth: 1, borderColor: '#3a3a6a' },
  filterBtnActive: { backgroundColor: '#4A90D9', borderColor: '#4A90D9' },
  filterText: { color: '#888', fontSize: 13 },
  filterTextActive: { color: '#fff', fontWeight: '600' },
  list: { flex: 1 },
  todoItem: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#1a1a3e', padding: 14, borderRadius: 12, marginBottom: 10, borderWidth: 1, borderColor: '#3a3a6a' },
  checkbox: { width: 28, height: 28, borderRadius: 14, borderWidth: 2, borderColor: '#3a3a6a', marginRight: 14, justifyContent: 'center', alignItems: 'center' },
  checkboxDone: { backgroundColor: '#4A90D9', borderColor: '#4A90D9' },
  checkmark: { color: '#fff', fontSize: 16, fontWeight: '700' },
  todoText: { flex: 1, fontSize: 16, color: '#fff' },
  todoTextDone: { color: '#666', textDecorationLine: 'line-through' },
  deleteBtn: { padding: 6 },
  deleteText: { color: '#e74c3c', fontSize: 24 },
  footer: { flexDirection: 'row', justifyContent: 'space-between', paddingTop: 16, borderTopWidth: 1, borderTopColor: '#3a3a6a' },
  count: { color: '#888' },
  clearText: { color: '#e74c3c' },
});

容器用深蓝黑色背景。输入区域横向布局,添加按钮是圆形。筛选按钮横向排列,激活时背景变蓝色。任务卡片横向布局,复选框是圆形,已完成时背景变蓝色。任务文字已完成时变灰色加删除线。底部统计区域横向布局,两端对齐。

小结

这个待办事项工具展示了任务管理和筛选功能的实现。用数组操作管理任务列表,每个任务有独立的动画值。添加任务时弹簧动画,删除任务时缩小消失。筛选器支持全部、未完成、已完成三种模式。


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

相关推荐
north_eagle1 小时前
ReAct 框架详解
前端·react.js·前端框架
OEC小胖胖1 小时前
13|React Server Components(RSC)在仓库中的落点与边界
前端·react.js·前端框架·react·开源库
OEC小胖胖1 小时前
14|Hook 的实现视角:从 API 到 Fiber Update Queue 的连接点
前端·react.js·前端框架·react·开源库
i7i8i9com2 小时前
React 19学习基础-2 新特性
javascript·学习·react.js
军军君012 小时前
Three.js基础功能学习十:渲染器与辅助对象
开发语言·前端·javascript·学习·3d·前端框架·ecmascript
Marshmallowc2 小时前
React useState 数组 push/splice 后页面不刷新?深度解析状态被『蹭』出来的影子更新陷阱
前端·react.js·前端框架
VT.馒头2 小时前
【力扣】2631. 分组
javascript·算法·leetcode·typescript
许同2 小时前
JS-WPS 自动化办公(3)数据整理-找数据
开发语言·javascript·wps
qq_3168377511 小时前
uni.chooseMedia 读取base64 或 二进制
开发语言·前端·javascript