#
空白不是错误
打开一个新安装的 App,列表是空的。这时候用户看到什么?
如果只是一片空白,用户会困惑:是加载失败了?是我操作错了?还是本来就没有内容?
空状态占位图就是为了解决这个问题。它告诉用户"这里确实没有内容,但这不是错误,你可以这样做来添加内容"。
一个好的空状态设计,能把"什么都没有"的尴尬变成"一切从这里开始"的期待。
FlatList 的空状态支持
React Native 的 FlatList 组件内置了空状态支持,通过 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: 18 加 fontWeight: '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 的内容区域。如果 FlatList 有 ListHeaderComponent,空状态会显示在 header 下面。
tsx
<FlatList
ListHeaderComponent={<Header />}
ListEmptyComponent={<Empty />}
...
/>
这意味着即使列表为空,header 仍然会显示。这通常是期望的行为,比如搜索框应该一直显示,不管有没有搜索结果。
我们的应用没有用 ListHeaderComponent,搜索框和筛选按钮是在 FlatList 外面的。所以空状态会占据整个列表区域。
空状态的高度
空状态应该有多高?
如果太矮,会显得内容很少,页面很空。如果太高,可能会超出屏幕,用户看不到引导文字。
我们用 paddingVertical: 60,让空状态有一定的高度但不会太高。在大多数设备上,空状态会显示在屏幕中央偏上的位置,用户一眼就能看到。
如果想让空状态垂直居中,可以给容器设置 flex: 1 和 justifyContent: '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>
小结
空状态是容易被忽视但很重要的细节。一个好的空状态能把"什么都没有"变成"一切从这里开始"。
FlatList 的 ListEmptyComponent 属性让实现空状态很简单。我们的空状态包含一个大图标、一行主文案、一行副文案,居中显示,用次要文字颜色。
可以根据不同情况显示不同的空状态,比如区分"没有任务"和"筛选无结果"。也可以在空状态里加按钮,让用户直接操作。
空状态虽然只在特定情况下显示,但它是用户体验的一部分。花点心思设计空状态,能让应用显得更专业、更贴心。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net