RN for OpenHarmony英雄联盟助手App实战:符文系统实现
摘要:英雄联盟符文系统实现 本文介绍了英雄联盟移动端符文列表页的实现过程,重点解析了符文系统的数据结构和界面开发技巧。符文系统采用三级结构(符文系-符文槽位-符文),通过TypeScript接口定义数据类型,并利用常量映射实现中英文名称和主题色的转换。页面实现中采用了动态主题适配、缓存策略优化和色带指示器设计,确保在不同主题下都能正确显示各符文系的特色(精密-金、主宰-红、巫术-蓝紫、坚决-绿、启

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_lol
符文是英雄联盟中非常重要的系统,它能为英雄提供额外的属性和特殊效果。不同的符文搭配可以让同一个英雄有完全不同的玩法。符文系统页面要展示五大符文系(精密、主宰、巫术、坚决、启迪),让用户可以浏览每个符文系下的所有符文。
这篇文章我们来实现符文列表页,重点是符文数据结构的理解、动态颜色主题的应用、以及 TypeScript 常量映射的使用技巧。
符文系统的数据结构
在开始写代码之前,先理解一下符文系统的数据结构。这对于后续的开发非常重要。
层级关系
符文系统有三个层级:
符文系 (RunePath)
└── 符文槽位 (RuneSlot)
└── 符文 (Rune)
符文系:共有 5 个,分别是精密、主宰、巫术、坚决、启迪。每个符文系有自己的主题色和图标。
符文槽位:每个符文系有 4 个槽位。第一个槽位是基石符文(最强的),后面三个是普通符文。
符文:每个槽位有 3-4 个符文可选,玩家需要从中选择一个。
类型定义
我们用 TypeScript 接口来描述这个结构:
// 符文系
export interface RunePath {
id: number;
key: string;
icon: string;
name: string;
slots: RuneSlot[];
}
// 符文槽位
export interface RuneSlot {
runes: Rune[];
}
// 符文
export interface Rune {
id: number;
key: string;
icon: string;
name: string;
shortDesc: string;
longDesc: string;
}
字段说明:
id:数字 ID,拳头内部使用的唯一标识key:英文标识,如 “Precision”、“Domination”icon:图标路径,用于拼接完整的图标 URLname:英文名称(API 返回的是英文,我们需要自己做中文映射)slots:符文槽位数组,每个槽位包含多个可选符文
常量映射的设计
API 返回的符文名称是英文的,我们需要转成中文。同时,每个符文系有自己的主题色,也需要定义。
// 符文系中文名
export const RunePathNames = {
Precision: '精密',
Domination: '主宰',
Sorcery: '巫术',
Resolve: '坚决',
Inspiration: '启迪',
} as const;
// 符文系颜色
export const RunePathColors = {
Precision: '#C8AA6E',
Domination: '#D44242',
Sorcery: '#9FAAFC',
Resolve: '#A1D586',
Inspiration: '#49AAB8',
} as const;
as const 的作用:
as const 是 TypeScript 的常量断言,它会把对象的类型收窄为字面量类型。比如 RunePathNames.Precision 的类型会是 '精密' 而不是 string。这在某些场景下能提供更精确的类型检查。
颜色的选择:
每个符文系的颜色都有其含义:
- 精密(金色):代表精准、高效,适合射手和需要持续输出的英雄
- 主宰(红色):代表杀戮、侵略,适合刺客和需要爆发的英雄
- 巫术(蓝紫色):代表魔法、神秘,适合法师
- 坚决(绿色):代表坚韧、防御,适合坦克和战士
- 启迪(青色):代表创新、灵活,提供各种实用效果
工具函数
// 获取符文系中文名
export function getRunePathName(key: string): string {
return RunePathNames[key as keyof typeof RunePathNames] || key;
}
// 获取符文系颜色
export function getRunePathColor(key: string): string {
return RunePathColors[key as keyof typeof RunePathColors] || '#C89B3C';
}
这两个函数根据符文系的 key 返回对应的中文名和颜色。如果遇到未知的 key(比如拳头新增了符文系),会返回原始 key 或默认颜色,避免报错。
keyof typeof 是 TypeScript 的类型操作符:
typeof RunePathNames获取对象的类型keyof获取这个类型的所有键的联合类型
页面实现
导入依赖
import React, {useEffect, useState, useMemo} from 'react';
import {View, Text, ScrollView, Image, TouchableOpacity, StyleSheet} from 'react-native';
import {useTheme} from '../../context/ThemeContext';
import {useApp} from '../../context/AppContext';
import {useNavigation} from '../../context/NavigationContext';
import {runeApi} from '../../api';
import {getRuneIconUrl} from '../../utils/image';
import {getRunePathName, getRunePathColor} from '../../models/Rune';
import {Loading} from '../../components/common';
这个页面用到了主题系统(useTheme),因为我们需要支持深色/浅色模式切换。符文系的颜色是固定的,但页面的背景色、文字颜色需要跟随主题变化。
组件结构与状态
export function RuneListPage() {
const {colors} = useTheme();
const {state, setRunes} = useApp();
const {navigate} = useNavigation();
const [loading, setLoading] = useState(true);
状态说明:
colors:当前主题的颜色配置state.runes:全局状态中缓存的符文数据setRunes:更新符文数据的方法loading:加载状态
动态样式
const styles = useMemo(() => StyleSheet.create({
container: {flex: 1, backgroundColor: colors.background, padding: 16},
pathCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.backgroundCard,
borderRadius: 12,
padding: 16,
marginBottom: 12,
borderWidth: 1,
borderColor: colors.border,
borderLeftWidth: 4
},
pathIcon: {width: 48, height: 48},
pathInfo: {flex: 1, marginLeft: 16},
pathName: {fontSize: 18, fontWeight: '600', marginBottom: 4},
pathDesc: {fontSize: 14, color: colors.textSecondary},
arrow: {fontSize: 20, color: colors.textMuted},
bottomSpace: {height: 20},
}), [colors]);
为什么用 useMemo?
样式对象依赖 colors,当主题切换时 colors 会变化。用 useMemo 包裹可以:
- 只在 colors 变化时重新创建样式对象
- 避免每次渲染都创建新的样式对象
borderLeftWidth: 4 的设计:
每个符文系卡片左边有一条 4px 的彩色边框,颜色是符文系的主题色。这种设计叫"色带指示器",能在不占用太多空间的情况下传达信息。
数据加载
useEffect(() => {
async function loadRunes() {
if (!state.version) return;
if (state.runes.length === 0) {
setLoading(true);
const runes = await runeApi.getRuneList(state.version);
setRunes(runes);
}
setLoading(false);
}
loadRunes();
}, [state.version]);
if (loading) return <Loading fullScreen />;
缓存策略:
if (state.runes.length === 0) 判断全局状态中是否已有符文数据。如果有,直接使用缓存,不再请求。这和装备列表页的策略一样。
符文数据在一个游戏版本内不会变化,缓存是安全的。
列表渲染
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
{state.runes.map(path => {
const pathColor = getRunePathColor(path.key);
return (
<TouchableOpacity
key={path.id}
style={[styles.pathCard, {borderLeftColor: pathColor}]}
onPress={() => navigate('RuneDetail', {pathId: path.id})}>
<Image
source={{uri: getRuneIconUrl(path.icon)}}
style={[styles.pathIcon, {tintColor: pathColor}]}
/>
<View style={styles.pathInfo}>
<Text style={[styles.pathName, {color: pathColor}]}>
{getRunePathName(path.key)}
</Text>
<Text style={styles.pathDesc}>{path.slots.length} 层符文</Text>
</View>
<Text style={styles.arrow}>→</Text>
</TouchableOpacity>
);
})}
<View style={styles.bottomSpace} />
</ScrollView>
);
}
动态颜色的应用:
每个符文系卡片有三处使用了动态颜色:
- 左边框:
borderLeftColor: pathColor - 图标:
tintColor: pathColor(给图标着色) - 名称:
color: pathColor
这样每个符文系都有自己独特的视觉标识,用户一眼就能区分。
tintColor 的作用:
tintColor 是 React Native Image 组件的属性,它会给图片着色。原理是把图片中的非透明像素都染成指定的颜色。这对于单色图标非常有用——我们只需要一套白色或黑色的图标,然后用 tintColor 动态改变颜色。
slots.length 的展示:
显示"4 层符文"这样的信息,让用户知道点进去会看到多少内容。这是一个小细节,但能提升用户体验。
符文描述的清理
符文的描述文本也是带 HTML 标签的,需要清理:
// 清理符文描述
export function cleanRuneDesc(desc: string): string {
if (!desc) return '';
return desc
.replace(/<br>/gi, '\n')
.replace(/<[^>]+>/g, '')
.trim();
}
这个函数和之前的 stripHtml 类似,把 <br> 转成换行符,移除其他 HTML 标签。
图标 URL 的生成
符文图标的 URL 格式和英雄、装备不太一样:
export function getRuneIconUrl(iconPath: string): string {
return `https://ddragon.leagueoflegends.com/cdn/img/${iconPath}`;
}
API 返回的 icon 字段已经包含了相对路径(如 perk-images/Styles/Precision/Precision.png),我们只需要拼接上 CDN 的基础 URL。
为什么用 ScrollView 而不是 FlatList?
符文系只有 5 个,数据量很小,用 ScrollView 就够了。FlatList 的优势是虚拟化渲染,适合长列表。对于只有几个元素的列表,FlatList 反而会增加不必要的复杂度。
选择组件的原则:
- < 20 个元素:ScrollView
- 20-100 个元素:FlatList
- > 100 个元素:FlatList + 性能优化(如 getItemLayout)
交互设计
点击符文系卡片后,跳转到符文详情页,传入 pathId 参数:
onPress={() => navigate('RuneDetail', {pathId: path.id})}
符文详情页会根据这个 ID 找到对应的符文系,展示其下所有的符文。
卡片右边的箭头(→)是一个视觉提示,告诉用户这个卡片是可点击的,点击后会进入下一级页面。
主题适配
这个页面完全支持深色/浅色模式切换:
- 背景色:跟随主题变化
- 卡片背景:跟随主题变化
- 边框颜色:跟随主题变化
- 文字颜色:跟随主题变化
- 符文系颜色:固定不变(这是符文系的标识色)
符文系的颜色不跟随主题变化是有意为之的。这些颜色是游戏中的标准色,玩家已经形成了认知(比如"红色是主宰系"),改变会造成困惑。
小结
符文系统页面展示了几个重要的技术点:
- 数据结构理解:符文系统有三层嵌套结构,需要先理解再开发
- 常量映射:用 TypeScript 常量对象做中英文映射和颜色映射
- 动态颜色:每个符文系有自己的主题色,通过内联样式动态应用
- tintColor:给图标动态着色,一套图标多种颜色
- 主题适配:页面背景跟随主题,符文系颜色保持固定
下一篇我们来实现符文详情页,展示每个符文系下的所有符文及其效果。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)