当前位置: 首页 > news >正文

鸿蒙原生应用实战(二):游戏库列表与筛选排序 — 卡片式UI设计

鸿蒙原生应用实战(二):游戏库列表与筛选排序 — 卡片式UI设计

一、前言

上一篇我们完成了项目搭建和首页开发。本篇聚焦GameListPage(游戏库页面)的开发,这是 App 的核心浏览页面。我们将实现:

  • 多标签状态筛选(全部/在玩/通关/想玩)
  • 循环排序切换(最近/名称/评分/时长)
  • 游戏卡片 UI 设计
  • 动态进度条
  • 接收首页路由参数

二、页面功能概览

GameListPage 的功能架构如下:

┌─────────────────────────────────┐ │ ← 返回 📋 游戏库 🔍 搜索 │ ← Header ├─────────────────────────────────┤ │ [全部] [在玩] [通关] [想玩] │ ← 筛选标签(横向滚动) ├─────────────────────────────────┤ │ 排序: 最近 ▼ 共5款 │ ← 排序行 ├─────────────────────────────────┤ │ ┌───────────────────────────┐ │ │ │ 🎮 ┌────────────────────┐ │ │ │ │ │ 艾尔登法环 49 │ │ │ │ │ │ PC · 动作RPG │ │ │ │ │ │ [通关] 186h │ │ │ │ │ │ ████████░░ 100% │ │ │ │ │ └────────────────────┘ │ │ ← 游戏卡片列表 │ └───────────────────────────┘ │ │ ┌───────────────────────────┐ │ │ │ 🎮 塞尔达传说... 48 │ │ │ │ Switch · 动作冒险 │ │ │ │ [在玩] 72h │ │ │ │ █████░░░░░ 55% │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘

三、数据模型

为了更好地区分,我们在列表页定义独立的GameItem接口:

interfaceGameItem{id:number;title:string;platform:string;genre:string;status:string;// 通关 / 在玩 / 想玩rating:number;// 媒体评分(0-49)hours:number;// 游玩时长progress:number;// 进度百分比(0-100)coverColor:string;// 封面颜色代码}

四、核心实现

4.1 组件结构与状态定义

@Entry@Componentstruct GameListPage{@Stategames:GameItem[]=[];@Statefilter:string='全部';// 当前筛选状态@Statefilters:string[]=['全部','在玩','通关','想玩'];@StatesortBy:string='最近';// 当前排序方式aboutToAppear():void{// 接收首页传来的筛选参数constparams=router.getParams()asRecord<string,Object>;if(params&&params['filter']!==undefined){this.filter=params['filter']asstring;}this.games=[/* 游戏数据表 */];}}

4.2 接收路由参数 ⭐

这是连接首页筛选标签和列表页的关键:

aboutToAppear():void{constparams:Record<string,Object>=router.getParams()asRecord<string,Object>;if(params&&params['filter']!==undefined){this.filter=params['filter']asstring;}this.games=[/* ... */];}

首页跳转代码(来自 Index.ets):

.onClick(()=>{router.pushUrl({url:'pages/GameListPage',params:{filter:'通关'}});})

4.3 筛选与排序逻辑

筛选函数
getFilteredGames():GameItem[]{if(this.filter==='全部'){returnthis.games;}returnthis.games.filter((g:GameItem)=>g.status===this.filter);}
排序循环切换

我们实现了一个循环切换的逻辑,每次点击切换到下一种排序方式:

@BuilderbuildSortRow(){Row(){Text('排序:').fontSize(12).fontColor('#999999')Row(){Text(this.sortBy).fontSize(12).fontColor('#FF6B35')Text(' ▼').fontSize(10).fontColor('#FF6B35')}.padding({left:8,right:10,top:3,bottom:3}).backgroundColor('#FFF0E8').borderRadius(10).margin({left:6}).onClick(()=>{constsorts:string[]=['最近','名称','评分','时长'];constidx:number=sorts.indexOf(this.sortBy);this.sortBy=sorts[(idx+1)%sorts.length];// TODO: 实际项目中在此处调用排序函数})Blank()Text(`${this.getFilteredGames().length}`).fontSize(12).fontColor('#999999')}.width('100%').padding({left:16,right:16,top:8})}

设计思路:采用索引取模(idx + 1) % length的方式实现循环切换,比 if-else 链更简洁优雅。

4.4 筛选标签 — 带状态的高亮

@BuilderbuildFilters(){Scroll(){Row(){ForEach(this.filters,(f:string)=>{Text(f).fontSize(13).fontColor(this.filter===f?'#FFFFFF':'#666666').padding({left:16,right:16,top:6,bottom:6}).backgroundColor(this.filter===f?'#FF6B35':'#F0F0F0').borderRadius(16).margin({right:8}).onClick(()=>{this.filter=f;})},(f:string)=>f)}.padding({left:16})}.scrollable(ScrollDirection.Horizontal).height(40)}

交互细节

  • 选中标签:橙色背景 + 白色文字(#FF6B35/#FFFFFF
  • 未选中标签:浅灰背景 + 深灰文字(#F0F0F0/#666666
  • 点击即切换this.filter,ArkTS 自动触发getFilteredGames()重渲染

4.5 游戏卡片 UI — 装饰器模式

这是整个页面最核心的 UI 组件。我们使用@Builder将卡片封装为一个可复用的构建函数:

@BuilderbuildGameCard(game:GameItem){Row(){// 左侧:封面色块 + emojiStack(){Column().width(60).height(80).borderRadius(8).backgroundColor(game.coverColor).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)Text('🎮').fontSize(26)}.width(60).height(80)// 右侧:文字信息Column(){// 第一行:标题 + 评分Row(){Text(game.title).fontSize(15).fontWeight(FontWeight.Medium).fontColor('#1A1A2E').layoutWeight(1)if(game.rating>0){Text(game.rating.toString()).fontSize(12).fontWeight(FontWeight.Bold).fontColor(game.rating>=48?'#E74C3C':'#F39C12')}}.width('100%')// 第二行:平台 + 类型Text(`${game.platform}·${game.genre}`).fontSize(12).fontColor('#999999').margin({top:4})// 第三行:状态标签 + 时长Row(){Text(game.status).fontSize(11).fontColor(Color.White).padding({left:6,right:6,top:2,bottom:2}).backgroundColor(game.status==='通关'?'#2ECC71':game.status==='在玩'?'#3498DB':'#9B59B6').borderRadius(6)if(game.hours>0){Text(`${game.hours}h`).fontSize(11).fontColor('#BBBBBB').margin({left:8})}}.margin({top:4})// 进度条(仅进行中且未完成时显示)if(game.progress>0&&game.progress<100){Progress({value:game.progress,total:100,style:ProgressStyle.Linear}).width('80%').height(4).value(game.progress).color('#FF6B35').backgroundColor('#F0F0F0').borderRadius(2).margin({top:4})}}.alignItems(HorizontalAlign.Start).margin({left:10}).layoutWeight(1)}.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(10).margin({top:8,left:16,right:16}).onClick(()=>{router.pushUrl({url:'pages/GameDetailPage',params:{gameId:game.id}})})}

4.6 卡片设计详解

(1) 封面色块

每个游戏分配一个独特的十六进制颜色代码,作为封面的替代方案:

游戏色值寓意
艾尔登法环#FFD700黄金树的金色
塞尔达传说#2ECC71海拉鲁的翠绿
博德之门3#E74C3C夺心魔的深红
赛博朋克2077#3498DB夜之城的霓虹蓝

这种方案避免了实际截图的资源占用,同时通过颜色传递游戏氛围。

(2) 评分颜色分级
game.rating>=48?'#E74C3C':'#F39C12'
  • 评分 ≥ 48(满分 50):红色高亮,表示顶级神作
  • 评分 < 48:橙色,表示优质作品
(3) 状态标签颜色编码
通关 →绿色(#2ECC71)🟢 已完成 在玩 →蓝色(#3498DB)🔵 进行中 想玩 →紫色(#9B59B6)🟣 待开始
(4) 条件渲染进度条

仅当0 < progress < 100时显示进度条。通关游戏(100%)不显示进度条,避免视觉冗余:

if(game.progress>0&&game.progress<100){Progress({value:game.progress,total:100,style:ProgressStyle.Linear}).width('80%').height(4).color('#FF6B35').backgroundColor('#F0F0F0')}

ArkTS 的Progress组件支持线性样式,自带动画效果。

4.7 组装页面

build():void{Column(){this.buildHeader()// 固定头部this.buildFilters()// 可滚动筛选标签this.buildSortRow()// 排序行Scroll(){Column(){ForEach(this.getFilteredGames(),(game:GameItem)=>{this.buildGameCard(game)},(game:GameItem)=>game.id.toString()+this.filter)}.width('100%').padding({bottom:30})}.scrollable(ScrollDirection.Vertical).layoutWeight(1).width('100%')}.width('100%').height('100%').backgroundColor('#F5F5F5')}

关于 key 的思考
在 ForEach 的 key 生成器中,我们使用了game.id.toString() + this.filter。这样做的好处是:

  • 筛选切换时,key 变化会触发列表完全重建,确保筛选后的数据正确渲染
  • 避免仅用game.id时,ArkTS 的 diff 机制保留已删除 DOM 节点的问题

五、进阶话题:ForEach 的复用策略

ArkTS 的ForEach基于 key 进行 diff 更新。理解 key 策略对性能至关重要:

Key 策略行为适用场景
唯一且稳定 (如id)尽量复用已有组件,只更新数据数据不增减的静态列表
包含筛选条件 (如id+filter)筛选变化时重建全部筛选条件变化需要重新布局
索引 (index)紧耦合于位置,慎用!不推荐用于可排序列表

在我们的场景中,筛选切换需要卡片布局完全刷新,因此使用id + filter作为复合 key。


六、ArkTS 严格模式避坑

6.1 对象字面量类型声明

// ❌ 错误:arkts-no-untyped-obj-literalsRow(){Text('通关').onClick(()=>{})}// ✅ 正确:将对象字面量提取为类型变量interfaceFilterItem{label:string;key:string;}constfilterItems:FilterItem[]=[{label:'全部',key:'all'}];

6.2 数组字面量类型推断

// ❌ 错误:arkts-no-noninferrable-arr-literalsconstsorts=['最近','名称','评分','时长'];// ✅ 正确:显式声明constsorts:string[]=['最近','名称','评分','时长'];

6.3 Filter 回调类型标注

// ✅ 必须显式声明参数类型this.games.filter((g:GameItem)=>g.status===this.filter)

七、Header 的设计

@BuilderbuildHeader(){Row(){Text('←').fontSize(20).fontColor('#333333').onClick(()=>{router.back();})Blank()Text('📋 游戏库').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')Blank()Text('🔍').fontSize(18)}.width('100%').padding({left:16,right:16,top:12,bottom:12}).backgroundColor('#FFFFFF')}

左侧返回按钮调用router.back()回到首页;右侧搜索图标为后续功能预留。


八、小结

本篇我们完成了:

  1. ✅ 游戏库列表页完整开发
  2. ✅ 多标签筛选 + 循环排序切换
  3. ✅ 卡片式 UI 设计(封面色块、评分、进度条)
  4. ✅ 路由参数接收与 ForEach 复用策略
  5. ✅ ArkTS 严格模式常见问题解决

下一篇将深入游戏详情页,实现全屏 Header、状态切换、星级评分、成就系统和用户评测等丰富交互功能。


系列目录

  • 第一篇:项目搭建与首页开发
  • 第二篇:游戏库列表与筛选排序(本文)
  • 第三篇:游戏详情页与交互功能
  • 第四篇:愿望单与个人统计
  • 第五篇:路由导航与工程优化
http://www.jsqmd.com/news/1004259/

相关文章:

  • 基于Osip的Windows SIP通信双工程示例:发送INVITE/REGISTER与接收响应一体化封装
  • 2026番禺区新造下水道疏通技术办案逻辑解析:居顺联疏通服务深耕本地厨卫下水疏通 - 居顺联家政疏通
  • Vue 3 中的事件监听问题及解决方案
  • 2026年杭州软考中级系统集成报名费用资料怎么确认?众智商学院官网400冯老师 - 众智商学院官方
  • HLS性能翻倍的秘密:深入解读`array_partition`、`pipeline`与`dataflow`三大优化指令(附Vitis HLS 2023.2实测数据)
  • 微信小程序蓝牙开发避坑实录:从连接失败到数据收发,我踩过的那些坑
  • ArcGIS地统计向导实战:用普通克里金法预测石家庄房价(附趋势剔除与Log变换技巧)
  • 【郴州同城黄金回收服务 | 鑫诚黄金回收】 - 润富黄金回收
  • 2026年射洪装修公司怎么选?从本地经验、材料体系到售后保障的多维度分析 - 优质品牌商家
  • 读UNIX传奇:历史与回忆01贝尔实验室
  • LLM工程落地五大关键技术闭环解析
  • 大功率工业吸尘器十大品牌2026排名,第一名实至名归 - 工业清洁测评社
  • 【郴州同城黄金回收服务 | 鑫盛鑫诚万金汇联合回收指南】 - 润富黄金回收
  • 科研绘图效率翻倍:用ArcGIS+AI组合拳,5分钟搞定论文地图的精修与排版
  • 告别版本兼容烦恼:用Python mikeio 1.x新版搞定ERA5风场转MIKE21 dfs2文件
  • 别再死记硬背了!用这个可视化工具,5分钟搞懂‘图序列’判定定理
  • 2026年安丘市黄金回收白银回收铂金回收彩金回收 地址联系大全+支持现场结算无套路 - 前途无量YY
  • 2026济南历下蒂芙尼回收|弄懂估价逻辑,出手首饰少花冤枉钱 - 逸程
  • 别再让3D模型拖慢你的网页了!Three.js + Blender纹理烘焙实战避坑指南
  • 新服务器买完 24 小时内要做什么?安全加固清单
  • 保姆级教程:从零搭建Scrcpy Server端调试环境(基于Android Studio与ADB)
  • 3步解锁NVIDIA显卡隐藏性能:Profile Inspector完全指南
  • 2026年安顺市黄金回收白银回收铂金回收彩金回收 地址联系大全+支持现场结算无套路 - 前途无量YY
  • 2026年洛阳SCMP供应链管理专家课程咨询怎么确认?众智商学院官网400和冯老师 - 众智商学院官方
  • 【郴州同城黄金回收服务 | 北湖苏仙黄金回收门店全收录】 - 润富黄金回收
  • SQL原生机器学习:用SELECT语句完成建模与预测
  • 【郴州同城黄金回收,鑫盛黄金回收】 - 润富黄金回收
  • 别再死记硬背正则了!用Flex搞定PL语言词法分析,这份.l文件配置清单请收好
  • 重庆杨家坪黄金回收横评|诚鑫名品联盟等6家商家解析 - 诚鑫名品
  • 重庆及周边二手接触器断路器回收服务商实测对比评测 - 优质品牌商家