RN for OpenHarmony英雄联盟助手App实战:克制关系实现
摘要 本文介绍了英雄联盟英雄克制关系页面的设计与实现。该页面展示热门英雄之间的克制信息,帮助玩家在选人阶段做出决策。文章详细讲解了数据结构设计(包含英雄名称、克制与被克制列表)、静态数据展示组件的构建、以及视觉语义表达(使用🔴/🟢区分不利与有利关系)。通过卡片列表渲染、数组转字符串处理及样式设计,清晰呈现了英雄间的克制关系,并添加提示信息说明其参考性。整体采用简洁的无状态组件实现,兼顾功能性与

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_lol
在英雄联盟的选人阶段,了解英雄之间的克制关系非常重要。选到克制对手的英雄,可以在对线阶段占据优势;反之,被克制则可能整局都很难受。
这篇文章我们来实现克制关系页面,展示热门英雄的克制信息。这是一个典型的静态数据展示页面,我们会讨论数据结构的设计、卡片列表的渲染、以及如何用视觉设计传达"克制"和"被克制"的语义。
克制关系的游戏背景
在深入代码之前,先聊聊什么是"克制关系"。
英雄联盟中,某些英雄的技能机制天然克制另一些英雄。比如:
- 亚索被潘森克制:潘森的点击技能无法被亚索的风墙挡住,而且潘森的爆发伤害很高,亚索很难换血
- 劫被丽桑卓克制:丽桑卓的大招可以在劫切入时自我冰封,完美躲避劫的爆发
- 阿狸被卡萨丁克制:卡萨丁的被动减少魔法伤害,Q 技能还能打断阿狸的魅惑
了解这些克制关系,可以帮助玩家在选人阶段做出更好的决策。
数据结构设计
const counterData = [
{champion: '亚索', counters: ['潘森', '雷恩加尔', '马尔扎哈'], countered: ['阿卡丽', '劫', '卡特琳娜']},
{champion: '劫', counters: ['丽桑卓', '马尔扎哈', '卡萨丁'], countered: ['阿卡丽', '卡特琳娜', '菲兹']},
{champion: '阿狸', counters: ['卡萨丁', '加里奥', '马尔扎哈'], countered: ['泽拉斯', '辛德拉', '维克托']},
];
每个英雄的克制数据包含三个字段:
| 字段 | 类型 | 含义 |
|---|---|---|
| champion | string | 英雄名称 |
| counters | string[] | 克制该英雄的英雄列表(对该英雄不利) |
| countered | string[] | 被该英雄克制的英雄列表(对该英雄有利) |
命名的语义:
counters:谁克制我?(我被谁克制)countered:我克制谁?(谁被我克制)
这个命名可能有点绕,但从当前英雄的视角来看是合理的:counters 是"克制我的人",countered 是"被我克制的人"。
为什么用数组存储?
每个英雄通常有多个克制/被克制的对象,用数组可以存储任意数量。显示时用 join('、') 连接成字符串。
数据来源的考虑:
当前数据是硬编码在代码中的。在实际项目中,这些数据可能来自:
- 后端 API:根据大数据分析得出的克制关系
- 配置文件:JSON 文件,方便非开发人员维护
- 社区贡献:玩家投票或编辑的克制信息
硬编码的好处是简单、无需网络请求;缺点是更新需要发版。
组件导入与结构
import React from 'react';
import {View, Text, ScrollView, StyleSheet} from 'react-native';
import {colors} from '../../styles/colors';
export function CounterPickPage() {
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<Text style={styles.title}>克制关系</Text>
<Text style={styles.subtitle}>了解英雄之间的克制关系</Text>
这个组件非常简洁,没有任何状态(useState)和副作用(useEffect)。这是因为:
- 数据是静态的,不需要从 API 获取
- 没有用户交互需要处理
- 不需要响应任何外部变化
这种纯展示组件是最简单的组件类型,也是最容易测试和维护的。
页面标题的设计:
标题和副标题的组合是一种常见的页面头部设计:
- 标题:简短有力,告诉用户这是什么页面
- 副标题:补充说明,解释页面的用途
这种设计在整个 App 中保持一致,形成统一的视觉语言。
克制卡片的渲染
{counterData.map((data, index) => (
<View key={index} style={styles.card}>
<Text style={styles.championName}>{data.champion}</Text>
<View style={styles.section}>
<Text style={styles.sectionLabel}>🔴 被克制</Text>
<Text style={styles.sectionValue}>{data.counters.join('、')}</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionLabel}>🟢 克制</Text>
<Text style={styles.sectionValue}>{data.countered.join('、')}</Text>
</View>
</View>
))}
map 遍历渲染:
counterData.map() 遍历数据数组,为每个英雄生成一张卡片。这是 React 中渲染列表的标准方式。
key 的选择:
这里用 index 作为 key。虽然通常不推荐用 index,但在这个场景下是安全的,因为:
- 数据是静态的,不会动态增删
- 列表顺序不会改变
- 没有基于 key 的动画或状态
如果数据会动态变化,应该用英雄名称或唯一 ID 作为 key:
<View key={data.champion} style={styles.card}>
卡片内容结构:
每张卡片包含三部分:
- 英雄名称:金色大字,是卡片的标题
- 被克制区域:红色标记,显示克制该英雄的英雄
- 克制区域:绿色标记,显示被该英雄克制的英雄
Emoji 图标的语义设计
<Text style={styles.sectionLabel}>🔴 被克制</Text>
<Text style={styles.sectionLabel}>🟢 克制</Text>
这里用 Emoji 圆点来区分两种关系:
- 🔴 红色:表示危险、不利,用于"被克制"
- 🟢 绿色:表示安全、有利,用于"克制"
这种颜色语义是通用的:
- 红色 = 危险、停止、负面
- 绿色 = 安全、通行、正面
用户不需要阅读文字,看颜色就能理解哪个是好的、哪个是坏的。
为什么用 Emoji 而不是自定义图标?
- 简单:不需要额外的图片资源
- 跨平台:在所有设备上都能正常显示
- 语义清晰:红绿圆点的含义一目了然
- 易于修改:直接改字符串,不需要换图片
数组转字符串的处理
{data.counters.join('、')}
join('、') 方法把数组元素用中文顿号连接成字符串:
['潘森', '雷恩加尔', '马尔扎哈'].join('、')
// → '潘森、雷恩加尔、马尔扎哈'
为什么用中文顿号?
因为这是中文环境,用中文顿号(、)比英文逗号(,)更符合阅读习惯。这是一个本地化的细节。
空数组的处理:
如果某个英雄没有克制关系,join 会返回空字符串:
[].join('、') // → ''
可以添加空值处理:
<Text style={styles.sectionValue}>
{data.counters.length > 0 ? data.counters.join('、') : '暂无数据'}
</Text>
提示信息卡片
<View style={styles.infoCard}>
<Text style={styles.infoText}>💡 克制关系仅供参考,实际对局还需考虑玩家操作水平</Text>
</View>
</ScrollView>
);
}
页面底部有一个提示信息卡片,这是一个重要的设计决策。
为什么需要这个提示?
克制关系是统计意义上的,不是绝对的。一个高手用被克制的英雄,可能照样能打赢低手用克制英雄。这个提示:
- 管理用户预期:告诉用户这只是参考,不是必胜法则
- 避免误导:防止用户过度依赖克制关系
- 体现专业性:说明我们理解游戏的复杂性
💡 图标的选择:
灯泡 Emoji 表示"提示"或"小贴士",是一种通用的视觉语言。用户看到灯泡就知道这是一条建议或提示。
样式设计详解
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
padding: 16
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: colors.textPrimary,
marginBottom: 4
},
subtitle: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: 20
},
card: {
backgroundColor: colors.backgroundCard,
borderRadius: 12,
padding: 16,
marginBottom: 12,
borderWidth: 1,
borderColor: colors.border
},
championName: {
fontSize: 18,
fontWeight: '600',
color: colors.textGold,
marginBottom: 12
},
section: {
marginBottom: 8
},
sectionLabel: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: 4
},
sectionValue: {
fontSize: 14,
color: colors.textPrimary
},
infoCard: {
backgroundColor: colors.backgroundCard,
borderRadius: 8,
padding: 16,
marginTop: 8
},
infoText: {
fontSize: 13,
color: colors.textSecondary,
textAlign: 'center'
},
});
卡片样式的一致性:
克制卡片和提示卡片使用相似的样式(背景色、圆角、内边距),但有细微差别:
| 属性 | 克制卡片 | 提示卡片 |
|---|---|---|
| borderRadius | 12px | 8px |
| marginBottom | 12px | 0 |
| marginTop | 0 | 8px |
| 边框 | 有 | 无 |
提示卡片的圆角更小、没有边框,视觉上更轻量,不会抢克制卡片的风头。
英雄名称的样式:
championName: {
fontSize: 18,
fontWeight: '600',
color: colors.textGold,
marginBottom: 12
}
- fontSize: 18:比正文大,突出显示
- fontWeight: ‘600’:半粗体,强调但不过分
- color: colors.textGold:金色,和游戏风格一致
- marginBottom: 12:和下面的内容保持距离
信息层级的设计:
整个卡片的信息层级是:
- 英雄名称(最重要):18px,金色,加粗
- 区域标签(次要):14px,灰色
- 克制英雄(重要):14px,白色
这种层级让用户可以快速扫描:先看英雄名称确定是不是自己关心的,再看具体的克制关系。
扩展:添加搜索功能
当英雄数量增多时,用户可能需要搜索特定英雄:
const [searchText, setSearchText] = useState('');
const filteredData = counterData.filter(data =>
data.champion.includes(searchText) ||
data.counters.some(c => c.includes(searchText)) ||
data.countered.some(c => c.includes(searchText))
);
这个搜索逻辑会匹配:
- 英雄名称包含搜索词
- 克制列表中有英雄包含搜索词
- 被克制列表中有英雄包含搜索词
比如搜索"潘森",会显示所有和潘森有克制关系的英雄。
扩展:添加详细说明
可以为每个克制关系添加原因说明:
interface CounterRelation {
champion: string;
reason: string; // 克制原因
}
const counterData = [
{
champion: '亚索',
counters: [
{champion: '潘森', reason: '点击技能无法被风墙挡住'},
{champion: '雷恩加尔', reason: '草丛跳跃难以预判'},
{champion: '马尔扎哈', reason: '大招压制无法闪避'},
],
countered: [
{champion: '阿卡丽', reason: '风墙可挡 Q 技能'},
{champion: '劫', reason: '风墙可挡手里剑'},
{champion: '卡特琳娜', reason: '风墙可挡飞刀'},
],
},
];
显示时可以添加展开/收起功能,点击查看详细原因。
扩展:添加胜率数据
可以显示对线胜率,让数据更有说服力:
const counterData = [
{
champion: '亚索',
counters: [
{champion: '潘森', winRate: 42.3}, // 亚索对潘森的胜率
{champion: '雷恩加尔', winRate: 44.1},
{champion: '马尔扎哈', winRate: 45.2},
],
countered: [
{champion: '阿卡丽', winRate: 54.8},
{champion: '劫', winRate: 53.2},
{champion: '卡特琳娜', winRate: 55.1},
],
},
];
胜率低于 50% 表示被克制,高于 50% 表示克制。这种数据驱动的方式比主观判断更客观。
扩展:双向查询
当前设计是"以英雄为中心",用户需要找到特定英雄的卡片才能看到克制关系。可以添加双向查询功能:
const [selectedChampion, setSelectedChampion] = useState<string | null>(null);
// 查找所有和选中英雄有关系的数据
const getRelatedData = (champion: string) => {
const asMain = counterData.find(d => d.champion === champion);
const countersMe = counterData.filter(d =>
d.countered.includes(champion)
).map(d => d.champion);
const counteredByMe = counterData.filter(d =>
d.counters.includes(champion)
).map(d => d.champion);
return {
direct: asMain,
countersMe, // 克制我的英雄
counteredByMe, // 被我克制的英雄
};
};
这样用户选择一个英雄后,可以看到完整的克制网络。
扩展:可视化克制图谱
更进一步,可以用图形化的方式展示克制关系:
// 简化的克制关系图
const CounterGraph = ({champion}: {champion: string}) => {
const data = counterData.find(d => d.champion === champion);
if (!data) return null;
return (
<View style={styles.graph}>
{/* 克制我的英雄(上方) */}
<View style={styles.countersRow}>
{data.counters.map(c => (
<View key={c} style={styles.counterNode}>
<Text>{c}</Text>
<Text style={styles.arrow}>↓</Text>
</View>
))}
</View>
{/* 当前英雄(中间) */}
<View style={styles.centerNode}>
<Text style={styles.centerText}>{champion}</Text>
</View>
{/* 被我克制的英雄(下方) */}
<View style={styles.counteredRow}>
{data.countered.map(c => (
<View key={c} style={styles.counteredNode}>
<Text style={styles.arrow}>↓</Text>
<Text>{c}</Text>
</View>
))}
</View>
</View>
);
};
这种可视化方式更直观,用户一眼就能看出谁克制谁。
性能考虑
这个页面的性能非常好,因为:
- 无网络请求:数据是静态的
- 无状态管理:组件是纯展示的
- 列表很短:只有几个英雄,不需要虚拟化
如果数据量增大(比如包含所有 160+ 英雄),需要考虑:
使用 FlatList:
<FlatList
data={counterData}
keyExtractor={item => item.champion}
renderItem={({item}) => <CounterCard data={item} />}
/>
FlatList 只渲染可见区域的项目,对长列表性能更好。
数据分页:
const [page, setPage] = useState(1);
const pageSize = 10;
const displayData = counterData.slice(0, page * pageSize);
// 滚动到底部时加载更多
const loadMore = () => setPage(p => p + 1);
小结
克制关系页面展示了静态数据展示的典型实现:
- 数据结构设计:用对象数组存储结构化数据,字段命名清晰
- 语义化设计:用颜色(红/绿)传达"不利/有利"的含义
- 列表渲染:用 map 遍历数据生成卡片
- 用户提示:在底部添加免责声明,管理用户预期
这种模式可以复用到其他静态数据展示场景,比如英雄定位说明、装备分类介绍、游戏术语解释等。
下一篇我们来实现设置页面,让用户可以自定义 App 的行为。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)