RN for OpenHarmony英雄联盟助手App实战:装备筛选实现
本文介绍了英雄联盟装备筛选页面的实现,重点是多维度筛选逻辑和交互设计。装备筛选包含分类和价格两个维度:分类包括攻击、防御、法术等6种类型;价格分为<1000、1000-2500、>2500三档,符合游戏不同阶段的出装需求。文章详细讲解了链式筛选的实现方法(使用useMemo优化性能)、条件样式的处理技巧,以及通过遍历渲染提高代码复用性的实践。该设计让玩家能快速找到符合当前游戏阶段和英雄

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_lol
装备列表页已经有了分类筛选,但有时候用户想要更精细的筛选条件,比如"我只想看 1000 金币以下的攻击装备"。装备筛选页提供分类和价格两个维度的筛选,让用户能更精准地找到目标装备。
这篇文章我们来实现装备筛选页,重点是多维度筛选的组合逻辑、价格区间的设计思路、以及筛选按钮的交互状态管理。
筛选维度的设计思考
在设计筛选功能之前,我们需要思考用户真正需要什么样的筛选条件。
分类筛选
装备的分类是最直观的筛选维度。英雄联盟的装备按属性分成多个类别:
- 攻击类:提供攻击力、暴击等属性
- 防御类:提供护甲、魔抗、生命值等属性
- 法术类:提供法术强度、法力值等属性
- 攻速类:提供攻击速度
- 移速类:提供移动速度
- 吸血类:提供生命偷取
玩家通常会根据自己的英雄定位来选择装备类型。比如 AD 刺客会关注攻击类装备,坦克会关注防御类装备。
价格筛选
价格是装备筛选特有的维度,这个维度在英雄筛选中是没有的。为什么价格对装备筛选很重要?
游戏节奏的影响:英雄联盟的对局分为前期、中期、后期。前期金币少,只能买便宜的基础装备;中期有一定经济,可以合成中等价位的装备;后期经济充裕,才能买得起昂贵的成型装备。
出装顺序的规划:玩家在规划出装路线时,需要知道每件装备的价格,才能合理安排购买顺序。
基于这些考虑,我们把价格分成三档:
const priceRanges = [
{key: 'All', label: '全部', min: 0, max: 99999},
{key: 'Low', label: '< 1000', min: 0, max: 999},
{key: 'Medium', label: '1000-2500', min: 1000, max: 2500},
{key: 'High', label: '> 2500', min: 2501, max: 99999},
];
这个分档是根据游戏经验设定的:
- 1000 以下:基础装备和小件,对线期就能买,比如长剑(350)、布甲(300)
- 1000-2500:中等装备,中期过渡用,比如塞瑞尔达的怨恨(2600 刚好超出,但很多中等装备在这个区间)
- 2500 以上:成型装备,后期核心,比如无尽之刃(3400)、兰顿之兆(2700)
页面结构与状态管理
import React, {useState, useMemo} from 'react';
import {View, Text, TouchableOpacity, StyleSheet} from 'react-native';
import {colors} from '../../styles/colors';
import {useApp} from '../../context/AppContext';
import {useNavigation} from '../../context/NavigationContext';
import {itemApi} from '../../api';
import {ItemGrid} from '../../components/item';
import type {Item} from '../../models/Item';
这个页面需要的依赖比较简单,主要是状态管理相关的 Hook 和 UI 组件。
export function ItemFilterPage() {
const {state} = useApp();
const {navigate} = useNavigation();
const [selectedTag, setSelectedTag] = useState('All');
const [selectedPrice, setSelectedPrice] = useState('All');
const categories = itemApi.getItemCategories();
状态设计说明:
selectedTag:当前选中的分类,默认 ‘All’ 表示不限制分类selectedPrice:当前选中的价格区间,默认 ‘All’ 表示不限制价格
两个状态都默认为 ‘All’,这样用户进入页面时会看到所有装备,然后根据需要逐步缩小范围。这种"从全量到精确"的筛选体验比较符合用户习惯。
categories 是分类选项列表,从 API 模块获取。把这个数据放在 API 模块而不是硬编码在页面中,是为了方便统一管理和后续修改。
筛选逻辑的实现
筛选逻辑是这个页面的核心,我们用 useMemo 来优化性能:
const filteredItems = useMemo(() => {
let result = state.items;
// 按分类筛选
if (selectedTag !== 'All') {
result = itemApi.filterItemsByTag(result, selectedTag);
}
// 按价格筛选
if (selectedPrice !== 'All') {
const priceRange = priceRanges.find(p => p.key === selectedPrice);
if (priceRange) {
result = result.filter(
item =>
(item.gold?.total || 0) >= priceRange.min &&
(item.gold?.total || 0) <= priceRange.max,
);
}
}
return result;
}, [state.items, selectedTag, selectedPrice]);
链式筛选的工作原理:
- 从全量数据
state.items开始 - 如果选择了分类,先按分类过滤
- 在分类过滤的结果上,再按价格过滤
- 返回最终结果
这种链式筛选意味着两个条件是**"与"的关系**——装备必须同时满足分类条件和价格条件才会显示。
为什么用 useMemo?
装备列表有 200+ 条数据,每次筛选都要遍历整个数组。如果不用 useMemo,每次组件渲染都会重新计算,即使筛选条件没有变化。useMemo 会缓存计算结果,只有当依赖项(state.items、selectedTag、selectedPrice)变化时才重新计算。
价格筛选的细节处理:
(item.gold?.total || 0) >= priceRange.min
这里用了可选链 ?. 和空值合并 || 0。有些装备的 gold 字段可能为空(比如一些特殊装备),这样处理可以避免报错,同时把没有价格的装备当作 0 金币处理。
分类筛选区域的渲染
const FilterHeader = (
<View style={styles.filterContainer}>
{/* 分类筛选 */}
<View style={styles.filterSection}>
<Text style={styles.filterTitle}>分类</Text>
<View style={styles.filterOptions}>
{categories.map(option => (
<TouchableOpacity
key={option.key}
style={[
styles.filterBtn,
selectedTag === option.key && styles.filterBtnActive,
]}
onPress={() => setSelectedTag(option.key)}>
<Text
style={[
styles.filterBtnText,
selectedTag === option.key && styles.filterBtnTextActive,
]}>
{option.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
条件样式的实现技巧:
style={[styles.filterBtn, selectedTag === option.key && styles.filterBtnActive]}
这是 React Native 中实现条件样式的常用模式。style 属性接受一个数组,数组中的样式会按顺序合并。当 selectedTag === option.key 为 true 时,filterBtnActive 的样式会覆盖 filterBtn 中的同名属性。
遍历渲染的好处:
用 categories.map() 遍历渲染按钮,而不是手写每个按钮,有几个好处:
- 代码简洁:不需要重复写 7 个几乎一样的按钮
- 易于维护:要加新分类只需要改 categories 数组
- 一致性:所有按钮的结构和行为完全一致
价格筛选区域的渲染
{/* 价格筛选 */}
<View style={styles.filterSection}>
<Text style={styles.filterTitle}>价格</Text>
<View style={styles.filterOptions}>
{priceRanges.map(option => (
<TouchableOpacity
key={option.key}
style={[
styles.filterBtn,
selectedPrice === option.key && styles.filterBtnActive,
]}
onPress={() => setSelectedPrice(option.key)}>
<Text
style={[
styles.filterBtnText,
selectedPrice === option.key && styles.filterBtnTextActive,
]}>
{option.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
价格筛选的渲染逻辑和分类筛选完全一样,只是数据源和状态变量不同。这种结构上的一致性让代码更容易理解和维护。
结果统计的展示
{/* 结果统计 */}
<View style={styles.resultInfo}>
<Text style={styles.resultText}>
找到 {filteredItems.length} 件装备
</Text>
</View>
</View>
);
结果统计是一个很重要的反馈信息,它告诉用户:
- 筛选是否生效:如果数量变少了,说明筛选条件起作用了
- 是否需要调整:如果显示"找到 0 件装备",用户就知道条件太严格了
- 当前范围大小:帮助用户判断是否需要进一步筛选
页面整体渲染
const handleItemPress = (item: Item) => {
navigate('ItemDetail', {itemId: item.id});
};
return (
<View style={styles.container}>
<ItemGrid
items={filteredItems}
version={state.version}
onItemPress={handleItemPress}
ListHeaderComponent={FilterHeader}
/>
</View>
);
}
复用 ItemGrid 组件展示筛选结果。ListHeaderComponent 属性让筛选区域显示在列表顶部,并且会随列表一起滚动。
样式设计详解
筛选按钮的样式
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
filterContainer: {
marginBottom: 8,
},
filterSection: {
marginBottom: 16,
},
filterTitle: {
fontSize: 14,
fontWeight: '600',
color: colors.textPrimary,
marginBottom: 8,
},
filterOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
marginHorizontal: -4,
},
filterOptions 的布局技巧:
flexDirection: 'row':让按钮横向排列flexWrap: 'wrap':允许换行,当一行放不下时自动换到下一行marginHorizontal: -4:抵消按钮的外边距,让第一个按钮和最后一个按钮与容器边缘对齐
按钮的两种状态
filterBtn: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
backgroundColor: colors.backgroundCard,
margin: 4,
borderWidth: 1,
borderColor: colors.border,
},
filterBtnActive: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
filterBtnText: {
fontSize: 13,
color: colors.textSecondary,
},
filterBtnTextActive: {
color: colors.background,
fontWeight: '600',
},
未选中状态:灰色背景、灰色边框、灰色文字,视觉上比较低调
选中状态:金色背景、金色边框、深色文字并加粗,非常醒目
这种对比让用户一眼就能看出哪些条件被选中了。borderRadius: 16 配合较小的高度形成"胶囊"形状,这是移动端常见的按钮设计。
结果统计的样式
resultInfo: {
paddingVertical: 8,
borderTopWidth: 1,
borderTopColor: colors.border,
},
resultText: {
fontSize: 14,
color: colors.textSecondary,
},
});
结果统计区域上方有一条分隔线,把它和筛选按钮在视觉上分开。文字用次要颜色,不抢筛选按钮的风头。
交互优化建议
当前实现已经能满足基本需求,但还有一些可以优化的地方:
重置按钮:当用户选了多个筛选条件后,可能想一键清除所有条件。可以在结果统计旁边加一个"重置"按钮:
<TouchableOpacity onPress={() => {
setSelectedTag('All');
setSelectedPrice('All');
}}>
<Text style={styles.resetText}>重置</Text>
</TouchableOpacity>
筛选条件的持久化:用户离开页面再回来,筛选条件会重置。如果想保持用户的选择,可以用 AsyncStorage 存储。
排序功能:在筛选的基础上加排序功能,比如按价格从低到高、从高到低排序。
空结果的友好提示:当筛选结果为空时,显示一个更友好的提示,引导用户放宽条件。
小结
装备筛选页展示了多维度筛选的实现方法。核心要点:
- 状态分离:每个筛选维度用独立的状态管理
- 链式筛选:多个条件依次过滤,形成"与"的关系
- useMemo 优化:缓存筛选结果,避免不必要的重复计算
- 条件样式:用数组合并样式,实现选中/未选中的视觉区分
下一篇我们来实现装备搜索功能,通过输入关键词快速找到目标装备。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)