请添加图片描述

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

选择的艺术

“这个任务重要吗?”

这是一个简单的问题,但答案往往不简单。重要性是相对的,取决于上下文、时间、个人判断。

在我们的 TodoList 应用中,我们把这个复杂的问题简化为三个选项:高、中、低。用户不需要深思熟虑,只需要快速做出一个大致的判断。

优先级选择器就是这个判断的入口。它需要做到两点:让用户快速理解选项的含义,让用户快速做出选择。


选择器的设计

我们的优先级选择器是三个并排的按钮:

<View style={styles.prioritySelector}>
  {(['high', 'medium', 'low'] as const).map(p => (
    <TouchableOpacity 
      key={p} 
      style={[styles.priorityOption, {borderColor: priorityColors[p]}, newTaskPriority === p && {backgroundColor: priorityColors[p]}]} 
      onPress={() => setNewTaskPriority(p)}>
      <Text style={{color: newTaskPriority === p ? '#fff' : priorityColors[p]}}>
        {p === 'high' ? '高' : p === 'medium' ? '中' : '低'}
      </Text>
    </TouchableOpacity>
  ))}
</View>

三个按钮,三种颜色,三个状态。简单直接。


颜色的语义

优先级的颜色不是随意选择的:

const priorityColors = {
  high: '#ff6b6b',    // 红色
  medium: '#ffd93d',  // 黄色
  low: '#6bcb77',     // 绿色
};

这是交通灯的颜色系统:

  • 红色 = 停!这很紧急
  • 黄色 = 注意,需要关注
  • 绿色 = 可以慢慢来

这种颜色映射是全球通用的,用户不需要学习就能理解。

为什么不用其他颜色?

比如用蓝色表示高优先级?

可以,但不直观。蓝色没有"紧急"的语义。用户需要额外的认知负担来记住"蓝色 = 高优先级"。

红黄绿的组合利用了用户已有的认知,降低了学习成本。


按钮的两种状态

每个按钮有两种状态:选中和未选中。

未选中状态

style={[styles.priorityOption, {borderColor: priorityColors[p]}]}
  • 有彩色边框
  • 透明背景
  • 彩色文字

选中状态

style={[styles.priorityOption, {borderColor: priorityColors[p]}, newTaskPriority === p && {backgroundColor: priorityColors[p]}]}
  • 有彩色边框
  • 彩色背景
  • 白色文字

选中状态通过填充背景色来表示,视觉上更"实",表示这是当前的选择。


样式的拆解

容器样式

prioritySelector: {flexDirection: 'row', gap: 12, marginBottom: 16},
  • flexDirection: 'row' - 三个按钮水平排列
  • gap: 12 - 按钮之间的间距
  • marginBottom: 16 - 与下方内容的间距

按钮样式

priorityOption: {flex: 1, paddingVertical: 10, borderRadius: 8, borderWidth: 2, alignItems: 'center'},
  • flex: 1 - 三个按钮平分可用空间,宽度相等
  • paddingVertical: 10 - 上下内边距,让按钮有足够的高度
  • borderRadius: 8 - 圆角
  • borderWidth: 2 - 边框宽度,比较粗以便显示颜色
  • alignItems: 'center' - 文字水平居中

为什么边框是 2 像素?

1 像素的边框在某些屏幕上可能显得太细,颜色不够明显。2 像素更稳妥,确保颜色能被清楚地看到。


动态样式的实现

React Native 支持数组形式的样式,后面的样式会覆盖前面的:

style={[
  styles.priorityOption,                    // 基础样式
  {borderColor: priorityColors[p]},         // 边框颜色
  newTaskPriority === p && {backgroundColor: priorityColors[p]}  // 选中时的背景色
]}

第三个元素使用了短路求值:

  • 如果 newTaskPriority === p 为 false,整个表达式返回 false,不应用任何样式
  • 如果为 true,返回 {backgroundColor: priorityColors[p]},应用背景色

这是 React 中常用的条件样式技巧。


文字颜色的切换

<Text style={{color: newTaskPriority === p ? '#fff' : priorityColors[p]}}>

三元运算符根据选中状态切换颜色:

  • 选中时:白色(#fff),与彩色背景形成对比
  • 未选中时:对应的优先级颜色,与透明背景形成对比

这确保了在任何状态下文字都是可读的。


文字标签的转换

{p === 'high' ? '高' : p === 'medium' ? '中' : '低'}

把英文的优先级值转换成中文标签。嵌套的三元运算符,虽然不太优雅,但对于只有三个选项的情况是可以接受的。

更清晰的写法是使用映射对象:

const priorityLabels: Record<string, string> = {
  high: '高',
  medium: '中',
  low: '低',
};

// 使用
{priorityLabels[p]}

这样更易读,也更容易扩展。


状态管理

选中的优先级存储在 newTaskPriority 状态中:

const [newTaskPriority, setNewTaskPriority] = useState<'high' | 'medium' | 'low'>('medium');

类型定义

'high' | 'medium' | 'low' 是 TypeScript 的联合类型,限制了状态只能是这三个值之一。如果尝试设置其他值,TypeScript 会报错。

默认值

默认是 'medium'(中等优先级)。这是一个中性的选择:

  • 如果默认是 'high',用户可能懒得修改,导致所有任务都是高优先级
  • 如果默认是 'low',用户可能觉得应用不重视他们的任务
  • 'medium' 是一个安全的中间值

更新状态

onPress={() => setNewTaskPriority(p)}

点击按钮时,把对应的优先级值设置到状态中。状态更新触发重新渲染,按钮的样式随之改变。


遍历生成按钮

{(['high', 'medium', 'low'] as const).map(p => (
  <TouchableOpacity key={p} ...>
    ...
  </TouchableOpacity>
))}

使用数组的 map 方法动态生成三个按钮,而不是手写三个。

为什么用 as const

['high', 'medium', 'low'] as const

as const 是 TypeScript 的类型断言,它告诉编译器这个数组是只读的,元素类型是字面量类型 'high' | 'medium' | 'low',而不是宽泛的 string

没有 as const

const arr = ['high', 'medium', 'low'];  // 类型是 string[]

as const

const arr = ['high', 'medium', 'low'] as const;  // 类型是 readonly ['high', 'medium', 'low']

这样在后面使用 p 时,TypeScript 知道它是 'high' | 'medium' | 'low' 之一,可以安全地用作 priorityColors 的键。

key 属性

key={p}

React 要求列表中的每个元素都有唯一的 key。这里用优先级值本身作为 key,因为它们是唯一的。


筛选中的优先级选择器

除了添加任务时的选择器,筛选功能也有一个类似的优先级选择器:

<View style={styles.filterContainer}>
  <View style={styles.filterGroup}>
    {(['all', 'high', 'medium', 'low'] as PriorityFilter[]).map(p => (
      <TouchableOpacity 
        key={p} 
        style={[styles.filterBtn, priorityFilter === p && {backgroundColor: p === 'all' ? theme.accent : priorityColors[p as keyof typeof priorityColors]}]} 
        onPress={() => setPriorityFilter(p)}>
        <Text style={[styles.filterBtnText, {color: priorityFilter === p ? '#fff' : theme.subText}]}>
          {p === 'all' ? '全部' : p === 'high' ? '高' : p === 'medium' ? '中' : '低'}
        </Text>
      </TouchableOpacity>
    ))}
  </View>
</View>

与添加选择器的区别

  1. 多了一个"全部"选项
  2. 样式不同(药丸形状 vs 圆角矩形)
  3. 未选中时没有彩色边框

"全部"选项的处理

{backgroundColor: p === 'all' ? theme.accent : priorityColors[p as keyof typeof priorityColors]}

"全部"选项使用主题强调色,而不是优先级颜色。


筛选按钮的样式

filterBtn: {paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, backgroundColor: 'rgba(255,255,255,0.1)'},
filterBtnText: {fontSize: 12},
  • borderRadius: 16 - 大圆角,形成药丸形状
  • backgroundColor: 'rgba(255,255,255,0.1)' - 半透明背景
  • fontSize: 12 - 小字体

筛选按钮比添加选择器更紧凑,因为它们需要在有限的空间内显示更多选项。


优先级与筛选逻辑

选择优先级筛选后,列表只显示对应优先级的任务:

const filteredTasks = tasks.filter(task => {
  // ... 其他筛选条件
  const matchesPriority = priorityFilter === 'all' || task.priority === priorityFilter;
  return matchesSearch && matchesFilter && matchesPriority && matchesCategory;
});

如果 priorityFilter'all',所有任务都匹配。否则,只有优先级相等的任务才匹配。


选择器的可访问性

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

<TouchableOpacity
  accessibilityRole="radio"
  accessibilityState={{selected: newTaskPriority === p}}
  accessibilityLabel={`${p === 'high' ? '高' : p === 'medium' ? '中' : '低'}优先级`}
>
  • accessibilityRole="radio" - 告诉屏幕阅读器这是一个单选按钮
  • accessibilityState={{selected: ...}} - 告诉屏幕阅读器当前是否选中
  • accessibilityLabel - 屏幕阅读器会朗读的标签

选择器的扩展

如果要支持更多优先级(比如"紧急"、“高”、“中”、“低”、“无”),需要修改几个地方:

1. 颜色定义

const priorityColors = {
  urgent: '#e74c3c',
  high: '#ff6b6b',
  medium: '#ffd93d',
  low: '#6bcb77',
  none: '#95a5a6',
};

2. 类型定义

type Priority = 'urgent' | 'high' | 'medium' | 'low' | 'none';

3. 选择器数组

{(['urgent', 'high', 'medium', 'low', 'none'] as const).map(p => ...)}

4. 标签映射

const priorityLabels = {
  urgent: '紧急',
  high: '高',
  medium: '中',
  low: '低',
  none: '无',
};

因为我们使用了数据驱动的方式生成按钮,扩展起来比较容易。


选择器的其他形式

除了按钮组,优先级选择器还可以有其他形式:

下拉选择

使用 Picker 组件,适合选项很多的情况。

滑块

使用 Slider 组件,适合连续的优先级(1-10)。

星级评分

显示 1-5 颗星,用户点击选择。

颜色选择

直接显示颜色块,用户点击选择颜色。

每种形式都有其适用场景。对于只有三个选项的情况,按钮组是最直观的选择。


小结

优先级选择器是一个看似简单但细节丰富的组件:

  • 三个按钮,三种颜色,语义清晰
  • 选中和未选中状态有明显的视觉差异
  • 使用数据驱动的方式生成,易于扩展
  • 与筛选功能联动,形成完整的优先级系统

好的选择器应该让用户"不假思索"就能做出选择。颜色的语义、按钮的布局、状态的反馈,都是为了降低用户的认知负担。

当用户看到红黄绿三个按钮时,他们不需要阅读文字就知道该选哪个。这就是设计的力量。


优先级选择器的交互细节

即时反馈

点击按钮后,状态立即更新,视觉立即变化。没有延迟,没有加载,用户能感受到操作的即时性。

单选行为

三个按钮是互斥的,选中一个会自动取消其他的选中状态。这是通过状态管理实现的——只有一个状态变量 newTaskPriority,它只能有一个值。

默认选中

打开弹窗时,中等优先级默认选中。用户可以直接点击"添加"而不修改优先级,这对于大多数任务来说是合理的默认值。

触摸反馈

TouchableOpacity 在按下时会降低透明度,给用户视觉反馈。这个反馈很subtle,但能让用户确认"我点到了"。


优先级在整个应用中的流转

优先级不仅仅在选择器中使用,它贯穿整个应用:

1. 添加任务时选择

用户在弹窗中选择优先级,保存到任务对象中。

2. 任务卡片中显示

通过左侧的彩色条显示优先级。

3. 筛选时使用

用户可以按优先级筛选任务。

4. 统计中展示

统计页面显示各优先级的任务数量。

这种一致性让优先级成为一个有意义的属性,而不是一个被设置后就遗忘的字段。


从选择器看组件设计原则

优先级选择器体现了几个重要的组件设计原则:

1. 单一职责

选择器只负责让用户选择优先级,不处理其他逻辑。

2. 受控组件

状态由父组件管理,选择器只负责显示和触发更新。

3. 数据驱动

按钮通过数组遍历生成,而不是硬编码。这让组件更灵活,更易于维护。

4. 视觉一致性

颜色、间距、圆角等视觉元素与应用的其他部分保持一致。

这些原则不仅适用于优先级选择器,也适用于任何 UI 组件的设计。


测试优先级选择器

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

渲染测试

// 应该渲染三个优先级按钮
expect(screen.getByText('高')).toBeTruthy();
expect(screen.getByText('中')).toBeTruthy();
expect(screen.getByText('低')).toBeTruthy();

默认状态测试

// 默认应该选中"中"
const mediumButton = screen.getByText('中').parent;
expect(mediumButton).toHaveStyle({backgroundColor: '#ffd93d'});

交互测试

// 点击"高"后应该选中"高"
fireEvent.press(screen.getByText('高'));
expect(setNewTaskPriority).toHaveBeenCalledWith('high');

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

Logo

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

更多推荐