RN for OpenHarmony AnimeHub项目实战:全部分类页面开发
案例开源地址:https://atomgit.com/nutpi/Rn_openharmony_AnimeHub
全部分类页展示所有动漫分类,用户可以点击分类查看该类型的动漫列表。这篇来讲全部分类页的实现,重点是彩色卡片的循环配色和双列网格布局。
功能设计
全部分类页需要实现以下功能:
- 分类列表 - 展示所有动漫分类(如动作、冒险、喜剧等)
- 彩色卡片 - 每个分类用不同颜色的卡片展示
- 点击跳转 - 点击分类跳转到该分类的动漫列表
- 双列布局 - 一行两个分类,充分利用空间
这个页面的特点是用彩色卡片让分类列表更有视觉吸引力。如果所有卡片都是同一种颜色,页面会显得单调乏味。通过给每个分类分配不同的颜色,可以让页面更加生动有趣,也方便用户快速定位到想要的分类。
颜色配置
定义一组预设颜色,用于分类卡片的背景:
const GENRE_COLORS = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD',
'#F7DC6F', '#BB8FCE', '#85C1E9', '#F8B500', '#58D68D',
'#EC7063', '#5DADE2', '#AF7AC5', '#48C9B0', '#F5B041',
];
颜色选择的考量:
- 共 15 种不同的颜色,覆盖红、绿、蓝、黄、紫等主要色系
- 颜色饱和度适中,不会太刺眼,长时间看也不会疲劳
- 都是中等亮度的颜色,白色文字在上面清晰可读
- 相邻颜色差异明显,视觉上容易区分
为什么选择 15 种颜色?动漫分类大约有 20-30 种,15 种颜色循环使用可以保证:
- 相邻的分类颜色不同,视觉上有变化
- 颜色种类足够多,不会显得重复
- 数量适中,容易维护
颜色的选择是一门学问。太深的颜色(如深蓝、深紫)会让白色文字不够清晰;太浅的颜色(如浅黄、浅粉)会让卡片不够醒目,在深色背景上对比度不足。这组颜色是经过多次调试的,在深色主题下效果很好。
状态定义
const [genres, setGenres] = useState<Genre[]>([]);
const [loading, setLoading] = useState(true);
状态说明:
genres- 分类列表数据,存储从 API 获取的所有分类loading- 加载状态,控制 Loading 组件的显示
这个页面的状态非常简单,只有数据和加载状态两个。不需要分页状态,因为分类数量是固定的(大约 20-30 个),一次性加载即可。也不需要筛选状态,因为分类本身就是用来筛选动漫的。
简单的状态设计是好事,说明页面功能单一、职责清晰。复杂的状态往往意味着页面承担了太多功能,应该考虑拆分。
数据加载
useEffect(() => {
loadGenres();
}, []);
const loadGenres = async () => {
try {
const data = await getGenres();
setGenres(data.data || []);
} catch (error) {
console.error('Load genres error:', error);
} finally {
setLoading(false);
}
};
加载逻辑详解:
useEffect的依赖数组为空,表示只在组件挂载时执行一次getGenres是封装好的 API 函数,返回所有动漫分类- 用
data.data || []处理 API 返回空数据的情况 - 错误时打印日志,不会导致页面崩溃
finally确保无论成功失败都会把 loading 设为 false
分类数据是相对固定的,不会经常变化。MyAnimeList 的分类列表多年来基本没有变过。所以这个 API 的响应可以考虑做本地缓存,减少网络请求。
为什么用
data.data || []而不是直接用data.data?这是一种防御性编程。如果 API 返回的数据结构不符合预期(比如 data 是 null),直接访问data.data会报错。用|| []可以确保 genres 始终是数组,后续的 map 操作不会出错。
分类卡片渲染
const renderItem = ({ item, index }: { item: Genre; index: number }) => (
<TouchableOpacity
style={[styles.genreCard, { backgroundColor: GENRE_COLORS[index % GENRE_COLORS.length] }]}
onPress={() => navigation.navigate('GenreAnime', {
genreId: item.mal_id,
genreName: item.name
})}
>
<Text style={styles.genreName}>{item.name}</Text>
<Icon name="forward" size={18} color="rgba(255,255,255,0.7)" />
</TouchableOpacity>
);
渲染逻辑详解:
renderItem接收item(当前分类数据)和index(当前索引)- 用
index % GENRE_COLORS.length计算颜色索引,实现循环配色 - 点击时跳转到 GenreAnime 页面,传递分类 ID 和名称
- 卡片内容包括分类名称和一个向右的箭头图标
index % GENRE_COLORS.length是取模运算(求余数),这是实现循环的关键。假设 GENRE_COLORS.length 是 15:
- index = 0: 0 % 15 = 0,使用第 1 种颜色(红色)
- index = 7: 7 % 15 = 7,使用第 8 种颜色(蓝色)
- index = 14: 14 % 15 = 14,使用第 15 种颜色(橙色)
- index = 15: 15 % 15 = 0,回到第 1 种颜色(红色)
- index = 16: 16 % 15 = 1,使用第 2 种颜色(青色)
这种循环使用颜色的方式非常实用,可以用有限的颜色覆盖无限的列表项。无论分类有多少个,都能保证每个分类有颜色。
箭头图标用半透明白色
rgba(255,255,255,0.7),而不是纯白色。这样箭头既能看清,又不会太抢眼,不会分散用户对分类名称的注意力。
genreCard: {
width: '48%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: Spacing.lg,
borderRadius: BorderRadius.lg,
marginBottom: Spacing.md,
},
genreName: {
fontSize: FontSize.md,
fontWeight: '600',
color: Colors.text,
},
卡片样式详解:
width: '48%'- 两个卡片加上间距正好占满一行flexDirection: 'row'- 横向布局,名称在左,箭头在右justifyContent: 'space-between'- 名称和箭头分布在两端alignItems: 'center'- 垂直居中- 内边距和圆角让卡片更精致
为什么用 48% 而不是 50%?因为 FlatList 的
columnWrapperStyle设置了justifyContent: 'space-between',会在两个卡片之间自动留出间距。如果用 50%,两个卡片会紧贴在一起没有间距;用 48%,剩下的 4% 就是中间的间距。
这种百分比宽度的设计可以适应不同屏幕宽度。无论是小屏手机还是大屏平板,两个卡片都能正确显示。
页面结构
return (
<View style={styles.container}>
<Header
title="全部分类"
showBack
onBack={() => navigation.goBack()}
/>
Header 配置:
- 标题显示"全部分类",简洁明了
- 显示返回按钮,点击返回上一页
- 没有副标题,因为这是一级浏览页面,不需要额外说明
这个页面是从个人中心跳转过来的,所以需要返回按钮。如果是 Tab 页面(如首页、搜索页),通常不需要返回按钮。
条件渲染
{loading ? (
<Loading fullScreen />
) : (
<FlatList
data={genres}
renderItem={renderItem}
keyExtractor={(item) => item.mal_id.toString()}
numColumns={2}
columnWrapperStyle={styles.row}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
)}
</View>
);
两种状态的处理:
- 加载中显示全屏 Loading
- 加载完成显示分类列表
这里没有空状态处理(EmptyState),因为分类数据不可能为空。只要 API 正常工作,一定会返回分类列表。如果 API 出错,会在 catch 中处理,loading 会变成 false,但 genres 保持为空数组,FlatList 会显示空白。
在实际项目中,可以考虑加一个错误状态,当 API 出错时显示"加载失败,点击重试"。但对于这个简单的页面,目前的处理已经足够。
FlatList 配置详解
<FlatList
data={genres}
renderItem={renderItem}
keyExtractor={(item) => item.mal_id.toString()}
numColumns={2}
columnWrapperStyle={styles.row}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
配置说明:
data={genres}- 数据源是分类数组renderItem={renderItem}- 渲染函数keyExtractor- 用分类 ID 作为 key,保证唯一性numColumns={2}- 两列布局,这是实现网格的关键columnWrapperStyle- 每行的样式contentContainerStyle- 内容容器的样式showsVerticalScrollIndicator={false}- 隐藏滚动条
numColumns是 FlatList 实现网格布局的核心属性。设置为 2 后,FlatList 会自动把数据分成多行,每行显示 2 个元素。这比手动计算布局简单得多。
keyExtractor返回的必须是字符串,所以用toString()把数字 ID 转换为字符串。
row: {
justifyContent: 'space-between',
},
list: {
padding: Spacing.lg,
},
布局样式:
justifyContent: 'space-between'让每行的两个卡片分布在两端,中间自动留出间距- 列表有内边距,让内容不贴边
分类数据结构
分类的数据结构在 types 中定义:
interface Genre {
mal_id: number;
name: string;
count: number;
}
字段说明:
mal_id- 分类 ID,用于请求该分类的动漫name- 分类名称,如 “Action”、“Adventure”、“Comedy”count- 该分类下的动漫数量
这里没有显示 count(动漫数量),但可以考虑在卡片上显示,比如 “Action (1234)”。这样用户可以知道每个分类有多少动漫,对热门分类有个概念。
分类名称是英文的,如果需要中文显示,可以做一个映射表,把英文名称转换为中文。但考虑到动漫爱好者通常熟悉这些英文术语,保持英文也是可以接受的。
导航跳转
点击分类卡片会跳转到分类动漫页面:
navigation.navigate('GenreAnime', {
genreId: item.mal_id,
genreName: item.name
})
参数说明:
genreId- 分类 ID,用于请求该分类的动漫列表genreName- 分类名称,用于显示在下一个页面的 Header
为什么要传 genreName?因为下一个页面(GenreAnime)需要在 Header 中显示分类名称,让用户知道当前在看哪个分类的动漫。如果不传,下一个页面就需要再请求一次分类信息,或者显示一个通用的标题。
这种"传递显示用的数据"是一个常见的模式。虽然下一个页面可以根据 ID 请求数据,但传递已有的数据可以让页面更快显示,用户体验更好。
颜色循环的原理
GENRE_COLORS[index % GENRE_COLORS.length]
取模运算的数学原理:
取模运算(%)返回除法的余数。对于任意非负整数 index 和正整数 length:
index % length的结果总是在 0 到 length-1 之间- 当 index 增加时,结果会循环:0, 1, 2, …, length-1, 0, 1, 2, …
这是一个非常实用的编程技巧,在很多场景都能用到:
- 循环使用颜色(如本例)
- 循环播放列表
- 轮播图的无限滚动
- 游戏中的循环地图
与其他页面的关系
全部分类页在应用的导航结构中的位置:
- 入口 - 从个人中心页面的"全部分类"菜单项进入
- 出口 - 点击分类跳转到分类动漫页面(GenreAnime)
// 个人中心的跳转代码
navigation.navigate('AllGenres')
// 全部分类的跳转代码
navigation.navigate('GenreAnime', { genreId, genreName })
这形成了一个清晰的导航链:个人中心 → 全部分类 → 分类动漫 → 动漫详情。用户可以通过返回按钮一步步返回,也可以通过底部 Tab 直接切换到其他页面。
小结
全部分类页的核心是彩色卡片和循环配色。用取模运算循环使用预设颜色,让每个分类都有不同的颜色,视觉上更有吸引力,也方便用户快速定位。
双列布局用 FlatList 的 numColumns 属性实现,卡片宽度用百分比设置,可以适应不同屏幕宽度。这种响应式设计在移动端开发中很重要。
点击分类跳转到分类动漫页面,传递分类 ID 和名称。传递名称可以让下一个页面立即显示标题,不需要等待数据加载。
下一篇会讲分类动漫页面,展示某个分类下的所有动漫,涉及到分页加载和网格布局。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)