RN for OpenHarmony 实战 TodoList 项目:空状态占位图

#

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

空白不是错误

打开一个新安装的 App,列表是空的。这时候用户看到什么?

如果只是一片空白,用户会困惑:是加载失败了?是我操作错了?还是本来就没有内容?

空状态占位图就是为了解决这个问题。它告诉用户"这里确实没有内容,但这不是错误,你可以这样做来添加内容"。

一个好的空状态设计,能把"什么都没有"的尴尬变成"一切从这里开始"的期待。


FlatList 的空状态支持

React NativeFlatList 组件内置了空状态支持,通过 ListEmptyComponent 属性:

tsx 复制代码
<FlatList 
  data={filteredTasks} 
  keyExtractor={item => item.id} 
  renderItem={({item, index}) => <TaskItem item={item} index={index} />} 
  contentContainerStyle={styles.listContent} 
  showsVerticalScrollIndicator={false}
  refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.accent} />}
  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>
  }
/>

data 数组为空时,FlatList 不会渲染任何列表项,而是显示 ListEmptyComponent 指定的组件。

这个设计很贴心。你不用自己判断数组是否为空,不用写 {data.length === 0 ? <Empty /> : <List />} 这样的条件渲染。FlatList 帮你处理了。


空状态的结构

我们的空状态有三个元素:

tsx 复制代码
<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>

图标

一个大大的 📝 emoji,代表任务/笔记。64 像素的字号,很醒目。

为什么用 emoji 而不是插图?因为简单。一个 emoji 就能传达意思,不需要设计师画图,不需要引入图片资源。对于演示项目来说够用了。

真实产品里,可能会用精心设计的插图。一个可爱的小人物举着空白的纸,或者一个打开的空盒子。这种插图更有品牌感,但也需要更多设计资源。

主文案

"暂无任务",简单直接。告诉用户当前状态是什么。

有些应用会用更有趣的文案,比如"这里空空如也"、"还没有任务哦"、"任务列表正在等待你的第一个任务"。这些文案更有个性,但也更主观。我们选择中性的表达。

副文案

"点击下方按钮添加新任务",引导用户下一步操作。

这句话很重要。光说"暂无任务",用户可能不知道怎么添加。告诉他"点击下方按钮",他就知道该往哪里看了。

引导要具体。"添加新任务"比"开始使用"更明确。用户不用猜,直接照做就行。


空状态的样式

tsx 复制代码
emptyContainer: {alignItems: 'center', paddingVertical: 60},
emptyIcon: {fontSize: 64, marginBottom: 16},
emptyText: {fontSize: 18, fontWeight: '600'},
emptySubText: {fontSize: 14, marginTop: 8},

居中显示

alignItems: 'center' 让所有内容水平居中。空状态应该在页面中央,而不是靠左或靠右。居中显示更有"这是一个特殊状态"的感觉。

垂直间距

paddingVertical: 60 让空状态和页面顶部、底部都有距离。不会贴着边缘,看起来更舒服。

60 像素是一个经验值。太小会显得拥挤,太大会让空状态显得孤零零的。可以根据实际效果调整。

图标大小

fontSize: 64 让 emoji 很大。空状态的图标应该醒目,让用户第一眼就看到。

marginBottom: 16 是图标和文字之间的间距。

文字层次

主文案 fontSize: 18fontWeight: '600',比较醒目。

副文案 fontSize: 14,比主文案小,是辅助信息。

marginTop: 8 让两行文字之间有一点间距,不会挤在一起。


空状态的颜色

tsx 复制代码
<Text style={[styles.emptyText, {color: theme.subText}]}>暂无任务</Text>
<Text style={[styles.emptySubText, {color: theme.subText}]}>点击下方按钮添加新任务</Text>

文字用 theme.subText,是次要文字颜色,比正文浅。

为什么不用正文颜色?因为空状态是一个"次要"的状态。它不是页面的主要内容,只是在没有内容时的占位。用浅一点的颜色,视觉上不会太抢眼。

图标没有设置颜色,用 emoji 的默认颜色。如果用图标库,可以设置成主题强调色或者次要颜色。


什么时候显示空状态

空状态在 filteredTasks 为空时显示。注意是 filteredTasks,不是 tasks

tsx 复制代码
<FlatList data={filteredTasks} ... />

这意味着两种情况会显示空状态:

真的没有任务

用户刚开始使用,还没添加任何任务。tasks 是空数组,filteredTasks 自然也是空的。

筛选后没有结果

用户有任务,但当前的筛选条件没有匹配的任务。比如筛选"高优先级",但所有任务都是中低优先级。

这两种情况显示同样的空状态,可能不太合适。第一种情况应该引导用户添加任务,第二种情况应该提示用户调整筛选条件。


区分不同的空状态

可以根据情况显示不同的空状态:

tsx 复制代码
const getEmptyComponent = () => {
  // 有筛选条件但没有结果
  if (tasks.length > 0 && filteredTasks.length === 0) {
    return (
      <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>
    );
  }
  
  // 真的没有任务
  return (
    <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>
  );
};

// 使用
<FlatList ... ListEmptyComponent={getEmptyComponent()} />

筛选无结果时显示放大镜图标和"没有匹配的任务",真的没有任务时显示笔记图标和"暂无任务"。

这种细分让空状态更有针对性,用户体验更好。我们的演示项目没有做这个区分,保持简单。


空状态里加按钮

副文案说"点击下方按钮添加新任务",但用户还要去找那个按钮。能不能直接在空状态里放一个按钮?

tsx 复制代码
<View style={styles.emptyContainer}>
  <Text style={styles.emptyIcon}>📝</Text>
  <Text style={[styles.emptyText, {color: theme.subText}]}>暂无任务</Text>
  <TouchableOpacity 
    style={{backgroundColor: theme.accent, paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8, marginTop: 20}}
    onPress={() => setShowAddModal(true)}
  >
    <Text style={{color: '#fff', fontSize: 16, fontWeight: '600'}}>添加第一个任务</Text>
  </TouchableOpacity>
</View>

直接点击按钮就能打开添加任务的弹窗,比"点击下方按钮"更直接。

这种设计的好处是减少用户的操作步骤。坏处是空状态变得更复杂,而且和浮动添加按钮功能重复。

我们选择不加按钮,因为浮动添加按钮已经很明显了。但如果你的应用没有浮动按钮,在空状态里加按钮是个好选择。


空状态的动画

可以给空状态加一些动画,让它更生动:

tsx 复制代码
const emptyAnim = useRef(new Animated.Value(0)).current;

useEffect(() => {
  if (filteredTasks.length === 0) {
    Animated.spring(emptyAnim, {
      toValue: 1,
      tension: 50,
      friction: 8,
      useNativeDriver: true,
    }).start();
  }
}, [filteredTasks.length]);

// 在空状态组件里
<Animated.View style={[styles.emptyContainer, {
  opacity: emptyAnim,
  transform: [{scale: emptyAnim}]
}]}>
  ...
</Animated.View>

空状态出现时有一个弹性放大的效果,比突然出现更柔和。

不过要注意,如果用户快速切换筛选条件,空状态可能频繁出现和消失,动画会显得很乱。简单的淡入效果可能比弹性动画更稳妥。


空状态和下拉刷新

即使列表为空,下拉刷新仍然可用:

tsx 复制代码
<FlatList 
  ...
  refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.accent} />}
  ListEmptyComponent={...}
/>

用户可以下拉刷新来检查是否有新数据。这在数据来自服务器的应用里很有用。

我们的应用是本地数据,下拉刷新只是一个视觉效果。但保留这个功能让应用感觉更完整。


空状态的位置

ListEmptyComponent 会显示在 FlatList 的内容区域。如果 FlatListListHeaderComponent,空状态会显示在 header 下面。

tsx 复制代码
<FlatList 
  ListHeaderComponent={<Header />}
  ListEmptyComponent={<Empty />}
  ...
/>

这意味着即使列表为空,header 仍然会显示。这通常是期望的行为,比如搜索框应该一直显示,不管有没有搜索结果。

我们的应用没有用 ListHeaderComponent,搜索框和筛选按钮是在 FlatList 外面的。所以空状态会占据整个列表区域。


空状态的高度

空状态应该有多高?

如果太矮,会显得内容很少,页面很空。如果太高,可能会超出屏幕,用户看不到引导文字。

我们用 paddingVertical: 60,让空状态有一定的高度但不会太高。在大多数设备上,空状态会显示在屏幕中央偏上的位置,用户一眼就能看到。

如果想让空状态垂直居中,可以给容器设置 flex: 1justifyContent: 'center'。但这需要知道容器的高度,在 FlatList 里不太好处理。


空状态的可访问性

tsx 复制代码
<View style={styles.emptyContainer} accessibilityLabel="任务列表为空,点击下方按钮添加新任务">
  <Text style={styles.emptyIcon} accessibilityElementsHidden={true}>📝</Text>
  <Text style={[styles.emptyText, {color: theme.subText}]}>暂无任务</Text>
  <Text style={[styles.emptySubText, {color: theme.subText}]}>点击下方按钮添加新任务</Text>
</View>

给容器加 accessibilityLabel,屏幕阅读器会朗读完整的提示。

图标加 accessibilityElementsHidden={true},屏幕阅读器会跳过它。emoji 图标对视障用户没有意义,不需要朗读。


不同场景的空状态

除了任务列表,其他地方也可能需要空状态:

搜索无结果

tsx 复制代码
<View style={styles.emptyContainer}>
  <Text style={styles.emptyIcon}>🔍</Text>
  <Text style={[styles.emptyText, {color: theme.subText}]}>没有找到"{searchText}"</Text>
  <Text style={[styles.emptySubText, {color: theme.subText}]}>试试其他关键词</Text>
</View>

显示用户搜索的关键词,让他知道搜索确实执行了,只是没有结果。

网络错误

tsx 复制代码
<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>
  <TouchableOpacity onPress={retry}>
    <Text style={{color: theme.accent, marginTop: 16}}>点击重试</Text>
  </TouchableOpacity>
</View>

网络错误不是空状态,但显示方式类似。提供重试按钮让用户可以再试一次。

权限不足

tsx 复制代码
<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 属性让实现空状态很简单。我们的空状态包含一个大图标、一行主文案、一行副文案,居中显示,用次要文字颜色。

可以根据不同情况显示不同的空状态,比如区分"没有任务"和"筛选无结果"。也可以在空状态里加按钮,让用户直接操作。

空状态虽然只在特定情况下显示,但它是用户体验的一部分。花点心思设计空状态,能让应用显得更专业、更贴心。


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

相关推荐
xkxnq6 小时前
第一阶段:Vue 基础入门(第 15天)
前端·javascript·vue.js
anyup8 小时前
2026第一站:分享我在高德大赛现场学到的技术、产品与心得
前端·架构·harmonyos
BBBBBAAAAAi8 小时前
Claude Code安装记录
开发语言·前端·javascript
源码获取_wx:Fegn08958 小时前
基于 vue智慧养老院系统
开发语言·前端·javascript·vue.js·spring boot·后端·课程设计
anyup8 小时前
从赛场到产品:分享我在高德大赛现场学到的技术、产品与心得
前端·harmonyos·产品
Jing_Rainbow9 小时前
【 前端三剑客-37 /Lesson61(2025-12-09)】JavaScript 内存机制与执行原理详解🧠
前端·javascript·程序员
UIUV9 小时前
模块化CSS学习笔记:从作用域问题到实战解决方案
前端·javascript·react.js
Kakarotto9 小时前
使用ThreeJS绘制东方明珠塔模型
前端·javascript·vue.js
donecoding9 小时前
TypeScript `satisfies` 的核心价值:两个例子讲清楚
前端·javascript