RN for OpenHarmony 实战 TodoList 项目:任务完成划线效果
在React Native中实现待办事项的划线效果,关键在于使用textDecorationLine: 'line-through'样式配合透明度调整,为用户提供直观的完成反馈。本文详细解析了实现方法,包括样式叠加技巧、状态管理逻辑,以及与勾选框的视觉配合。同时探讨了深色模式适配、常见问题解决方案,并简要提及可能的动画扩展。这种简洁高效的视觉反馈机制,既符合用户心理预期,又能有效区分已完成和未完成
案例开源地址:https://atomgit.com/lqjmac/rn_openharmony_todolist
你有没有注意到,当我们在纸质待办清单上完成一项任务时,最自然的动作就是划掉它?这个简单的动作给人一种强烈的成就感——“我完成了!”。在数字化的待办应用中,我们同样需要这种仪式感。划线效果不仅仅是一个视觉装饰,它是用户完成任务后获得心理满足的重要来源。
今天我们来聊聊如何在 React Native 中实现这个看似简单却意义重大的功能。
从一个问题开始
假设你刚接手这个 TodoList 项目,产品经理跑过来说:“我们需要让用户一眼就能区分哪些任务完成了,哪些还没完成。”
你会怎么做?
最直观的方案可能是:
- 改变背景颜色?
- 添加一个"已完成"的标签?
- 把完成的任务移到列表底部?
这些方案都可以,但都不如划线效果来得直接和优雅。划线是一个全球通用的符号,不需要任何文字说明,用户就能理解它的含义。
看看我们的实现
在项目代码中,完成状态的视觉反馈是这样实现的:
<Text style={[styles.taskTitle, {color: theme.text}, item.completed && styles.completedText]}>{item.title}</Text>
就这么一行代码,却包含了三层样式的叠加。让我们拆解一下。
第一层是基础样式 styles.taskTitle,定义了文字的基本外观:
taskTitle: {fontSize: 16, fontWeight: '500', marginBottom: 4},
字号 16 像素,中等粗细,底部留 4 像素的间距。这是所有任务标题共享的样式,无论完成与否。
第二层是主题颜色 {color: theme.text},根据当前是深色还是浅色模式,动态设置文字颜色。深色模式下是白色,浅色模式下是深灰色。
第三层是条件样式 item.completed && styles.completedText,只有当任务完成时才会应用:
completedText: {textDecorationLine: 'line-through', opacity: 0.5},
这里用到了一个 JavaScript 的小技巧。&& 运算符在左边为 true 时返回右边的值,为 false 时返回 false。而在样式数组中,false 会被忽略。所以当 item.completed 为 false 时,这个位置就像不存在一样。
textDecorationLine 的秘密
textDecorationLine 是 React Native 中用于文字装饰的属性,它的可选值有:
none- 无装饰(默认值)underline- 下划线line-through- 删除线(就是我们要的划线效果)underline line-through- 同时有下划线和删除线
有趣的是,你可以组合使用这些值。比如 underline line-through 会同时显示下划线和删除线,虽然在待办应用中这样做没什么意义,但在其他场景可能有用。
在 Web 开发中,这个属性叫 text-decoration,而且可以设置更多选项比如装饰线的颜色和样式。React Native 简化了这个属性,只保留了最常用的功能。
为什么还要降低透明度?
你可能注意到了,除了划线,我们还把透明度降到了 0.5:
completedText: {textDecorationLine: 'line-through', opacity: 0.5},
这是一个设计上的考量。单纯的划线效果虽然能表示"完成",但视觉上已完成的任务和未完成的任务还是同样醒目。降低透明度可以让已完成的任务在视觉上"退后",让用户的注意力自然集中在还需要处理的任务上。
想象一下你的任务列表有 20 个任务,其中 15 个已完成。如果所有任务都同样醒目,你需要逐个扫描才能找到未完成的任务。但如果已完成的任务变淡了,未完成的任务就会自动"跳出来"。
这就是所谓的视觉层次(Visual Hierarchy)。通过透明度的差异,我们创造了两个层次:
- 前景层:未完成的任务,需要用户关注
- 背景层:已完成的任务,仅供参考
完整的状态变化链
让我们跟踪一下,当用户点击勾选框时,发生了什么:
const toggleTask = (id: string) => {
setTasks(tasks.map(task => task.id === id ? {...task, completed: !task.completed} : task));
};
- 用户点击勾选框
toggleTask函数被调用,传入任务 IDtasks.map遍历所有任务- 找到匹配的任务,将其
completed属性取反 setTasks更新状态- React 检测到状态变化,触发重新渲染
- 在渲染过程中,
item.completed && styles.completedText的结果改变 - 文字样式更新,划线效果出现或消失
整个过程是即时的,用户点击的瞬间就能看到效果。这种即时反馈对用户体验至关重要。
勾选框的配合
划线效果不是孤立存在的,它需要和勾选框配合才能形成完整的视觉反馈:
<TouchableOpacity onPress={() => toggleTask(item.id)} style={styles.checkbox}>
<View style={[styles.checkboxInner, {borderColor: theme.accent}, item.completed && {backgroundColor: theme.accent}]}>
{item.completed && <Text style={styles.checkmark}>✓</Text>}
</View>
</TouchableOpacity>
当任务完成时:
- 勾选框从空心变成实心(填充主题色)
- 勾选框内显示 ✓ 符号
- 任务标题添加划线
- 任务标题透明度降低
这四个变化同时发生,形成一个完整的"完成"状态。用户不需要思考,直觉就能理解发生了什么。
深色模式下的表现
我们的应用支持深色和浅色两种模式,划线效果在两种模式下都需要清晰可见:
const theme = {
bg: darkMode ? '#0f0f23' : '#f5f5f5',
card: darkMode ? '#1a1a2e' : '#ffffff',
text: darkMode ? '#ffffff' : '#333333',
subText: darkMode ? '#888888' : '#666666',
border: darkMode ? '#2a2a4a' : '#e0e0e0',
accent: '#6c5ce7',
};
在深色模式下,文字是白色的,划线也是白色的。在浅色模式下,文字是深灰色的,划线也是深灰色的。因为划线的颜色继承自文字颜色,所以我们不需要额外处理。
透明度的效果在两种模式下略有不同:
- 深色模式:白色文字 50% 透明度,在深色背景上显示为灰色
- 浅色模式:深灰色文字 50% 透明度,在浅色背景上显示为浅灰色
两种情况下,已完成的任务都会明显变淡,达到了我们想要的效果。
一个常见的坑
在实现划线效果时,有一个容易踩的坑:在某些字体下,划线可能不会穿过文字中心,而是偏上或偏下。
这是因为不同字体的基线(baseline)位置不同,而划线的位置是相对于基线计算的。在大多数情况下,系统默认字体的表现是正常的,但如果你使用了自定义字体,可能需要测试一下。
如果遇到这个问题,一个解决方案是不使用 textDecorationLine,而是用一个绝对定位的 View 来模拟划线:
const StrikethroughText = ({children, strikethrough}) => (
<View style={{position: 'relative'}}>
<Text>{children}</Text>
{strikethrough && (
<View style={{
position: 'absolute',
left: 0,
right: 0,
top: '50%',
height: 1,
backgroundColor: 'currentColor',
}} />
)}
</View>
);
不过在我们的项目中,使用系统默认字体,textDecorationLine 工作得很好,不需要这种 workaround。
动画的可能性
当前的实现是即时切换的,没有动画过渡。如果想要更精致的效果,可以考虑添加动画。
一种思路是让划线从左到右"画"出来:
const AnimatedStrikethrough = ({completed, children}) => {
const widthAnim = useRef(new Animated.Value(completed ? 1 : 0)).current;
useEffect(() => {
Animated.timing(widthAnim, {
toValue: completed ? 1 : 0,
duration: 300,
useNativeDriver: false,
}).start();
}, [completed]);
return (
<View style={{position: 'relative'}}>
<Text>{children}</Text>
<Animated.View style={{
position: 'absolute',
left: 0,
top: '50%',
height: 2,
backgroundColor: '#666',
width: widthAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
}),
}} />
</View>
);
};
这个动画会让划线从左边开始,逐渐延伸到右边,就像真的在用笔划掉一样。
不过要注意,动画虽然好看,但也会增加复杂度和性能开销。对于一个任务列表来说,用户可能会频繁地勾选和取消勾选,过多的动画反而会让人觉得"慢"。所以在实际项目中,我们选择了简单直接的即时切换。
与其他元素的协调
任务卡片上不只有标题,还有分类标签、截止日期、备注等信息:
<View style={styles.taskInfo}>
<Text style={[styles.taskTitle, {color: theme.text}, item.completed && styles.completedText]}>{item.title}</Text>
<View style={styles.taskMeta}>
<View style={[styles.categoryTag, {backgroundColor: theme.accent + '30'}]}>
<Text style={[styles.categoryText, {color: theme.accent}]}>{item.category}</Text>
</View>
<Text style={[styles.dueDate, {color: theme.subText}]}>📅 {item.dueDate}</Text>
</View>
{item.note ? <Text style={[styles.noteText, {color: theme.subText}]} numberOfLines={1}>💬 {item.note}</Text> : null}
</View>
你可能注意到,我们只给标题添加了划线效果,而没有给分类、日期、备注添加。这是有意为之的。
标题是任务的核心信息,划掉标题就足以表达"这个任务完成了"的含义。如果把所有文字都划掉,反而会显得杂乱,而且会影响这些辅助信息的可读性。
不过,透明度的降低是应用到整个卡片的吗?让我们看看:
<Animated.View style={[styles.taskCard, {backgroundColor: theme.card, borderColor: theme.border, opacity: itemAnim, ...}]}>
实际上,卡片的透明度是由入场动画控制的,不是由完成状态控制的。完成状态只影响标题的样式。这意味着分类标签、日期、备注在任务完成后仍然保持原来的清晰度。
如果你想让整个卡片在完成后都变淡,可以这样修改:
<Animated.View style={[
styles.taskCard,
{backgroundColor: theme.card, borderColor: theme.border},
{opacity: item.completed ? 0.6 : 1}
]}>
但这样做的话,勾选框和删除按钮也会变淡,可能影响操作。所以当前的设计——只让标题变淡——是一个平衡的选择。
样式数组的工作原理
React Native 的样式系统支持数组形式,这是实现条件样式的关键:
style={[styles.taskTitle, {color: theme.text}, item.completed && styles.completedText]}
当 React Native 处理这个样式数组时,它会:
- 从左到右遍历数组中的每个元素
- 忽略
null、undefined、false等假值 - 将所有有效的样式对象合并成一个
- 后面的属性会覆盖前面的同名属性
所以如果 item.completed 为 true,最终的样式是:
{
fontSize: 16,
fontWeight: '500',
marginBottom: 4,
color: '#ffffff', // 或 '#333333',取决于主题
textDecorationLine: 'line-through',
opacity: 0.5,
}
如果 item.completed 为 false,styles.completedText 不会被应用,最终样式是:
{
fontSize: 16,
fontWeight: '500',
marginBottom: 4,
color: '#ffffff', // 或 '#333333'
}
这种模式非常灵活,你可以根据任意条件组合任意数量的样式。
测试你的理解
在继续之前,试着回答这几个问题:
-
如果想让划线变成红色而不是继承文字颜色,应该怎么做?
-
如果想让已完成任务的透明度是 0.3 而不是 0.5,需要修改哪里?
-
如果想给分类标签也添加划线效果,代码应该怎么写?
答案在文章最后。
小结
划线效果看起来简单,但它背后涉及到:
- CSS 文字装饰属性在 React Native 中的应用
- 条件样式的实现技巧
- 视觉层次的设计理念
- 状态变化到视觉更新的完整链路
- 深色/浅色模式的适配
- 与其他 UI 元素的协调
一个小小的功能,却能体现出开发者对用户体验的关注。当用户划掉一个任务时,那一瞬间的满足感,就是我们努力的意义。
问题答案
-
在 React Native 中,
textDecorationLine的颜色继承自color属性,无法单独设置。如果需要不同颜色的划线,需要使用自定义 View 来模拟。 -
修改
completedText样式中的opacity值:
completedText: {textDecorationLine: 'line-through', opacity: 0.3},
- 给分类标签的 Text 添加条件样式:
<Text style={[styles.categoryText, {color: theme.accent}, item.completed && styles.completedText]}>{item.category}</Text>
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)