RN for OpenHarmony英雄联盟助手App实战:合成树实现

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_lol
一件成型装备可能需要好几层合成。比如无尽之刃需要暴风大剑 + 斗篷 + 长剑,而暴风大剑又需要长剑 + 长剑。合成树功能把这种层级关系用树形结构展示出来,让玩家一眼就能看清完整的合成路径。
这篇文章我们来实现合成树页面,重点是递归数据结构的构建、树形 UI 的渲染、以及缩进层级的处理。
合成关系的数据结构
在装备数据中,每件装备都有一个 from 字段,记录合成它需要哪些装备的 ID:
// 无尽之刃的数据
{
id: "3031",
name: "无尽之刃",
from: ["1038", "1018", "1036"], // 暴风大剑、斗篷、长剑
// ...
}
// 暴风大剑的数据
{
id: "1038",
name: "暴风大剑",
from: ["1036", "1036"], // 两把长剑
// ...
}
// 长剑的数据(基础装备,没有 from)
{
id: "1036",
name: "长剑",
// 没有 from 字段,说明是基础装备
}
我们要做的就是根据这个 from 关系,递归地构建出完整的合成树。
页面结构
import React from 'react';
import {View, Text, ScrollView, Image, StyleSheet} from 'react-native';
import {colors} from '../../styles/colors';
import {useApp} from '../../context/AppContext';
import {useNavigation} from '../../context/NavigationContext';
import {getItemIconUrl} from '../../utils/image';
import {formatNumber} from '../../utils/format';
export function ItemBuildPage() {
const {state} = useApp();
const {params} = useNavigation();
const itemId = params.itemId as string;
const item = state.items.find(i => i.id === itemId);
if (!item) {
return (
<View style={styles.container}>
<Text style={styles.errorText}>装备不存在</Text>
</View>
);
}
和装备详情页一样,从全局状态中查找装备数据。如果找不到,显示错误提示。
递归构建合成树
这是整个页面的核心逻辑。我们用递归函数遍历合成关系,构建出树形数据结构。
// 递归获取合成树
const getBuildTree = (id: string, depth = 0): any => {
const currentItem = state.items.find(i => i.id === id);
if (!currentItem) return null;
return {
item: currentItem,
depth,
children: currentItem.from?.map(fromId => getBuildTree(fromId, depth + 1)) || [],
};
};
const buildTree = getBuildTree(itemId);
这个函数做了什么:
- 根据 ID 查找装备数据
- 如果找不到,返回 null(递归终止条件之一)
- 如果找到了,返回一个节点对象,包含:
item:装备数据depth:当前深度(用于计算缩进)children:子节点数组,通过递归调用生成
递归的终止条件有两个:
- 找不到装备(返回 null)
- 装备没有
from字段(基础装备,children 为空数组)
以无尽之刃为例,生成的树结构大概是这样的:
{
item: { name: "无尽之刃", ... },
depth: 0,
children: [
{
item: { name: "暴风大剑", ... },
depth: 1,
children: [
{ item: { name: "长剑", ... }, depth: 2, children: [] },
{ item: { name: "长剑", ... }, depth: 2, children: [] },
]
},
{ item: { name: "斗篷", ... }, depth: 1, children: [] },
{ item: { name: "长剑", ... }, depth: 1, children: [] },
]
}
递归渲染树节点
有了树形数据,接下来用递归函数渲染 UI。
const renderTreeNode = (node: any) => {
if (!node) return null;
return (
<View key={node.item.id} style={styles.treeNode}>
<View style={[styles.itemRow, {marginLeft: node.depth * 20}]}>
<Image
source={{uri: getItemIconUrl(state.version, node.item.id)}}
style={styles.itemIcon}
/>
<View style={styles.itemInfo}>
<Text style={styles.itemName}>{node.item.name}</Text>
<Text style={styles.itemGold}>
💰 {formatNumber(node.item.gold?.total || 0)}
</Text>
</View>
</View>
{node.children.length > 0 && (
<View style={styles.childrenContainer}>
{node.children.map((child: any) => renderTreeNode(child))}
</View>
)}
</View>
);
};
关键点解析:
缩进的实现:marginLeft: node.depth * 20 根据深度计算左边距。深度 0 没有缩进,深度 1 缩进 20px,深度 2 缩进 40px,以此类推。
递归渲染子节点:如果有子节点,遍历 children 数组,对每个子节点递归调用 renderTreeNode。
key 的选择:用 node.item.id 作为 key。注意同一件装备可能在树中出现多次(比如长剑),但 React 的 key 只需要在同级元素中唯一,不同层级可以重复。
页面渲染
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<View style={styles.header}>
<Text style={styles.title}>合成树</Text>
<Text style={styles.subtitle}>{item.name}</Text>
</View>
<View style={styles.treeContainer}>
{renderTreeNode(buildTree)}
</View>
{/* 总价计算 */}
<View style={styles.totalCard}>
<Text style={styles.totalLabel}>总价</Text>
<Text style={styles.totalValue}>
💰 {formatNumber(item.gold?.total || 0)}
</Text>
</View>
<View style={styles.bottomSpace} />
</ScrollView>
);
}
页面分三部分:
- 头部:显示"合成树"标题和装备名称
- 树形区域:展示完整的合成路径
- 总价卡片:显示合成这件装备需要的总金币
样式设计
树节点样式
treeNode: {
marginBottom: 8,
},
itemRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
borderLeftWidth: 2,
borderLeftColor: colors.primary,
paddingLeft: 12,
},
itemIcon: {
width: 36,
height: 36,
borderRadius: 4,
borderWidth: 1,
borderColor: colors.border,
},
itemInfo: {
marginLeft: 12,
flex: 1,
},
每个节点左边有一条金色的竖线(borderLeftWidth: 2),形成视觉上的连接感。图标尺寸 36x36,比详情页的小一些,因为树中可能有很多节点。
子节点容器样式
childrenContainer: {
marginLeft: 16,
borderLeftWidth: 1,
borderLeftColor: colors.border,
},
子节点容器也有一条竖线,但颜色是灰色的,和父节点的金色竖线区分开。这样形成了一个层级分明的视觉效果。
总价卡片样式
totalCard: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: colors.backgroundCard,
borderRadius: 8,
padding: 16,
marginTop: 16,
borderWidth: 1,
borderColor: colors.borderGold,
},
totalValue: {
fontSize: 20,
fontWeight: 'bold',
color: colors.textGold,
},
总价卡片用金色边框突出显示,价格用大号金色字体,让用户一眼就能看到。
性能考虑
递归在数据量大的时候可能有性能问题。不过英雄联盟的装备合成树通常不会太深(最多 3-4 层),节点数量也不会太多(最多十几个),所以不需要特别优化。
如果你的场景有更深的树结构,可以考虑:
虚拟化渲染:只渲染可视区域的节点,滚动时动态加载。不过 React Native 的 FlatList 不太适合树形结构,需要自己实现。
折叠功能:默认只展开第一层,用户点击后展开更深的层级。这样初始渲染的节点数量会少很多。
缓存计算结果:用 useMemo 缓存 buildTree 的结果,避免每次渲染都重新计算。
const buildTree = useMemo(() => getBuildTree(itemId), [itemId, state.items]);
交互优化
当前实现是纯展示的,可以加一些交互功能:
点击跳转:点击树中的任意装备,跳转到那件装备的详情页。
<TouchableOpacity onPress={() => navigate('ItemDetail', {itemId: node.item.id})}>
{/* 节点内容 */}
</TouchableOpacity>
展开/折叠:让用户可以折叠不关心的分支,专注于某一条合成路径。
高亮路径:当用户点击某个节点时,高亮从根节点到该节点的路径。
小结
合成树页面的核心是递归——递归构建数据结构,递归渲染 UI。这是处理树形数据的标准方法。
关键技术点:
- 递归函数:用递归遍历合成关系,构建树形数据
- 深度参数:记录每个节点的深度,用于计算缩进
- 递归渲染:用递归函数渲染树节点,处理任意深度的树
- 视觉层级:用缩进和竖线表示层级关系
下一篇我们来实现装备筛选功能,让用户可以按分类和属性筛选装备。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)