请添加图片描述

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

任务的两种状态

任务只有两种状态:做完了,或者还没做完。

这听起来很简单,但当任务列表变长时,你会发现自己经常想"只看看还有哪些没做的"或者"看看今天完成了多少"。状态筛选就是为这个需求而生的。

在我们的 TodoList 应用中,状态筛选是三个按钮:全部、待办、已完成。点击不同的按钮,列表就会显示对应状态的任务。


筛选类型的定义

首先我们需要定义筛选的类型:

type FilterType = 'all' | 'active' | 'completed';

为什么用联合类型

TypeScript 的联合类型限制了变量只能是这三个值之一。如果你不小心写成 setFilter('done'),编译器会立刻报错。这种类型安全在开发时能帮你避免很多低级错误。

三种筛选状态

  • all 表示显示所有任务,不做任何筛选
  • active 表示显示待办任务,也就是 completedfalse 的任务
  • completed 表示显示已完成任务,也就是 completedtrue 的任务

筛选状态的管理

筛选需要一个状态来记录当前选中的筛选条件:

const [filter, setFilter] = useState<FilterType>('all');

默认值的选择

默认值是 'all',也就是显示所有任务。这是最自然的选择,用户打开应用时应该能看到全部任务,而不是被筛选后的部分任务。

状态更新

当用户点击筛选按钮时,调用 setFilter 更新状态。状态变化会触发组件重新渲染,列表会根据新的筛选条件显示对应的任务。


筛选按钮组的实现

看看我们的筛选按钮是怎么实现的:

<View style={styles.filterContainer}>
  <View style={styles.filterGroup}>
    {(['all', 'active', 'completed'] as FilterType[]).map(f => (
      <TouchableOpacity 
        key={f} 
        style={[styles.filterBtn, filter === f && {backgroundColor: theme.accent}]} 
        onPress={() => setFilter(f)}>
        <Text style={[styles.filterBtnText, {color: filter === f ? '#fff' : theme.subText}]}>
          {f === 'all' ? '全部' : f === 'active' ? '待办' : '已完成'}
        </Text>
      </TouchableOpacity>
    ))}
  </View>
</View>

遍历生成按钮

我们用数组的 map 方法遍历三个筛选值,动态生成三个按钮。这比手写三个按钮更简洁,也更容易维护。如果以后要加一个新的筛选条件,只需要在数组里加一项就行。

as FilterType[] 的作用

as FilterType[] 是类型断言,告诉 TypeScript 这个数组的元素类型是 FilterType。没有这个断言,TypeScript 会把数组类型推断为 string[],后面使用 f 时就会报类型错误。

key 属性

key={f} 给每个按钮一个唯一的标识。React 用这个 key 来追踪列表项的变化,优化渲染性能。这里用筛选值本身作为 key,因为它们是唯一的。


按钮样式的动态切换

按钮有两种状态:选中和未选中。我们通过条件样式来区分:

style={[styles.filterBtn, filter === f && {backgroundColor: theme.accent}]}

样式数组

React Native 支持数组形式的样式,后面的样式会覆盖前面的。styles.filterBtn 是基础样式,filter === f && {backgroundColor: theme.accent} 是条件样式。

短路求值

filter === f && {backgroundColor: theme.accent} 使用了短路求值。如果 filter === ffalse,整个表达式返回 false,不应用任何额外样式。如果为 true,返回后面的样式对象,应用背景色。

选中状态的视觉反馈

选中的按钮背景色变成主题强调色 theme.accent,未选中的按钮保持默认的半透明背景。这种视觉差异让用户一眼就能看出当前选中的是哪个筛选条件。


文字颜色的切换

按钮文字的颜色也要根据选中状态变化:

<Text style={[styles.filterBtnText, {color: filter === f ? '#fff' : theme.subText}]}>

三元运算符

filter === f ? '#fff' : theme.subText 是三元运算符。如果当前按钮被选中,文字颜色是白色,与紫色背景形成对比。如果未选中,文字颜色是次要文字颜色,比较低调。

对比度的重要性

选中状态用白色文字配紫色背景,对比度很高,容易阅读。未选中状态用灰色文字配半透明背景,视觉上比较弱化,不会抢夺注意力。这种设计让用户的注意力自然集中在选中的按钮上。


文字标签的转换

筛选值是英文的,但显示给用户的应该是中文:

{f === 'all' ? '全部' : f === 'active' ? '待办' : '已完成'}

嵌套三元运算符

这是一个嵌套的三元运算符。先判断是不是 'all',如果是就显示"全部"。如果不是,再判断是不是 'active',如果是就显示"待办"。如果都不是,那就是 'completed',显示"已完成"。

另一种写法

嵌套三元运算符不太好读,可以用对象映射来代替:

const filterLabels: Record<FilterType, string> = {
  all: '全部',
  active: '待办',
  completed: '已完成',
};

// 使用
{filterLabels[f]}

这种写法更清晰,也更容易扩展。不过对于只有三个选项的情况,三元运算符也能接受。


筛选按钮的样式

filterContainer: {marginBottom: 8},
filterGroup: {flexDirection: 'row', gap: 8},
filterBtn: {paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, backgroundColor: 'rgba(255,255,255,0.1)'},
filterBtnText: {fontSize: 12},

filterContainer 容器样式

marginBottom: 8 是与下方内容的间距,让筛选按钮和任务列表之间有一点空隙。

filterGroup 按钮组样式

  • flexDirection: 'row' 让三个按钮水平排列
  • gap: 8 是按钮之间的间距,这是 React Native 较新版本支持的属性,比用 marginRight 更方便

filterBtn 按钮样式

  • paddingHorizontal: 12paddingVertical: 6 设置内边距,让按钮有合适的大小
  • borderRadius: 16 是大圆角,让按钮看起来像药丸形状
  • backgroundColor: 'rgba(255,255,255,0.1)' 是半透明白色背景,在深色主题下有微弱的可见度

filterBtnText 文字样式

fontSize: 12 是小字体,因为筛选按钮不需要太大,够看清就行。


筛选逻辑的实现

筛选的核心是 filter 方法:

const filteredTasks = tasks.filter(task => {
  const matchesSearch = task.title.toLowerCase().includes(searchText.toLowerCase());
  const matchesFilter = filter === 'all' || (filter === 'active' && !task.completed) || (filter === 'completed' && task.completed);
  const matchesPriority = priorityFilter === 'all' || task.priority === priorityFilter;
  const matchesCategory = categoryFilter === 'all' || task.category === categoryFilter;
  return matchesSearch && matchesFilter && matchesPriority && matchesCategory;
});

matchesFilter 的逻辑

状态筛选的逻辑在 matchesFilter 这一行:

const matchesFilter = filter === 'all' || (filter === 'active' && !task.completed) || (filter === 'completed' && task.completed);

这是一个或运算,满足任意一个条件就算匹配:

  • filter === 'all' 如果筛选条件是"全部",所有任务都匹配
  • filter === 'active' && !task.completed 如果筛选条件是"待办",只有未完成的任务匹配
  • filter === 'completed' && task.completed 如果筛选条件是"已完成",只有已完成的任务匹配

与其他筛选条件的组合

状态筛选不是孤立的,它和搜索、优先级筛选、分类筛选一起工作。四个条件用 && 连接,只有全部满足的任务才会显示。


筛选的即时响应

当用户点击筛选按钮时,列表会立即更新。这是怎么实现的?

数据流

  1. 用户点击"待办"按钮
  2. onPress 回调执行 setFilter('active')
  3. filter 状态从 'all' 变成 'active'
  4. 状态变化触发组件重新渲染
  5. filteredTasks 重新计算,只包含未完成的任务
  6. FlatList 接收到新的数据,显示筛选后的列表

整个过程是自动的,我们不需要手动触发列表更新。这就是 React 响应式编程的魅力。

性能考虑

每次状态变化都会重新计算 filteredTasks。对于几十条甚至几百条任务,这完全不是问题。JavaScriptfilter 方法很快,用户感觉不到任何延迟。


筛选结果为空的处理

如果筛选后没有匹配的任务怎么办?

ListEmptyComponent={
  <View style={styles.emptyContainer}>
    <Text style={styles.emptyIcon}>📝</Text>
    <Text style={[styles.emptyText, {color: theme.subText}]}>暂无任务</Text>
    <Text style={[styles.emptySubText, {color: theme.subText}]}>点击下方按钮添加新任务</Text>
  </View>
}

通用的空状态

FlatListListEmptyComponent 会在列表为空时显示。不管是因为没有任务,还是因为筛选后没有匹配的任务,都会显示这个组件。

更精确的提示

如果想根据筛选条件显示不同的提示,可以这样做:

ListEmptyComponent={
  <View style={styles.emptyContainer}>
    <Text style={styles.emptyIcon}>{filter === 'completed' ? '🎉' : '📝'}</Text>
    <Text style={[styles.emptyText, {color: theme.subText}]}>
      {filter === 'all' ? '暂无任务' : filter === 'active' ? '没有待办任务' : '没有已完成任务'}
    </Text>
  </View>
}

筛选"已完成"但没有结果时,显示庆祝图标和"没有已完成任务"的提示。筛选"待办"但没有结果时,显示"没有待办任务"。这种细节能让用户体验更好。


筛选按钮的交互细节

单选行为

三个按钮是互斥的,同一时间只能选中一个。这是通过状态管理实现的,filter 状态只能有一个值。

即时反馈

点击按钮后,按钮样式立即变化,列表立即更新。没有延迟,没有加载动画。这种即时反馈让用户感觉应用很"灵敏"。

触摸反馈

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


筛选状态的持久化

目前筛选状态是临时的,刷新页面或重启应用后会重置为"全部"。如果想保持用户的筛选偏好,可以用 AsyncStorage 持久化:

import AsyncStorage from '@react-native-async-storage/async-storage';

// 读取保存的筛选状态
useEffect(() => {
  AsyncStorage.getItem('filter').then(value => {
    if (value) setFilter(value as FilterType);
  });
}, []);

// 保存筛选状态
useEffect(() => {
  AsyncStorage.setItem('filter', filter);
}, [filter]);

是否需要持久化

这取决于产品需求。有些用户可能希望每次打开应用都看到全部任务,有些用户可能希望保持上次的筛选。可以在设置里提供一个选项让用户自己选择。


筛选按钮的可访问性

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

<TouchableOpacity 
  key={f} 
  style={[styles.filterBtn, filter === f && {backgroundColor: theme.accent}]} 
  onPress={() => setFilter(f)}
  accessibilityRole="button"
  accessibilityState={{selected: filter === f}}
  accessibilityLabel={f === 'all' ? '显示全部任务' : f === 'active' ? '显示待办任务' : '显示已完成任务'}
>

accessibilityRole

accessibilityRole="button" 告诉屏幕阅读器这是一个按钮。

accessibilityState

accessibilityState={{selected: filter === f}} 告诉屏幕阅读器当前按钮是否被选中。

accessibilityLabel

accessibilityLabel 是屏幕阅读器会朗读的标签,比显示的文字更详细,帮助视障用户理解按钮的作用。


筛选与统计的联动

筛选会影响显示的任务数量,但不会影响统计数据:

const totalTasks = tasks.length;
const completedTasks = tasks.filter(t => t.completed).length;

统计基于原始数据

统计数据是基于原始的 tasks 数组计算的,不受筛选影响。这样用户在筛选"待办"任务时,仍然能看到总任务数和已完成数。

为什么这样设计

如果统计也跟着筛选变化,用户筛选"待办"后会看到"已完成: 0",这会让人困惑。保持统计数据不变,用户能更好地了解整体情况。


小结

状态筛选是一个简单但实用的功能。通过三个按钮让用户快速切换查看全部任务、待办任务或已完成任务。

实现要点:

  • TypeScript 联合类型定义筛选值,保证类型安全
  • useState 管理筛选状态,默认显示全部
  • map 遍历生成按钮,代码更简洁
  • 用条件样式区分选中和未选中状态
  • filter 方法筛选任务列表,与其他筛选条件组合使用

好的筛选应该是"一目了然、一点即达"。用户看一眼就知道当前选中的是什么,点一下就能切换到想要的筛选条件。


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

Logo

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

更多推荐