RN for OpenHarmony英雄联盟助手App实战:装备对比实现
摘要 本文介绍了《英雄联盟》装备对比功能的实现方案。该功能通过并排展示两件装备的价格、属性等关键数据,帮助玩家做出更明智的出装决策。文章从使用场景分析入手,重点探讨了同类装备选择、性价比考量等典型场景。技术实现上采用复用弹窗选择器、条件渲染对比结果等设计,通过React状态管理控制交互流程。核心代码展示了装备选择逻辑、对比数据展示布局以及差异化属性显示处理,为游戏类应用的装备系统开发提供了可复用的

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_lol
出装的时候经常会纠结:同样是攻击装,无尽之刃和饮血剑哪个更适合当前局势?同样是防御装,兰顿之兆和荆棘之甲哪个性价比更高?装备对比功能让用户可以选择两件装备,把它们的价格、属性并排展示,帮助做出更好的出装决策。
这篇文章我们从产品设计的角度出发,分析装备对比的使用场景,然后一步步实现这个功能。重点包括弹窗选择器的复用设计、对比数据的展示布局、以及如何处理装备属性的差异化显示。
使用场景分析
在开始写代码之前,先想想用户会在什么情况下使用装备对比功能:
场景一:同类装备的选择
用户想出一件攻击装,但不确定选哪个。比如无尽之刃和纳什之牙都能提升输出,但一个偏物理暴击,一个偏攻速法伤。通过对比可以看出两者的属性差异。
场景二:性价比的考量
有时候两件装备效果类似,但价格差很多。比如用户想要护甲,布甲只要 300 金币,锁子甲要 800 金币。对比可以帮助用户判断多花的钱是否值得。
场景三:合成路线的规划
用户在规划出装路线时,可能想比较两条不同的合成路线。比如先出暴风大剑还是先出斗篷,哪个更划算。
理解这些场景后,我们知道对比功能需要展示的核心信息是:价格和属性加成。
整体架构设计
装备对比页的交互流程:
┌─────────────────────────────────────┐
│ ┌─────────┐ VS ┌─────────┐ │
│ │ 选择框1 │ │ 选择框2 │ │
│ │ 点击 │ │ 点击 │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 弹窗:选择装备 │ │
│ │ (复用同一个弹窗组件) │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ 对比结果区域 │ │
│ │ (两件都选好后显示) │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
关键设计决策:
- 复用弹窗:两个选择框共用一个弹窗组件,通过状态区分是为哪个位置选装备
- 条件渲染:只有两件装备都选好后才显示对比结果
- 数据来源:装备数据从全局状态获取,不需要额外请求
导入依赖与类型定义
import React, {useState} from 'react';
import {
View,
Text,
ScrollView,
Image,
TouchableOpacity,
StyleSheet,
} from 'react-native';
import {colors} from '../../styles/colors';
import {useApp} from '../../context/AppContext';
import {getItemIconUrl} from '../../utils/image';
import {formatNumber} from '../../utils/format';
import {Modal} from '../../components/common';
import type {Item} from '../../models/Item';
依赖说明:
Modal:自己封装的弹窗组件,用于显示装备选择列表formatNumber:数字格式化函数,给价格加上千分位getItemIconUrl:生成装备图标 URL 的工具函数Item:装备的类型定义
这些都是项目中已有的模块,体现了代码复用的思想。
状态设计
export function ItemComparePage() {
const {state} = useApp();
const [item1, setItem1] = useState<Item | null>(null);
const [item2, setItem2] = useState<Item | null>(null);
const [showPicker, setShowPicker] = useState<1 | 2 | null>(null);
这个页面有三个状态,每个都有明确的职责:
item1 和 item2
存储用户选择的两件装备。类型是 Item | null,null 表示还没选择。
为什么用 null 而不是 undefined?这是一个编码习惯的问题。null 表示"明确没有值",undefined 表示"值未定义"。在这个场景下,初始状态是"用户还没选择",用 null 语义更准确。
showPicker
控制弹窗的显示状态。类型是 1 | 2 | null,这是 TypeScript 的字面量联合类型:
1:正在为左边的位置选装备2:正在为右边的位置选装备null:弹窗关闭
为什么不用 boolean?因为我们不仅要知道弹窗是否打开,还要知道是为哪个位置选装备。用字面量类型比用两个 boolean(showPicker + pickerTarget)更简洁。
选择装备的处理逻辑
const handleSelectItem = (item: Item) => {
if (showPicker === 1) setItem1(item);
else if (showPicker === 2) setItem2(item);
setShowPicker(null);
};
当用户在弹窗中点击一件装备时:
- 根据
showPicker的值判断是更新 item1 还是 item2 - 关闭弹窗(
setShowPicker(null))
这个函数很简单,但它是"一个弹窗服务两个选择框"设计的关键。如果用两个独立的弹窗,就需要两个类似的处理函数,代码会重复。
选择区域的渲染
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<View style={styles.selectArea}>
<TouchableOpacity style={styles.selectBox} onPress={() => setShowPicker(1)}>
{item1 ? (
<>
<Image source={{uri: getItemIconUrl(state.version, item1.id)}} style={styles.itemIcon} />
<Text style={styles.itemName}>{item1.name}</Text>
</>
) : (
<Text style={styles.selectText}>选择装备</Text>
)}
</TouchableOpacity>
<Text style={styles.vsText}>VS</Text>
<TouchableOpacity style={styles.selectBox} onPress={() => setShowPicker(2)}>
{item2 ? (
<>
<Image source={{uri: getItemIconUrl(state.version, item2.id)}} style={styles.itemIcon} />
<Text style={styles.itemName}>{item2.name}</Text>
</>
) : (
<Text style={styles.selectText}>选择装备</Text>
)}
</TouchableOpacity>
</View>
选择框的两种状态:
每个选择框根据是否已选择装备,显示不同的内容:
- 未选择:显示灰色的"选择装备"提示文字
- 已选择:显示装备图标和名称
这种条件渲染用三元表达式实现:{item1 ? (...已选择的内容...) : (...未选择的内容...)}
Fragment 的使用:
<> 和 </> 是 React Fragment 的简写。当需要返回多个元素但不想增加额外的 DOM 节点时使用。这里用它包裹图标和名称,因为 TouchableOpacity 的 children 需要是单个元素或数组。
VS 文字的设计:
中间的"VS"用金色大字体,增加对抗感和视觉冲击力。这是一个纯装饰性的元素,但能让页面更有游戏感。
对比结果的渲染
{item1 && item2 && (
<View style={styles.compareCard}>
<View style={styles.compareRow}>
<Text style={styles.value}>{formatNumber(item1.gold?.total || 0)}</Text>
<Text style={styles.label}>价格</Text>
<Text style={styles.value}>{formatNumber(item2.gold?.total || 0)}</Text>
</View>
</View>
)}
条件渲染的时机:
{item1 && item2 && (...)} 确保只有两件装备都选好后才显示对比结果。这是短路求值的应用:
- 如果 item1 是 null,整个表达式返回 null(不渲染任何内容)
- 如果 item1 有值但 item2 是 null,整个表达式返回 null
- 只有两个都有值时,才渲染后面的 JSX
对比行的布局:
每一行分三部分:左边的值、中间的标签、右边的值。用 justifyContent: 'space-between' 让它们均匀分布。
formatNumber 的作用:
formatNumber(3400) 会返回 "3,400",给数字加上千分位分隔符,更易读。
装备选择弹窗
<Modal visible={showPicker !== null} onClose={() => setShowPicker(null)} title="选择装备">
<ScrollView style={styles.pickerList}>
{state.items.slice(0, 50).map(item => (
<TouchableOpacity key={item.id} style={styles.pickerItem} onPress={() => handleSelectItem(item)}>
<Image source={{uri: getItemIconUrl(state.version, item.id)}} style={styles.pickerIcon} />
<Text style={styles.pickerName}>{item.name}</Text>
</TouchableOpacity>
))}
</ScrollView>
</Modal>
</ScrollView>
);
}
Modal 组件的属性:
visible:控制弹窗是否显示,showPicker !== null时显示onClose:关闭弹窗的回调,设置showPicker为 nulltitle:弹窗标题
装备列表的渲染:
state.items.slice(0, 50) 只显示前 50 件装备。为什么要限制数量?
- 性能考虑:200+ 件装备全部渲染会有性能问题
- 用户体验:列表太长用户很难找到想要的装备
更好的做法是加一个搜索框,让用户可以搜索装备。这个优化留给读者作为练习。
列表项的结构:
每个列表项包含装备图标和名称,横向排列。点击后调用 handleSelectItem,传入被点击的装备。
样式设计详解
选择区域样式
const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: colors.background, padding: 16},
selectArea: {flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24},
selectBox: {flex: 1, backgroundColor: colors.backgroundCard, borderRadius: 12, padding: 20, alignItems: 'center', borderWidth: 1, borderColor: colors.border},
selectText: {fontSize: 14, color: colors.textMuted},
itemIcon: {width: 48, height: 48, borderRadius: 8, marginBottom: 8},
itemName: {fontSize: 12, color: colors.textPrimary, textAlign: 'center'},
vsText: {fontSize: 20, fontWeight: 'bold', color: colors.textGold, marginHorizontal: 12},
selectArea 的布局:
flexDirection: 'row':三个元素(左选择框、VS、右选择框)横向排列justifyContent: 'space-between':两端对齐,VS 在中间alignItems: 'center':垂直居中对齐
selectBox 的设计:
flex: 1:两个选择框平分剩余空间(VS 文字有固定的 marginHorizontal)borderRadius: 12:圆角让卡片更柔和alignItems: 'center':内容居中显示
对比结果样式
compareCard: {backgroundColor: colors.backgroundCard, borderRadius: 8, padding: 16, borderWidth: 1, borderColor: colors.border},
compareRow: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8},
label: {fontSize: 14, color: colors.textSecondary},
value: {fontSize: 14, color: colors.textGold, fontWeight: '600', width: 80, textAlign: 'center'},
value 的固定宽度:
width: 80 让左右两边的数值区域宽度一致,即使数字长度不同也能对齐。textAlign: 'center' 让数字在区域内居中。
弹窗列表样式
pickerList: {maxHeight: 400},
pickerItem: {flexDirection: 'row', alignItems: 'center', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: colors.border},
pickerIcon: {width: 32, height: 32, borderRadius: 4, marginRight: 12},
pickerName: {fontSize: 14, color: colors.textPrimary},
});
maxHeight 的作用:
限制列表最大高度为 400px,避免弹窗占满整个屏幕。超出部分可以滚动查看。
分隔线的设计:
每个列表项底部有 1px 的分隔线,帮助用户区分不同的装备。最后一项也有分隔线,这是一个小瑕疵,可以用 :last-child 伪类去掉(但 React Native 不支持,需要用条件判断)。
功能扩展建议
当前实现是基础版本,还有很多可以扩展的地方:
属性对比
除了价格,还可以对比装备的属性加成:
// 获取两件装备的所有属性键
const allStatKeys = new Set([
...Object.keys(item1.stats || {}),
...Object.keys(item2.stats || {}),
]);
// 渲染每个属性的对比
{Array.from(allStatKeys).map(key => (
<View key={key} style={styles.compareRow}>
<Text style={styles.value}>{item1.stats?.[key] || '-'}</Text>
<Text style={styles.label}>{formatStatKey(key)}</Text>
<Text style={styles.value}>{item2.stats?.[key] || '-'}</Text>
</View>
))}
胜负高亮
像英雄对比页一样,数值高的一方用金色高亮:
const winner = value1 > value2 ? 1 : value1 < value2 ? 2 : 0;
<Text style={[styles.value, winner === 1 && styles.valueWinner]}>
{value1}
</Text>
搜索功能
在弹窗中加一个搜索框,让用户可以快速找到想要的装备:
<Modal ...>
<SearchBar value={searchText} onChangeText={setSearchText} />
<ScrollView>
{filteredItems.map(item => ...)}
</ScrollView>
</Modal>
交换位置
加一个按钮让用户可以交换左右两件装备的位置:
<TouchableOpacity onPress={() => {
const temp = item1;
setItem1(item2);
setItem2(temp);
}}>
<Text>⇄ 交换</Text>
</TouchableOpacity>
小结
装备对比页展示了如何用一个弹窗服务多个选择框的设计模式。核心要点:
- 状态设计:用字面量联合类型(
1 | 2 | null)区分弹窗的服务对象 - 条件渲染:根据状态显示不同的内容(已选择/未选择、显示对比/隐藏对比)
- 组件复用:复用 Modal 组件,避免重复代码
- 布局技巧:用 Flexbox 实现左-中-右的对称布局
下一篇我们来实现符文系统,展示游戏中的符文列表和详情。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)