RN for OpenHarmony AnimeHub项目实战:推荐页面开发
文章摘要: 本文介绍了动漫推荐页的实现方案,重点讲解双列网格布局和卡片宽度计算。通过精确计算卡片宽度(屏幕宽度减去总间距后平分两列),确保布局整齐。页面包含动漫封面、标题、推荐数等信息,采用FlatList的numColumns属性实现网格布局。状态管理包括加载状态和推荐数据,支持点击跳转详情页。样式设计注重视觉一致性,包括固定图片高度、圆角处理和文字限制两行显示。整体方案充分利用屏幕空间,提升用

案例开源地址:https://atomgit.com/nutpi/Rn_openharmony_AnimeHub
推荐页展示和当前动漫相似的其他作品,帮助用户发现更多感兴趣的内容。这篇来讲推荐页的实现,重点是双列网格布局和卡片宽度的计算。
功能设计
推荐页需要展示以下内容:
- 动漫封面 - 推荐动漫的海报图片
- 动漫标题 - 最多显示两行
- 推荐数 - 有多少用户推荐了这部动漫
- 网格布局 - 一行两个,充分利用屏幕空间
点击卡片可以跳转到对应动漫的详情页,形成浏览链。
卡片宽度计算
双列布局需要精确计算卡片宽度:
const { width } = Dimensions.get('window');
const CARD_WIDTH = (width - Spacing.lg * 3) / 2;
计算公式解析:
width- 屏幕宽度Spacing.lg * 3- 左边距 + 中间间距 + 右边距/ 2- 两列平分
假设 Spacing.lg 是 16,屏幕宽度是 375(iPhone SE):
- 总间距 = 16 * 3 = 48
- 可用宽度 = 375 - 48 = 327
- 卡片宽度 = 327 / 2 = 163.5
这个计算确保两个卡片加上间距正好占满屏幕宽度,不会有多余的空白或溢出。
状态定义
const { animeId, title } = route.params;
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
const [loading, setLoading] = useState(true);
状态说明:
animeId- 当前动漫 ID,用于请求推荐数据title- 当前动漫标题,显示在 Header 副标题recommendations- 推荐列表数据loading- 加载状态
推荐数据是基于当前动漫的,所以需要传入 animeId。API 会返回和这部动漫相似的其他作品。
数据加载
useEffect(() => {
loadRecommendations();
}, []);
const loadRecommendations = async () => {
try {
const data = await getAnimeRecommendations(animeId);
setRecommendations(data.data || []);
} catch (error) {
console.error('Load recommendations error:', error);
} finally {
setLoading(false);
}
};
加载逻辑:
- 组件挂载时加载数据
- 用
data.data || []处理空数据 - 错误时打印日志
推荐数据通常不会太多,一次性加载即可,不需要分页。
推荐卡片渲染
const renderItem = ({ item }: { item: Recommendation }) => (
<TouchableOpacity
style={styles.card}
onPress={() => navigation.push('AnimeDetail', { animeId: item.entry.mal_id })}
>
卡片入口:
- 整个卡片可点击
- 用
navigation.push跳转到详情页 - 传递推荐动漫的 ID
为什么用 push 而不是 navigate?因为用户可能从动漫 A 的推荐页跳到动漫 B,再从动漫 B 的推荐页跳到动漫 C。用 push 可以保留完整的浏览历史,用户可以一步步返回。
<Image
source={{ uri: item.entry.images?.jpg?.image_url }}
style={styles.image}
/>
封面图片:
- 从嵌套结构中取图片 URL
- 用可选链防止空值报错
推荐数据的结构是
item.entry.xxx,entry 里面才是动漫信息。这种嵌套设计是因为推荐本身也是一个实体,包含推荐数等额外信息。
<View style={styles.info}>
<Text style={styles.title} numberOfLines={2}>{item.entry.title}</Text>
<View style={styles.votes}>
<Icon name="heart" size={12} color={Colors.accent} />
<Text style={styles.votesText}>{item.votes} 推荐</Text>
</View>
</View>
</TouchableOpacity>
);
信息区域:
- 标题最多两行,超出显示省略号
- 推荐数用爱心图标 + 数字显示
推荐数(votes)表示有多少用户认为这两部动漫相似。数字越大,说明推荐越可靠。
页面结构
return (
<View style={styles.container}>
<Header
title="相关推荐"
subtitle={title}
showBack
onBack={() => navigation.goBack()}
/>
Header 配置:
- 标题显示"相关推荐"
- 副标题显示当前动漫名称
- 显示返回按钮
副标题显示当前动漫名称,让用户知道这是哪部动漫的推荐列表。
条件渲染
{loading ? (
<Loading fullScreen />
) : recommendations.length === 0 ? (
<EmptyState icon="sparkle" title="暂无推荐" />
) : (
<FlatList
data={recommendations}
renderItem={renderItem}
keyExtractor={(item) => item.entry.mal_id.toString()}
numColumns={2}
columnWrapperStyle={styles.row}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
)}
</View>
);
三种状态:
- 加载中显示 Loading
- 无推荐显示空状态,用星星图标
- 有推荐显示网格列表
空状态用 sparkle(星星)图标,和推荐的主题呼应。
FlatList 网格配置
<FlatList
data={recommendations}
renderItem={renderItem}
keyExtractor={(item) => item.entry.mal_id.toString()}
numColumns={2}
columnWrapperStyle={styles.row}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
网格配置详解:
numColumns={2}- 设置为两列columnWrapperStyle- 每行的样式keyExtractor- 用动漫 ID 作为 key
numColumns是 FlatList 实现网格布局的关键属性。设置后,FlatList 会自动把数据分成多行,每行显示指定数量的元素。
注意 keyExtractor 里用的是
item.entry.mal_id,因为数据是嵌套结构。
row: {
justifyContent: 'space-between',
},
行样式:
justifyContent: 'space-between'让两个卡片分布在两端- 中间的间距自动计算
配合卡片宽度的计算,两个卡片会正好占满一行,中间留出 Spacing.lg 的间距。
卡片样式
card: {
width: CARD_WIDTH,
backgroundColor: Colors.backgroundCard,
borderRadius: BorderRadius.lg,
marginBottom: Spacing.md,
overflow: 'hidden',
},
卡片容器:
- 宽度用计算好的 CARD_WIDTH
- 卡片背景色,圆角
overflow: 'hidden'让图片圆角生效- 底部间距
overflow: 'hidden'很重要,如果不设置,图片会超出圆角边界。
image: {
width: '100%',
height: 180,
backgroundColor: Colors.backgroundLight,
},
图片样式:
- 宽度 100% 填满卡片
- 高度固定 180
- 设置背景色作为加载占位
图片高度固定可以保证所有卡片高度一致,视觉上更整齐。180 的高度配合卡片宽度,大约是 1:1.1 的比例,接近海报的常见比例。
info: {
padding: Spacing.md,
},
title: {
fontSize: FontSize.md,
fontWeight: '600',
color: Colors.text,
lineHeight: 20,
},
信息区域样式:
- 内边距让内容不贴边
- 标题加粗,设置行高
lineHeight: 20配合numberOfLines={2}可以让两行标题的高度固定为 40。
votes: {
flexDirection: 'row',
alignItems: 'center',
marginTop: Spacing.sm,
gap: Spacing.xs,
},
votesText: {
fontSize: FontSize.xs,
color: Colors.textSecondary,
},
推荐数样式:
- 横向布局,图标和文字在一行
- 用 gap 设置间距
- 小字号,次要颜色
推荐数据结构
推荐的数据结构在 types 中定义:
interface Recommendation {
entry: {
mal_id: number;
title: string;
images: {
jpg: {
image_url: string;
};
};
};
votes: number;
}
字段说明:
entry- 推荐的动漫信息votes- 推荐数,表示有多少用户推荐
这个结构和角色列表、出演作品的结构类似,都是嵌套设计。entry 里面是动漫的基本信息,外层是推荐相关的信息。
与新番页的对比
推荐页和新番页都是网格布局,但有一些区别:
- 数据来源 - 新番页是全局数据,推荐页是某部动漫的相关数据
- 卡片内容 - 新番页显示评分,推荐页显示推荐数
- 分页 - 新番页有分页,推荐页没有
虽然布局类似,但因为数据结构和展示内容不同,没有抽象成通用组件。如果项目中有更多类似的页面,可以考虑抽象。
导航跳转
点击推荐卡片会跳转到动漫详情页:
navigation.push('AnimeDetail', { animeId: item.entry.mal_id })
跳转说明:
- 用 push 创建新页面实例
- 传递推荐动漫的 ID
用户可以从推荐页不断跳转到新的动漫,形成探索链。每次跳转都是新页面,返回时可以一步步回到之前的页面。
推荐算法
Jikan API 的推荐是基于用户投票的:
- 用户可以在 MyAnimeList 网站上推荐相似动漫
- 每个推荐都有投票数
- API 返回投票数最高的推荐
这种基于用户投票的推荐比算法推荐更可靠,因为是真实用户认为这两部动漫相似。投票数越高,说明越多人认同这个推荐。
小结
推荐页的核心是双列网格布局和卡片宽度的计算。用 numColumns={2} 配合精确的宽度计算,可以实现整齐的网格效果。
推荐数用爱心图标显示,让用户知道这个推荐有多少人认同。点击卡片可以跳转到详情页,用 push 保留浏览历史。
这个页面的设计和新番页类似,但数据来源和展示内容不同。网格布局是展示多个动漫的常用方式,在应用中多次出现。
下一篇会讲统计页面,展示动漫的评分分布等数据。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)