RN for OpenHarmony 实战 TodoList 项目:优先级选择器
本文介绍了TodoList应用中优先级选择器的设计与实现。选择器采用红黄绿三色按钮直观表示高、中、低三个优先级,利用用户已有的交通灯认知降低学习成本。文章详细讲解了按钮的两种状态(选中/未选中)的样式处理、动态样式的实现方法、文字颜色切换逻辑,以及使用TypeScript类型约束确保代码安全性。此外还说明了状态管理策略和默认值选择考量,并对比了任务添加和筛选功能中两种优先级选择器的异同。整个设计注
案例开源地址: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>
与添加选择器的区别
- 多了一个"全部"选项
- 样式不同(药丸形状 vs 圆角矩形)
- 未选中时没有彩色边框
"全部"选项的处理
{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
更多推荐

所有评论(0)