RN for OpenHarmony英雄联盟助手App实战:使用技巧实现
这篇文章介绍了如何为《英雄联盟》手游英雄创建技巧与对战建议页面,重点展示了列表渲染、空状态处理和视觉区分的设计要点。 主要技术实现: 使用ScrollView构建可滚动页面,处理长内容展示 通过allytips和enemytips两个数组分别渲染使用技巧和对战建议 采用绿色和红色区分两类内容,建立直观的视觉语义 为每条技巧添加序号标记,增强可读性和参考性 完善空状态处理,显示"暂无内容&

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_lol
玩一个新英雄,除了看技能说明,最想知道的就是"这个英雄怎么玩"和"怎么打这个英雄"。拳头官方为每个英雄都准备了使用技巧(allytips)和对战建议(enemytips),我们把这些信息整理成一个清晰的列表页面。
这篇文章的重点是列表渲染、空状态处理、以及如何用颜色区分不同类型的内容。
两种技巧的本质区别
API 返回的英雄数据中有两个数组字段,它们面向的用户群体完全不同:
allytips(使用技巧) 是给想玩这个英雄的人看的。比如盖伦的使用技巧可能会告诉你"E 技能可以在草丛中提前开启,出草丛时伤害已经叠满",这种信息能帮助新手更快上手一个英雄。
enemytips(对战建议) 是给要对抗这个英雄的人看的。比如"盖伦的被动在脱战后会快速回血,要持续消耗他不让他回血",知道这个信息后,你在对线时就会有意识地保持骚扰。
我们用颜色来区分这两类信息——使用技巧用绿色(代表"对你有利"),对战建议用红色(代表"需要警惕")。这种颜色语义在很多应用中都是通用的,用户不需要学习就能理解。
页面整体结构
import React, {useEffect, useState} from 'react';
import {View, Text, ScrollView, StyleSheet} from 'react-native';
import {colors} from '../../styles/colors';
import {useApp} from '../../context/AppContext';
import {useNavigation} from '../../context/NavigationContext';
import {championApi} from '../../api';
import {Loading} from '../../components/common';
import type {ChampionDetail} from '../../models/Champion';
这个页面不需要图片相关的组件,是一个纯文本展示页面。ScrollView 用来处理内容超出一屏的情况,因为有些英雄的技巧条目比较多。
export function ChampionTipsPage() {
const {state} = useApp();
const {params} = useNavigation();
const [champion, setChampion] = useState<ChampionDetail | null>(null);
const [loading, setLoading] = useState(true);
const championId = params.championId as string;
状态管理延续了之前页面的模式。champion 存储英雄详情数据,loading 控制加载状态的显示。championId 从路由参数获取,这个参数是从英雄详情页跳转过来时传入的。
数据加载的实现
useEffect(() => {
async function loadDetail() {
if (!state.version || !championId) return;
setLoading(true);
const detail = await championApi.getChampionDetail(
state.version,
championId,
);
setChampion(detail);
setLoading(false);
}
loadDetail();
}, [state.version, championId]);
if (loading || !champion) {
return <Loading fullScreen />;
}
这段数据加载的代码在前面几篇文章中出现过很多次了。你可能会想,能不能把它抽取成一个自定义 Hook?当然可以:
// hooks/useChampionDetail.ts
export function useChampionDetail(championId: string) {
const {state} = useApp();
const [champion, setChampion] = useState<ChampionDetail | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 加载逻辑...
}, [state.version, championId]);
return {champion, loading};
}
抽取后,页面组件就可以简化成:
const {champion, loading} = useChampionDetail(championId);
不过为了让每篇文章的代码相对独立、便于理解,我们在这个系列中保持了完整的写法。实际项目中推荐做这种抽取,能减少重复代码。
使用技巧列表的渲染
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
{/* 使用技巧 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>💡 使用技巧</Text>
{champion.allytips.length > 0 ? (
champion.allytips.map((tip, index) => (
<View key={index} style={styles.tipCard}>
<View style={styles.tipNumber}>
<Text style={styles.tipNumberText}>{index + 1}</Text>
</View>
<Text style={styles.tipText}>{tip}</Text>
</View>
))
) : (
<View style={styles.emptyCard}>
<Text style={styles.emptyText}>暂无使用技巧</Text>
</View>
)}
</View>
这段代码有几个值得展开说的点。
序号设计的考量
每条技巧前面有一个圆形的序号标记,用 index + 1 生成(因为 index 从 0 开始,显示给用户要从 1 开始)。
为什么要加序号?有几个原因:
- 技巧之间可能有先后顺序或重要程度的区别
- 序号能帮助用户记忆和引用,比如和朋友讨论时可以说"第 2 条技巧说的那个"
- 视觉上更有层次感,不会显得是一堆无序的文字
空状态的处理
{champion.allytips.length > 0 ? (
// 渲染列表
) : (
<View style={styles.emptyCard}>
<Text style={styles.emptyText}>暂无使用技巧</Text>
</View>
)}
有些英雄的 allytips 数组是空的,这时候显示"暂无使用技巧"比什么都不显示要好。用户至少知道这里应该有内容,只是暂时没有,而不是以为页面加载出了问题或者漏掉了什么。
空状态处理是一个容易被忽视但很重要的细节。好的空状态设计应该:
- 明确告诉用户"这里没有内容"
- 如果可能,解释为什么没有内容
- 如果可能,提供下一步操作的建议
在这个场景下,我们只做了第一点,因为内容是拳头官方提供的,我们无法控制也无法引导用户去创建内容。
key 的选择
{champion.allytips.map((tip, index) => (
<View key={index} style={styles.tipCard}>
这里用 index 作为 key。通常我们不推荐用 index 做 key,因为如果列表会重新排序或者动态增删,React 可能会复用错误的组件实例。但在这个场景下,技巧列表是静态的,不会变化,用 index 是安全的。
如果你想更严谨,可以用技巧内容的哈希值作为 key:
key={`tip-${tip.substring(0, 20)}-${index}`}
对战建议列表的渲染
{/* 对战建议 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>⚔️ 对战建议</Text>
{champion.enemytips.length > 0 ? (
champion.enemytips.map((tip, index) => (
<View key={index} style={[styles.tipCard, styles.enemyTipCard]}>
<View style={[styles.tipNumber, styles.enemyTipNumber]}>
<Text style={styles.tipNumberText}>{index + 1}</Text>
</View>
<Text style={styles.tipText}>{tip}</Text>
</View>
))
) : (
<View style={styles.emptyCard}>
<Text style={styles.emptyText}>暂无对战建议</Text>
</View>
)}
</View>
<View style={styles.bottomSpace} />
</ScrollView>
);
}
对战建议的结构和使用技巧完全一样,区别只在样式上。看这行代码:
style={[styles.tipCard, styles.enemyTipCard]}
React Native 的 style 属性支持数组,数组中的样式会按顺序合并,后面的会覆盖前面的同名属性。这种"基础样式 + 变体样式"的模式非常实用:
tipCard定义了卡片的通用样式:背景色、圆角、内边距、边框等enemyTipCard只覆盖需要变化的部分:左边框颜色从绿色变成红色
这样做的好处是代码更 DRY(Don’t Repeat Yourself)。如果以后要改卡片的圆角大小,只需要改 tipCard 一处,两种卡片都会生效。
样式设计详解
容器和区块样式
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
padding: 16,
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.textPrimary,
marginBottom: 12,
},
section 之间留 24px 的间距,让"使用技巧"和"对战建议"两个列表在视觉上明显分开。如果间距太小,用户可能会把两个列表混为一谈。
技巧卡片样式
tipCard: {
flexDirection: 'row',
backgroundColor: colors.backgroundCard,
borderRadius: 8,
padding: 12,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.border,
borderLeftWidth: 3,
borderLeftColor: colors.success,
},
enemyTipCard: {
borderLeftColor: colors.error,
},
卡片左边有一条 3px 的彩色边框,这是一个常见的设计手法,叫做"色带指示器"(color bar indicator)。它能在不占用太多空间的情况下传达信息类型:
- 绿色(success)= 对你有利的信息 = 使用技巧
- 红色(error)= 需要警惕的信息 = 对战建议
用户扫一眼颜色就知道这是哪类信息,不需要每次都读标题。
flexDirection: 'row' 让序号和文字横向排列。默认的 column 布局会让它们上下排列,不符合我们的设计。
序号样式
tipNumber: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: colors.success,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
enemyTipNumber: {
backgroundColor: colors.error,
},
tipNumberText: {
fontSize: 12,
fontWeight: 'bold',
color: colors.white,
},
序号用一个圆形背景包裹。实现圆形的方法是:width 和 height 相等,borderRadius 是它们的一半。24x24 的圆形配合 12px 的字号,数字在视觉上居中且不会显得拥挤。
alignItems: 'center' 和 justifyContent: 'center' 配合使用,让数字在圆形中水平和垂直都居中。这是 Flexbox 居中的标准写法。
文字样式
tipText: {
flex: 1,
fontSize: 14,
color: colors.textSecondary,
lineHeight: 20,
},
flex: 1 让文字区域占据序号之外的所有剩余宽度。如果不设置这个,文字区域的宽度会由内容决定,可能会导致布局问题。
lineHeight: 20 配合 fontSize: 14,行高是字号的 1.43 倍。这个比例让多行文字阅读起来比较舒适,行与行之间不会太挤也不会太松。
空状态样式
emptyCard: {
backgroundColor: colors.backgroundCard,
borderRadius: 8,
padding: 24,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
},
emptyText: {
fontSize: 14,
color: colors.textMuted,
},
bottomSpace: {
height: 20,
},
});
空状态卡片的 padding 比普通卡片大(24 vs 12),让"暂无内容"的提示不会显得太局促,有一种"留白"的感觉。
文字用 textMuted 颜色,比 textSecondary 更淡。这是一个视觉暗示:这条信息不重要,只是一个占位说明。
数据结构补充说明
allytips 和 enemytips 都是字符串数组,每个元素是一条独立的技巧:
interface ChampionDetail {
// ... 其他字段
allytips: string[]; // ["技巧1", "技巧2", "技巧3"]
enemytips: string[]; // ["建议1", "建议2"]
}
数组长度不固定,有的英雄有 3 条,有的有 5 条,有的一条都没有。拳头的数据质量参差不齐,有些英雄的技巧写得很详细很实用,有些就比较敷衍甚至过时了。这不是我们能控制的,只能如实展示。
如果你想提供更好的内容,可以考虑自建数据库,收集社区的高质量攻略。但这超出了这个项目的范围。
可能的优化方向
当前实现已经能满足基本需求,但如果想做得更好,还有一些可以改进的地方。
技巧分类:有些技巧是关于对线的,有些是关于团战的,有些是关于出装的。如果能自动分类或者让用户筛选,体验会更好。不过这需要对技巧内容做自然语言处理,实现复杂度较高。
用户贡献:官方的技巧有限且可能过时,可以让用户提交自己的心得,形成一个 UGC(用户生成内容)社区。这需要后端支持用户系统、内容审核等,是一个完整的产品方向。
关联展示:技巧中提到的技能名称可以做成可点击的链接,跳转到对应的技能详情。比如"盖伦的 E 技能"可以点击跳转到 E 技能的详细说明。这需要解析文本中的技能名称并做匹配,有一定的技术挑战。
收藏功能:让用户可以收藏觉得有用的技巧,方便以后查看。这需要本地存储或者用户账号系统的支持。
小结
使用技巧页是一个典型的列表展示页面,虽然功能简单,但涉及到几个重要的前端开发概念:
- 条件渲染:根据数据是否为空决定渲染列表还是空状态
- 样式复用:用"基础样式 + 变体样式"的模式减少重复代码
- 颜色语义:用颜色传达信息类型,减少用户的认知负担
- 空状态设计:妥善处理没有数据的情况,给用户明确的反馈
下一篇我们来实现英雄筛选功能,让用户可以按职业、难度等条件筛选英雄列表,这会涉及到更复杂的状态管理和列表过滤逻辑。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)