请添加图片描述

案例开源地址:https://gitcode.com/lqjmac/rn_openharmony_todolist

勾选框的设计哲学

勾选框是 TodoList 应用的灵魂。

它不仅仅是一个功能组件,更是用户与任务交互的核心触点。每一次点击,都是一次"完成"的仪式感。一个设计精良的勾选框,能让这个简单的动作变得愉悦。

在我们的应用中,勾选框采用了圆形设计,而不是传统的方形。圆形更柔和、更现代,与整体的圆角设计风格保持一致。


勾选框的结构

先看勾选框的完整代码:

<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>

结构很简单,三层嵌套:

  1. TouchableOpacity - 可点击的容器,处理触摸事件
  2. View (checkboxInner) - 圆形的视觉容器
  3. Text (checkmark) - 勾选符号,只在完成时显示

外层:触摸容器

<TouchableOpacity onPress={() => toggleTask(item.id)} style={styles.checkbox}>

TouchableOpacity 是 React Native 提供的触摸组件。当用户按下时,它会降低子元素的透明度,提供视觉反馈。

点击事件

onPress={() => toggleTask(item.id)}

点击时调用 toggleTask 函数,传入任务 ID。这个函数会切换任务的完成状态:

const toggleTask = (id: string) => {
  setTasks(tasks.map(task => task.id === id ? {...task, completed: !task.completed} : task));
};

样式

checkbox: {marginRight: 12},

只设置了右边距,让勾选框与任务标题保持距离。

为什么不设置宽高?因为 TouchableOpacity 会自动包裹子元素的大小。实际的触摸区域由内部的 checkboxInner 决定。


中层:圆形容器

<View style={[styles.checkboxInner, {borderColor: theme.accent}, item.completed && {backgroundColor: theme.accent}]}>

这是勾选框的视觉主体。

基础样式

checkboxInner: {width: 24, height: 24, borderRadius: 12, borderWidth: 2, justifyContent: 'center', alignItems: 'center'},
  • width: 24, height: 24 - 24x24 像素的正方形
  • borderRadius: 12 - 半径等于宽度的一半,形成圆形
  • borderWidth: 2 - 2 像素的边框
  • justifyContent: 'center', alignItems: 'center' - 内容居中(为了让勾选符号居中)

动态边框颜色

{borderColor: theme.accent}

边框颜色使用主题的强调色 #6c5ce7(紫色)。无论任务是否完成,边框颜色都是一样的。

完成状态的背景色

item.completed && {backgroundColor: theme.accent}

当任务完成时,背景色变成强调色。这是一个条件样式——只有 item.completed 为 true 时才应用。

未完成状态:紫色边框 + 透明背景
完成状态:紫色边框 + 紫色背景


内层:勾选符号

{item.completed && <Text style={styles.checkmark}>✓</Text>}

勾选符号只在任务完成时显示。

条件渲染

{item.completed && <Text>...</Text>}

这是 React 的条件渲染语法。当 item.completed 为 false 时,整个表达式返回 false,不渲染任何内容。当 item.completed 为 true 时,渲染后面的 <Text> 组件。

勾选符号

使用 Unicode 字符 ✓(U+2713)作为勾选符号。也可以用其他符号,比如 ✔(U+2714)或 ☑(U+2611)。

样式

checkmark: {color: '#fff', fontSize: 14, fontWeight: 'bold'},
  • color: '#fff' - 白色,与紫色背景形成对比
  • fontSize: 14 - 字体大小,比容器小一些,留出边距
  • fontWeight: 'bold' - 粗体,让符号更醒目

两种状态的视觉对比

未完成状态

┌─────────┐
│  ○      │  紫色圆环,透明内部
└─────────┘
  • 边框:紫色(#6c5ce7)
  • 背景:透明
  • 内容:无

完成状态

┌─────────┐
│  ●✓     │  紫色实心圆,白色勾选
└─────────┘
  • 边框:紫色(#6c5ce7)
  • 背景:紫色(#6c5ce7)
  • 内容:白色勾选符号

这种设计的好处是状态变化非常明显。从空心到实心,从无到有,用户一眼就能看出任务是否完成。


为什么用圆形而不是方形?

传统的勾选框是方形的,但我们选择了圆形。原因有几个:

1. 与整体设计风格一致

应用中大量使用了圆角设计:卡片圆角、按钮圆角、标签圆角。圆形勾选框与这些元素更协调。

2. 更现代的视觉感受

圆形给人柔和、友好的感觉。方形则更正式、严肃。对于一个日常使用的待办应用,圆形更合适。

3. 触摸更自然

圆形的触摸区域更符合手指的形状。虽然实际的触摸区域是矩形的(TouchableOpacity 的边界),但视觉上的圆形让用户更愿意去点击。

4. 差异化

大多数待办应用使用方形勾选框。圆形是一个小小的差异化,让应用更有辨识度。


尺寸的选择

勾选框的尺寸是 24x24 像素。这个数字是怎么来的?

最小触摸目标

Apple 的人机界面指南建议,触摸目标至少应该是 44x44 点。Google 的 Material Design 建议至少 48x48 dp。

24 像素看起来小于这个标准,但实际上 TouchableOpacity 的触摸区域会比视觉区域大一些。而且勾选框旁边有足够的空白,用户不太容易误触。

视觉平衡

24 像素与 16 像素的任务标题字体大小比较协调。如果勾选框太大,会抢走标题的注意力;太小则不够醒目。

边框宽度

2 像素的边框在 24 像素的圆形上看起来刚好。1 像素太细,3 像素太粗。


颜色的选择

勾选框使用主题的强调色 #6c5ce7(紫色)。

为什么用强调色?

勾选框是用户交互的核心元素,应该足够醒目。使用强调色可以让它从其他元素中脱颖而出。

为什么不用绿色?

绿色通常表示"成功"或"完成",用在完成状态的勾选框上似乎很合理。但我们选择了紫色,原因是:

  1. 紫色是应用的主题色,保持一致性
  2. 绿色已经用于低优先级标签,避免颜色冲突
  3. 紫色更有个性,不那么"俗套"

白色勾选符号

白色与紫色背景有足够的对比度,确保可读性。如果背景是浅色,勾选符号应该用深色。


状态切换的实现

点击勾选框时,任务的完成状态会切换:

const toggleTask = (id: string) => {
  setTasks(tasks.map(task => task.id === id ? {...task, completed: !task.completed} : task));
};

这行代码做了什么?

1. 遍历所有任务

tasks.map(task => ...)

map 方法遍历数组,对每个元素执行回调函数,返回一个新数组。

2. 找到目标任务

task.id === id ? ... : task

如果任务 ID 匹配,执行状态切换;否则返回原任务(不变)。

3. 切换完成状态

{...task, completed: !task.completed}

使用展开运算符创建任务的副本,然后覆盖 completed 属性。!task.completed 取反,true 变 false,false 变 true。

4. 更新状态

setTasks(...)

用新数组更新状态,触发重新渲染。

这种不可变更新的方式是 React 的最佳实践。不直接修改原数组,而是创建新数组,让 React 能正确检测到变化。


触摸反馈

TouchableOpacity 提供了默认的触摸反馈——按下时透明度降低。

可以通过 activeOpacity 属性调整:

<TouchableOpacity activeOpacity={0.7} onPress={...}>

默认值是 0.2,表示按下时透明度变为 20%。0.7 表示变为 70%,反馈更subtle。

也可以使用其他触摸组件:

  • TouchableHighlight - 按下时显示底色
  • TouchableWithoutFeedback - 无视觉反馈
  • Pressable - 更灵活的触摸组件(React Native 0.63+)

可访问性

为了让勾选框对所有用户都友好,可以添加可访问性属性:

<TouchableOpacity 
  onPress={() => toggleTask(item.id)} 
  style={styles.checkbox}
  accessibilityRole="checkbox"
  accessibilityState={{checked: item.completed}}
  accessibilityLabel={`任务:${item.title}`}
  accessibilityHint={item.completed ? '点击标记为未完成' : '点击标记为已完成'}
>
  • accessibilityRole="checkbox" - 告诉屏幕阅读器这是一个复选框
  • accessibilityState={{checked: item.completed}} - 告诉屏幕阅读器当前状态
  • accessibilityLabel - 屏幕阅读器会朗读的标签
  • accessibilityHint - 额外的提示信息

动画增强

当前的实现没有状态切换动画。如果要添加,可以使用 Animated API:

const scaleAnim = useRef(new Animated.Value(1)).current;

const handlePress = () => {
  Animated.sequence([
    Animated.timing(scaleAnim, {toValue: 0.8, duration: 100, useNativeDriver: true}),
    Animated.timing(scaleAnim, {toValue: 1, duration: 100, useNativeDriver: true}),
  ]).start();
  toggleTask(item.id);
};

<Animated.View style={[styles.checkboxInner, {transform: [{scale: scaleAnim}]}]}>

这会在点击时产生一个"弹跳"效果,让交互更有趣。


其他勾选框设计方案

除了当前的设计,还有很多其他方案:

方形勾选框

checkboxInner: {width: 20, height: 20, borderRadius: 4, borderWidth: 2},

borderRadius 改小,就变成圆角方形。

图标勾选框

使用图标库(如 react-native-vector-icons)的勾选图标,而不是文字符号。

滑动开关

Switch 组件代替勾选框,适合某些场景。

自定义图形

使用 SVG 或自定义绘制,实现更复杂的勾选效果。

每种方案都有其适用场景,关键是要与整体设计风格保持一致。


小结

一个小小的勾选框,包含了很多设计和实现的细节:

  • 三层嵌套结构:触摸容器 → 视觉容器 → 勾选符号
  • 圆形设计,与整体风格一致
  • 两种状态的明显视觉差异
  • 合适的尺寸和颜色
  • 不可变的状态更新
  • 触摸反馈和可访问性

勾选框虽小,但它是用户与应用交互最频繁的元素。把它做好,能显著提升用户体验。


勾选框与任务状态的联动

勾选框不是孤立存在的,它与任务的其他视觉元素有联动:

标题的删除线

当任务完成时,标题会显示删除线:

<Text style={[styles.taskTitle, {color: theme.text}, item.completed && styles.completedText]}>{item.title}</Text>
completedText: {textDecorationLine: 'line-through', opacity: 0.5},

勾选框填充 + 标题删除线,双重视觉反馈,让完成状态更明显。

统计数据更新

勾选框的状态变化会影响统计数据:

const completedTasks = tasks.filter(t => t.completed).length;
const progress = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;

每次切换任务状态,已完成数量和进度百分比都会重新计算。

筛选结果变化

如果当前筛选条件是"待办"或"已完成",切换任务状态可能会让任务从列表中消失或出现。

这些联动让应用感觉是一个整体,而不是孤立的功能堆砌。


勾选框的边界情况

在实际使用中,需要考虑一些边界情况:

快速连续点击

用户可能快速连续点击勾选框。当前的实现没有防抖,每次点击都会触发状态更新。

如果需要防抖,可以这样:

const [isToggling, setIsToggling] = useState(false);

const handleToggle = async () => {
  if (isToggling) return;
  setIsToggling(true);
  toggleTask(item.id);
  setTimeout(() => setIsToggling(false), 300);
};

网络同步

如果任务数据需要同步到服务器,点击勾选框后应该:

  1. 立即更新本地状态(乐观更新)
  2. 发送请求到服务器
  3. 如果请求失败,回滚本地状态

离线支持

如果应用需要离线支持,状态变化应该先保存到本地存储,等网络恢复后再同步。


勾选框的测试

如果要为勾选框写测试,需要覆盖这些场景:

渲染测试

// 未完成任务应该显示空心勾选框
expect(screen.queryByText('✓')).toBeNull();

// 已完成任务应该显示实心勾选框和勾选符号
expect(screen.getByText('✓')).toBeTruthy();

交互测试

// 点击勾选框应该切换任务状态
fireEvent.press(checkbox);
expect(toggleTask).toHaveBeenCalledWith(taskId);

样式测试

// 已完成任务的勾选框应该有背景色
expect(checkboxInner).toHaveStyle({backgroundColor: theme.accent});

从勾选框看组件设计

勾选框是一个很好的组件设计案例。它展示了几个重要的设计原则:

单一职责

勾选框只负责显示和切换完成状态,不处理其他逻辑。

可组合性

勾选框可以放在任何需要的地方,不依赖特定的父组件。

可定制性

通过 props 可以定制颜色、大小等属性(虽然当前实现是硬编码的)。

状态外置

勾选框不管理自己的状态,状态由父组件传入。这让状态管理更清晰。

如果要把勾选框抽取为独立组件,可以这样设计:

interface CheckboxProps {
  checked: boolean;
  onToggle: () => void;
  size?: number;
  color?: string;
}

const Checkbox: React.FC<CheckboxProps> = ({
  checked,
  onToggle,
  size = 24,
  color = '#6c5ce7',
}) => {
  return (
    <TouchableOpacity onPress={onToggle}>
      <View style={[
        styles.checkboxInner,
        {
          width: size,
          height: size,
          borderRadius: size / 2,
          borderColor: color,
          backgroundColor: checked ? color : 'transparent',
        },
      ]}>
        {checked && <Text style={styles.checkmark}>✓</Text>}
      </View>
    </TouchableOpacity>
  );
};

这样的组件更通用,可以在不同场景复用。


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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐