RN for OpenHarmony AnimeHub项目实战:角色列表页面开发

案例开源地址:https://atomgit.com/nutpi/Rn_openharmony_AnimeHub
角色列表是动漫详情的延伸页面,展示动漫中的所有角色信息。这篇来讲角色列表页的实现,重点是嵌套数据结构的处理和声优信息的展示。
功能设计
角色列表页需要展示以下信息:
- 角色头像 - 角色的图片
- 角色名称 - 角色的名字
- 角色类型 - Main(主角)或 Supporting(配角)
- 声优信息 - 配音演员的名字,优先显示日语声优
点击角色卡片可以跳转到角色详情页。
数据结构
角色数据的结构比较复杂,是嵌套的:
interface Character {
character: {
mal_id: number;
name: string;
images: {
jpg: {
image_url: string;
};
};
};
role: string;
voice_actors?: VoiceActor[];
}
结构说明:
character- 角色基本信息,包含 ID、名字、图片role- 角色类型,Main 或 Supportingvoice_actors- 声优数组,可能为空
API 返回的数据结构是这样设计的,角色信息嵌套在 character 字段里。这种设计是因为同一个角色可能出现在多部动漫里,角色本身是独立的实体。
状态定义
const { animeId, title } = route.params;
const [characters, setCharacters] = useState<Character[]>([]);
const [loading, setLoading] = useState(true);
状态说明:
animeId- 动漫 ID,用于请求数据title- 动漫标题,显示在页面副标题characters- 角色列表数据loading- 加载状态
把 title 也传过来是为了在 Header 里显示,让用户知道这是哪部动漫的角色列表。
数据加载
useEffect(() => {
loadCharacters();
}, []);
const loadCharacters = async () => {
try {
const data = await getAnimeCharacters(animeId);
setCharacters(data.data || []);
} catch (error) {
console.error('Load characters error:', error);
} finally {
setLoading(false);
}
};
加载逻辑:
- 组件挂载时加载数据
- 用
data.data || []处理空数据 - 错误时打印日志,不影响页面显示
这个页面不需要分页,因为一部动漫的角色数量通常不会太多,一次性加载即可。
角色卡片渲染
const renderItem = ({ item }: { item: Character }) => (
<TouchableOpacity
style={styles.characterCard}
onPress={() => navigation.navigate('CharacterDetail', {
characterId: item.character.mal_id
})}
>
卡片入口:
- 整个卡片可点击
- 点击跳转到角色详情页
- 传递 characterId 参数
注意这里用的是
item.character.mal_id,因为数据是嵌套的。
<Image
source={{ uri: item.character.images?.jpg?.image_url }}
style={styles.characterImage}
/>
角色头像:
- 从嵌套结构中取图片 URL
- 用可选链
?.防止空值报错
<View style={styles.characterInfo}>
<Text style={styles.characterName}>{item.character.name}</Text>
<Text style={styles.characterRole}>{item.role}</Text>
角色信息:
- 名字用主要文字颜色
- 角色类型用主题色,区分主角和配角
角色类型用主题色高亮,让用户一眼就能看出哪些是主角。
声优信息处理
{item.voice_actors && item.voice_actors.length > 0 && (
<View style={styles.voiceActor}>
<Text style={styles.vaLabel}>CV:</Text>
<Text style={styles.vaName}>
{item.voice_actors.find(va => va.language === 'Japanese')?.person.name ||
item.voice_actors[0]?.person.name}
</Text>
</View>
)}
</View>
</TouchableOpacity>
);
声优显示逻辑:
- 先检查 voice_actors 是否存在且不为空
- 优先显示日语声优
- 如果没有日语声优,显示第一个声优
动漫通常有多国配音,但用户最关心的是日语原版声优。用
find方法查找 language 为 Japanese 的声优,找不到就用第一个。
{item.voice_actors.find(va => va.language === 'Japanese')?.person.name ||
item.voice_actors[0]?.person.name}
这行代码的逻辑:
find(va => va.language === 'Japanese')- 查找日语声优?.person.name- 如果找到了,取声优名字||- 如果没找到日语声优item.voice_actors[0]?.person.name- 取第一个声优的名字
这是一个常见的"优先取 A,否则取 B"的模式。用可选链和逻辑或组合实现。
页面结构
return (
<View style={styles.container}>
<Header
title="角色"
subtitle={title}
showBack
onBack={() => navigation.goBack()}
/>
Header 组件:
- title 显示"角色"
- subtitle 显示动漫标题
- showBack 显示返回按钮
- onBack 处理返回逻辑
Header 是封装好的通用组件,在多个页面复用。通过 Props 控制显示内容和行为。
条件渲染
{loading ? (
<Loading fullScreen />
) : characters.length === 0 ? (
<EmptyState icon="people" title="暂无角色信息" />
) : (
<FlatList
data={characters}
renderItem={renderItem}
keyExtractor={(item) => item.character.mal_id.toString()}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
)}
</View>
);
三种状态:
- 加载中 - 显示 Loading
- 无数据 - 显示空状态,用人物图标
- 有数据 - 显示列表
空状态用 people 图标,和角色主题呼应。
FlatList 配置
<FlatList
data={characters}
renderItem={renderItem}
keyExtractor={(item) => item.character.mal_id.toString()}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
配置说明:
keyExtractor- 用角色 ID 作为 keycontentContainerStyle- 设置内容区域的样式- 隐藏滚动条
注意 keyExtractor 里用的是
item.character.mal_id,因为数据是嵌套的。
卡片样式
characterCard: {
flexDirection: 'row',
backgroundColor: Colors.backgroundCard,
borderRadius: BorderRadius.lg,
marginBottom: Spacing.md,
overflow: 'hidden',
},
卡片容器:
- 横向布局,图片在左,信息在右
- 卡片背景色,圆角
overflow: 'hidden'让图片的圆角生效
overflow: 'hidden'很重要,如果不设置,图片会超出圆角边界。
characterImage: {
width: 80,
height: 100,
backgroundColor: Colors.backgroundLight,
},
图片样式:
- 固定宽高 80x100
- 设置背景色,图片加载前显示
图片比例大约是 4:5,这是人物头像的常见比例。
characterInfo: {
flex: 1,
padding: Spacing.md,
justifyContent: 'center',
},
信息区域:
flex: 1占据剩余空间- 内边距让内容不贴边
- 垂直居中
characterName: {
fontSize: FontSize.md,
fontWeight: '600',
color: Colors.text,
},
characterRole: {
fontSize: FontSize.sm,
color: Colors.primary,
marginTop: Spacing.xs,
},
名字和角色类型:
- 名字用正常字号,加粗
- 角色类型用小字号,主题色
voiceActor: {
flexDirection: 'row',
marginTop: Spacing.sm,
},
vaLabel: {
fontSize: FontSize.sm,
color: Colors.textMuted,
},
vaName: {
fontSize: FontSize.sm,
color: Colors.textSecondary,
marginLeft: Spacing.xs,
},
声优信息样式:
- 横向布局,CV 标签和名字在一行
- 标签用最淡的颜色
- 名字用次要颜色
CV 是 Character Voice 的缩写,在动漫圈是常用的术语。
列表容器样式
list: {
padding: Spacing.lg,
},
内边距:
只设置了 padding,没有设置 paddingBottom。因为这个页面没有 Tab 栏,不需要额外的底部留白。
与详情页的关系
角色列表页是从详情页跳转过来的:
// 详情页的跳转代码
navigation.navigate('AnimeCharacters', {
animeId: anime.mal_id,
title: anime.title
})
参数传递:
- animeId 用于请求角色数据
- title 用于显示在 Header 副标题
这种设计让用户知道当前在看哪部动漫的角色,不会迷失方向。
跳转到角色详情
点击角色卡片会跳转到角色详情页:
navigation.navigate('CharacterDetail', {
characterId: item.character.mal_id
})
只传 characterId:
角色详情页会根据 characterId 请求完整的角色信息,不需要传其他参数。
数据流向
整个数据流向是这样的:
- 首页/搜索页 → 动漫详情页(传 animeId)
- 动漫详情页 → 角色列表页(传 animeId, title)
- 角色列表页 → 角色详情页(传 characterId)
每一层只传必要的参数,下一层自己请求需要的数据。这样可以保证数据的新鲜度,也避免传递大量数据。
性能考虑
角色列表通常不会太长,一部动漫的角色数量一般在几十个以内。所以:
- 不需要分页加载
- 不需要虚拟列表优化
- 一次性加载所有数据即可
如果遇到角色特别多的动漫(比如群像剧),可能需要考虑分页。但目前的实现对大多数情况够用了。
小结
角色列表页的核心是处理嵌套的数据结构和声优信息的优先级显示。数据结构嵌套时要注意用可选链防止空值报错,声优信息用 find + 逻辑或实现优先级。
卡片布局用横向排列,图片在左信息在右,是列表项的常见设计。角色类型用主题色高亮,让用户快速区分主角和配角。
下一篇会讲角色详情页,展示单个角色的完整信息。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)