鸿蒙原生应用实战(四):成就系统与排行榜开发 — 数据展示与交互进阶

鸿蒙原生应用实战(四):成就系统与排行榜开发 --- 数据展示与交互进阶

前言

经过前三篇的开发,我们的数独游戏已经具备了完整的游戏体验。本篇将为游戏添加激励机制------成就系统和排行榜。这两个页面将显著提升用户粘性和游戏趣味性。

本篇重点:

  • 成就系统的数据结构设计与分类筛选
  • 进度条组件的 ArkTS 实现
  • 排行榜的多难度切换
  • 列表渲染性能优化
  • 条件渲染的最佳实践

一、成就系统 --- AchievementsPage

成就系统是游戏化设计的核心。我们设计了 12 个成就,分为 3 个类别。

1.1 数据结构设计

typescript 复制代码
interface Achievement {
  id: number;           // 唯一标识
  title: string;        // 成就名称
  description: string;  // 成就描述
  icon: string;         // 表情图标
  unlocked: boolean;    // 是否已解锁
  progress: number;     // 当前进度
  maxProgress: number;  // 目标进度
  category: string;     // 分类
}

1.2 12 个成就的完整设计

成就分为三大类:

局数成就(5个):鼓励持续游戏

成就 条件 难度
🎯 初次挑战 完成第1局
🔰 小试牛刀 完成10局 ⭐⭐
⭐ 渐入佳境 完成50局 ⭐⭐⭐
🌟 数独达人 完成100局 ⭐⭐⭐⭐
🔥 坚持不懈 连续7天每日挑战 ⭐⭐⭐

速度成就(3个):鼓励快速解题

成就 条件 难度
⚡ 闪电手 简单5分钟内完成 ⭐⭐
💨 快如风 中等10分钟内完成 ⭐⭐⭐
🏆 极速传说 困难15分钟内完成 ⭐⭐⭐⭐

特殊成就(4个):鼓励挑战自我

成就 条件 难度
💎 完美无缺 不使用提示完成一局 ⭐⭐⭐
🎯 零失误 不擦除完成一局 ⭐⭐⭐⭐
📝 笔记大师 笔记模式下完成一局 ⭐⭐
👑 全通挑战 完成全部难度每日挑战 ⭐⭐⭐⭐⭐

1.3 计算属性

typescript 复制代码
// 按分类筛选
get filteredAchievements(): Achievement[] {
  if (this.selectedCategory === 0) return this.achievements;
  return this.achievements.filter(
    a => a.category === this.categories[this.selectedCategory]
  );
}

// 已解锁计数
get unlockedCount(): number {
  let count = 0;
  for (let a of this.achievements) {
    if (a.unlocked) count++;
  }
  return count;
}

1.4 进度条组件详解

进度条是用 ArkTS 的 overlay 特性实现的:

typescript 复制代码
Row() {
  Column()
    .width('100%')                // 灰色背景条(满宽)
    .height(8)
    .backgroundColor('#FFF0F0F0')
    .borderRadius(4)
    .overlay(() => {
      Column()
        .width((this.unlockedCount / this.achievements.length) * 100 + '%')  // 百分比宽度
        .height(8)
        .backgroundColor($r('app.color.primary'))
        .borderRadius(4)
    })
}

overlay 是 ArkTS 的一个强大特性------它可以让一个组件"叠加"在另一个组件之上,且不改变布局流。这里相当于:底层是灰色背景条,上层是动态宽度的彩色填充条。

1.5 分类标签栏

typescript 复制代码
@State selectedCategory: number = 0;
private categories: string[] = ['全部', '局数成就', '速度成就', '特殊成就'];

Row() {
  ForEach(this.categories, (cat: string, index: number) => {
    Text(cat)
      .fontSize($r('app.float.small_font_size'))
      .fontColor(this.selectedCategory === index ? Color.White : $r('app.color.text_primary'))
      .backgroundColor(this.selectedCategory === index ? $r('app.color.primary') : $r('app.color.background'))
      .padding({ left: 14, right: 14, top: 6, bottom: 6 })
      .borderRadius(16)
      .margin({ right: 4 })
      .onClick(() => { this.selectedCategory = index; })
  }, (cat: string) => cat)
}

这种胶囊标签设计在移动应用中很流行:

  • borderRadius(16) 产生胶囊形状
  • 点按时切换 selectedCategory,触发 UI 刷新
  • ForEach 的第三个参数是键值生成器,用于列表 diff 优化

1.6 成就列表渲染

typescript 复制代码
List() {
  ForEach(this.filteredAchievements, (ach: Achievement) => {
    ListItem() {
      Column() {
        Row() {
          Text(ach.icon).fontSize(36)
            .opacity(ach.unlocked ? 1 : 0.3)

          Column() {
            Text(ach.title)
              .fontColor(ach.unlocked ? $r('app.color.text_primary') : $r('app.color.text_hint'))
            Text(ach.description)
              .fontSize($r('app.float.small_font_size'))
              .fontColor($r('app.color.text_hint'))
              .margin({ top: 2 })
          }
          .margin({ left: 12 })
          .layoutWeight(1)

          Text(ach.unlocked ? '✓' : '🔒')
            .fontColor(ach.unlocked ? $r('app.color.status_delivered') : $r('app.color.text_hint'))
        }
        .width('100%')

        // 未解锁显示进度条
        if (!ach.unlocked) {
          Row() { /* 进度条 */ }.width('100%').margin({ top: 8 })
          Text('进度: ' + ach.progress + '/' + ach.maxProgress)
            .fontSize(11)
            .fontColor($r('app.color.text_hint'))
        }
      }
      .padding(16)
      .backgroundColor($r('app.color.card_bg'))
      .borderRadius($r('app.float.card_corner_radius'))
    }
    .padding({ left: 16, right: 16, top: 4, bottom: 4 })
  }, (ach: Achievement) => ach.id.toString())
}
.width('100%')
.layoutWeight(1)

1.7 条件渲染详解

ArkTS 的条件渲染使用 if 指令,与 TypeScript/JavaScript 类似但语法略有不同:

typescript 复制代码
if (!ach.unlocked) {
  // 这些内容只在未解锁时渲染
  Row() { /* progress bar */ }
  Text('进度: ' + ach.progress + '/' + ach.maxProgress)
}

要点

  • if 直接在组件树中使用,不需要大括号包裹
  • 条件不满足时,内部组件树完全不会创建(不是隐藏)
  • 这比 CSS 的 display: none 更高效

已完成 vs 未完成视觉对比

状态 图标透明度 标题颜色 右侧标记 额外内容
已解锁 1(正常) 深色 绿色 ✓
未解锁 0.3(半透明) 灰色 🔒 进度条 + 进度文字

二、排行榜 --- LeaderboardPage

2.1 数据结构

typescript 复制代码
interface RankEntry {
  rank: number;       // 排名
  name: string;       // 玩家名称
  difficulty: string; // 难度
  time: string;       // 用时 (MM:SS)
}

2.2 分难度数据管理

三种难度各有 5 条排名数据,通过 selectedDifficulty 索引切换:

typescript 复制代码
@State selectedDifficulty: number = 0;
private difficulties: string[] = ['简单', '中等', '困难'];

@State easyRanks: RankEntry[] = [
  { rank: 1, name: '张三', difficulty: '简单', time: '03:25' },
  { rank: 2, name: '李四', difficulty: '简单', time: '04:10' },
  { rank: 3, name: '王五', difficulty: '简单', time: '05:48' },
  { rank: 4, name: '赵六', difficulty: '简单', time: '06:32' },
  { rank: 5, name: '陈七', difficulty: '简单', time: '08:15' }
];
// mediumRanks / hardRanks 类似...

get currentRanks(): RankEntry[] {
  if (this.selectedDifficulty === 0) return this.easyRanks;
  if (this.selectedDifficulty === 1) return this.mediumRanks;
  return this.hardRanks;
}

2.3 排名显示的高级技巧

前三名用奖杯表情,其余显示数字:

typescript 复制代码
Text(entry.rank <= 3 
  ? ['🥇', '🥈', '🥉'][entry.rank - 1] 
  : entry.rank.toString()
)
.fontSize(20)
.width(40)
.textAlign(TextAlign.Center)

这个一行的三元表达式实现了:第 1 名 🥇 → 第 2 名 🥈 → 第 3 名 🥉 → 第 4+ 名数字。

2.4 排行榜完整列表项

typescript 复制代码
ListItem() {
  Row() {
    // 排名(奖杯或数字)
    Text(entry.rank <= 3 ? ['🥇', '🥈', '🥉'][entry.rank - 1] : entry.rank.toString())
      .fontSize(20)
      .width(40)
      .textAlign(TextAlign.Center)

    // 玩家信息
    Column() {
      Text(entry.name)
        .fontSize($r('app.float.body_font_size'))
        .fontWeight(FontWeight.Medium)
        .fontColor($r('app.color.text_primary'))
      Text(entry.difficulty)
        .fontSize($r('app.float.small_font_size'))
        .fontColor($r('app.color.text_hint'))
    }
    .margin({ left: 12 })

    Blank()

    // 用时
    Column() {
      Text($r('app.string.rank_time'))
        .fontSize($r('app.float.small_font_size'))
        .fontColor($r('app.color.text_hint'))
      Text(entry.time)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.primary'))
        .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.End)
  }
  .width('100%')
  .padding($r('app.float.padding_medium'))
  .backgroundColor($r('app.color.card_bg'))
  .borderRadius($r('app.float.card_corner_radius'))
}

布局分析 :每一行由三段组成------左侧排名(固定宽度 40vp)、中间玩家信息(弹性空间)、右侧用时(右对齐),Blank() 把左右两部分撑开。

2.5 难度标签切换

typescript 复制代码
Row() {
  ForEach(this.difficulties, (diff: string, index: number) => {
    Text(diff)
      .fontSize($r('app.float.body_font_size'))
      .fontWeight(this.selectedDifficulty === index ? FontWeight.Bold : FontWeight.Regular)
      .fontColor(this.selectedDifficulty === index ? Color.White : $r('app.color.text_primary'))
      .backgroundColor(this.selectedDifficulty === index ? $r('app.color.primary') : $r('app.color.background'))
      .padding({ left: 20, right: 20, top: 8, bottom: 8 })
      .borderRadius(20)
      .onClick(() => { this.selectedDifficulty = index; })
      .margin({ right: 8 })
  }, (diff: string) => diff)
}

这里比成就系统的标签多了一个 fontWeight 变化------选中时加粗,视觉反馈更明显。


三、ForEach 与列表渲染优化

3.1 ForEach 的第三个参数:键值生成器

typescript 复制代码
ForEach(
  this.filteredAchievements,        // 数据源
  (item: Type) => { /* 渲染函数 */ }, // 渲染逻辑
  (item: Type) => item.id.toString()  // 键值生成器
)

第三个参数键值生成器非常重要:

  • 帮助 ArkTS 框架进行高效的列表 diff 比较
  • 当数据变化时,只重新渲染变化的项,而不是整个列表
  • 键值应该是唯一且稳定 的,一般用 id.toString()

3.2 List + ListItem 的性能优势

为什么用 List + ListItem 而不是 Column + ForEach

typescript 复制代码
// ✅ 推荐:List + ListItem(虚拟滚动)
List() {
  ForEach(items, (item) => {
    ListItem() { /* 内容 */ }
  })
}

// ❌ 不推荐:Column + ForEach(全量渲染)
Column() {
  ForEach(items, (item) => {
    Column() { /* 内容 */ }
  })
}

List 组件支持虚拟滚动------只渲染屏幕可见区域内的列表项,数据量大时性能优势极其明显。我们的成就列表只有 12 项,差异不明显,但在排行榜扩展到 50+ 项时就能感受到差距。


四、@State 的角色------数据驱动 UI

在全篇中,所有页面的交互都遵循"数据驱动 UI"的模式:

复制代码
用户操作 → 修改 @State 变量 → 框架自动更新 UI

这个模式的核心在于:

  1. 声明式:你声明 UI 和数据的关系,不需要手动操作 DOM
  2. 不可变触发@State 变量引用变化时会触发重渲染
  3. 最小更新:框架只更新变化的部分

在我们的成就系统中:

typescript 复制代码
@State selectedCategory: number = 0;

// 用户点击分类标签
.onClick(() => { this.selectedCategory = index; })

// 模板中自动响应
ForEach(this.filteredAchievements, ...)  // filteredAchievements 依赖 selectedCategory

修改 selectedCategoryfilteredAchievements 重新计算 → ForEach 比较新旧列表 → 仅更新变化的列表项。全链路自动完成。


五、页面路由导航

两个页面都实现了统一的导航方式:

成就页面进入:目前通过路由直接跳转,可在首页或游戏中添加入口按钮:

typescript 复制代码
router.pushUrl({ url: 'pages/AchievementsPage' });

排行榜页面:已在首页底部提供入口:

typescript 复制代码
Row() {
  Text('🏆 ')
  Text($r('app.string.title_leaderboard'))
}
.onClick(() => { router.pushUrl({ url: 'pages/LeaderboardPage' }); })

返回上一页:两个页面都实现了统一的返回按钮:

typescript 复制代码
Button('←')
  .fontSize(22)
  .fontColor($r('app.color.text_primary'))
  .backgroundColor(Color.Transparent)
  .onClick(() => { router.back(); })

六、完整页面效果

AchievementsPage 功能清单

功能 实现方式 交互效果
进度总览 unlockedCount 计算属性 + overlay 进度条 顶部卡片展示已解锁/总数
分类筛选 selectedCategory @State + filter 4 个胶囊标签切换
成就列表 List + ForEach 虚拟滚动渲染
解锁/未解锁状态 条件渲染 if + 视觉差异 半透明/彩色图标,锁定/对勾
进度显示 进度条 + 百分比文字 未解锁时显示进度追踪

LeaderboardPage 功能清单

功能 实现方式 交互效果
难度切换 selectedDifficulty @State + 标签栏 三档难度切换
排名列表 List + ForEach 奖杯排名 + 玩家信息 + 用时
视觉效果 三元表达式选择奖杯表情 前三名奖杯,其他显示数字

七、小结与预告

本篇我们完成了:

  • ✅ 成就系统(12 个成就、3 分类、进度追踪)
  • ✅ 排行榜(3 难度、5 排名、奖杯显示)
  • ✅ 条件渲染(if)、列表性能优化(List + 键值)
  • ✅ 数据驱动 UI 的完整模式

目前所有数据都是静态的。在实际应用中,应该使用 PersistentStorage 或数据库来持久化数据,确保成就进度和排行榜数据在应用重启后不丢失。

最后一篇我们将开发教程页面自定义主题功能,并回顾整个项目的架构设计与优化方向。敬请期待!

相关推荐
伶俜661 小时前
鸿蒙原生应用实战(五)ArkUI 图片拼接/长图生成:多图合并 + Canvas 绘制 + 导出分享
华为·harmonyos
前端不太难1 小时前
当 AI 接管 Workspace:鸿蒙 PC Agent 架构设计实践
人工智能·状态模式·harmonyos
伶俜661 小时前
鸿蒙原生应用实战(六)ArkUI 屏幕录制 + GIF 截取:录屏 + 裁剪关键帧 + 转 GIF
华为·harmonyos
祭曦念2 小时前
【共创季稿事节】谁是卧底词语生成器_鸿蒙开发实战
华为·harmonyos
YM52e2 小时前
鸿蒙PC ArkTS 死亡轮循深度解析与解决方案
学习·华为·harmonyos·鸿蒙·鸿蒙系统
木咺吟2 小时前
鸿蒙原生应用实战(二):首页与包裹列表开发——List组件、ForEach渲染与状态管理
harmonyos
风华圆舞2 小时前
鸿蒙 MICROPHONE 权限在 Flutter 项目里怎么处理
flutter·华为·harmonyos
xcLeigh2 小时前
鸿蒙平台 NixNote2 富文本笔记应用适配实战:从 Linux 到 鸿蒙PC 的 Electron 迁移
linux·笔记·harmonyos·富文本·nixnote2·evernote
伶俜662 小时前
鸿蒙原生应用实战(一):从零开发一个短视频编辑器 App
编辑器·音视频·harmonyos