HarmonyOS应用开发实战:猫猫大作战-成就殿堂页面完整实战——Circle 进度环、成就卡片与解锁交互

上图是「猫猫大作战」成就殿堂页面在 HarmonyOS 模拟器上的真实效果:顶部导航栏、成就进度卡片(含进度环)、「全部 / 已解锁 / 未解锁」标签栏,以及成就卡片列表。本文完整拆解它的实现。
前言
成就系统是游戏类应用的留存神器:它把「得分」「连击」「进化」「局数」等行为数据翻译成看得见的里程碑,激励玩家持续游玩。在「猫猫大作战」里,成就殿堂页面(AchievementPage.ets)承担了这一职责,它同时用到了 Circle 描边画进度环、List 复用成就卡片、@Builder 封装卡片、标签栏条件样式、自定义 Toast 和 分享面板。
本篇围绕这个真实页面,讲清楚五件事:
- 成就数据模型与
AchievementManager单例怎么设计; Circle组件如何用strokeDashArray+strokeDashOffset画出进度环;- 成就卡片用
@Builder怎么做到一处定义、N 处复用; - 三标签筛选 + 自定义 Toast + 分享面板的完整交互;
- 成就解锁状态如何与
preferences持久化闭环。
提示:本文假设你已经掌握 ArkTS 基础语法与
@State、@Builder等装饰器用法,不再重复环境搭建。项目源码位于dazhuozha/entry/src/main/ets/pages/AchievementPage.ets与components/AchievementManager.ets。
一、成就系统的功能拆解
1.1 页面职责与五层结构
成就殿堂页面从上到下分为五层:
| 层级 | 区域 | 用到的组件/特性 |
|---|---|---|
| 第 1 层 | 顶部导航栏(返回 + 标题 + 分享) | Row、Button、Blank、.shadow |
| 第 2 层 | 成就进度卡片(解锁数 + 进度环) | Circle、strokeDashArray、strokeDashOffset |
| 第 3 层 | 标签栏(全部 / 已解锁 / 未解锁) | ForEach、Divider、条件样式 |
| 第 4 层 | 成就列表 | List、ListItem、@Builder 卡片 |
| 第 5 层 | Toast 与分享面板(浮层) | Stack、transition、animateTo |
build() {
Stack({ alignContent: Alignment.Bottom }) {
// 主内容
Column() {
// 第 1 层:顶部导航栏
Row() { /* 返回 + 标题 + 分享 */ }
// 第 2 层:成就进度卡片
Column() { /* 🏆 计数 + 进度环 */ }
// 第 3 层:标签栏
Row() { /* ForEach 三个标签 */ }
// 第 4 层:成就列表
List({ space: 8 }) { ForEach(...) }
}
// 第 5 层:浮层
if (this.toastData.visible) { this.ToastComponent() }
if (this.showShareDialog) { this.ShareSheet() }
}
}
提示:页面根容器是
Stack({ alignContent: Alignment.Bottom })——主内容填满全屏,Toast 和分享面板作为浮层叠在上面。Stack是 ArkUI 叠层布局的唯一选择,本系列第 9 篇讲过它的 9 位对齐与 zIndex。
1.2 数据从哪里来:AchievementManager 单例
成就数据由 AchievementManager 单例管理,页面只做渲染:
import { AchievementManager, Achievement, AchievementType } from '../components/AchievementManager';
import { DataStorage } from '../components/DataStorage';
@Entry
@Component
struct AchievementPage {
@State achievements: Achievement[] = [];
@State unlockedCount: number = 0;
@State totalCount: number = 0;
@State selectedTab: number = 0; // 0: 全部, 1: 已解锁, 2: 未解锁
// ...
private achievementManager = AchievementManager.getInstance();
// ...
}
加载流程在 aboutToAppear 里完成:初始化存储 → 读统计 → 恢复成就 → 刷新 UI:
async aboutToAppear() {
this.shareManager.setContext(getContext(this) as Context);
// 加载成就数据
const storage = DataStorage.getInstance();
await storage.init(getContext(this) as Context);
const stats = await storage.getStats();
this.achievementManager.loadAchievements(stats.achievements);
this.achievements = this.achievementManager.getAllAchievements();
this.unlockedCount = this.achievements.filter(a => a.unlocked).length;
this.totalCount = this.achievements.length;
}
stats.achievements 是已解锁成就的 id 字符串数组,loadAchievements 把它们回填到单例里。页面本身不修改成就状态,只负责展示——职责边界清晰。
二、成就数据模型与单例设计
2.1 成就类型枚举
成就一共有 14 个,用枚举定义 id:
// 成就类型
export enum AchievementType {
SCORE_1000 = 'score_1000', // 得分达到1000
SCORE_5000 = 'score_5000', // 得分达到5000
SCORE_10000 = 'score_10000', // 得分达到10000
COMBO_3 = 'combo_3', // 连击3次
COMBO_5 = 'combo_5', // 连击5次
COMBO_10 = 'combo_10', // 连击10次
LEVEL_MEDIUM = 'level_medium', // 合并出中猫
LEVEL_LARGE = 'level_large', // 合并出大猫
LEVEL_XLARGE = 'level_xlarge', // 合并出特大猫
LEVEL_SUPER = 'level_super', // 合并出超级猫
LEVEL_LEGENDARY = 'level_legendary', // 合并出传奇猫
GAMES_10 = 'games_10', // 游戏10局
GAMES_50 = 'games_50', // 游戏50局
GAMES_100 = 'games_100', // 游戏100局
}
枚举值用小写短横线字符串,好处是直接可作存储 key、可序列化,不会因数值变化破坏历史数据。
2.2 成就接口与配置表
// 成就定义
export interface Achievement {
id: AchievementType;
name: string;
description: string;
icon: string;
unlocked: boolean;
unlockTime?: number;
}
// 成就配置
export const AchievementConfigs: Record<string, Achievement> = {
[AchievementType.SCORE_1000]: {
id: AchievementType.SCORE_1000,
name: '初露锋芒',
description: '单局得分达到1000分',
icon: '🌟',
unlocked: false
},
// ... 其余 13 个
};
14 个成就全部以声明式配置集中维护,新增成就只需在 AchievementConfigs 加一项 + 在检查函数加一条判断。unlocked 初始为 false,实际状态从持久化恢复。
2.3 单例构造与解锁回调
export class AchievementManager {
private static instance: AchievementManager;
private achievements: Map<AchievementType, Achievement> = new Map();
private onAchievementUnlocked?: (achievement: Achievement) => void;
static getInstance(): AchievementManager {
if (!AchievementManager.instance) {
AchievementManager.instance = new AchievementManager();
}
return AchievementManager.instance;
}
constructor() {
Object.values(AchievementConfigs).forEach(config => {
const achievement: Achievement = {
id: config.id,
name: config.name,
description: config.description,
icon: config.icon,
unlocked: false
};
this.achievements.set(config.id, achievement);
});
}
// 设置成就解锁回调
setUnlockCallback(callback: (achievement: Achievement) => void): void {
this.onAchievementUnlocked = callback;
}
// ...
}
Map<AchievementType, Achievement> 保证 O(1) 查询,setUnlockCallback 让游戏页可以订阅「成就解锁」事件弹出动画。
三、Circle 进度环的实现
3.1 进度环的视觉结构
进度卡片右侧是一个圆环,显示解锁百分比。它由两个圆环叠成:灰色底环 + 蓝色进度环:
Stack() {
// 背景圆环
Circle()
.width(80)
.height(80)
.fill(Color.Transparent)
.stroke('#E8E8E8')
.strokeWidth(8)
// 进度圆环
Circle()
.width(80)
.height(80)
.fill(Color.Transparent)
.stroke('#3498DB')
.strokeWidth(8)
.strokeDashArray([Math.PI * 80])
.strokeDashOffset(Math.PI * 80 * (1 - this.unlockedCount / this.totalCount))
// 百分比文字
Text(`${Math.round(this.unlockedCount / this.totalCount * 100)}%`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3498DB')
}
.layoutWeight(1)
提示:
Circle组件默认是实心圆,画「环」必须配合fill(Color.Transparent)(透明填充)+stroke()(描边色)+strokeWidth()(描边粗细)。
3.2 strokeDashArray 与 strokeDashOffset 的数学原理
进度环的精髓在两条属性:
| 属性 | 作用 | 本项目取值 |
|---|---|---|
strokeDashArray | 定义「画一段、空一段」的虚线模式 | [Math.PI * 80] |
strokeDashOffset | 虚线起始偏移量,负值逆时针回退 | Math.PI * 80 * (1 - 进度) |
推导过程:
- 圆环周长 =
π × 直径 = π × 80; strokeDashArray([π × 80])让「实线段 = 整圆周长」,即先画一个完整的圆;strokeDashOffset(π × 80 × (1 - 进度))把起点往后偏移,剩余可见的实线长度 =周长 - 偏移 = 周长 × 进度;- 进度 = 43% 时,可见弧长 = 整圆的 43%,看起来就是一个「环形进度条」。
// 进度环数学:可见弧长 = 周长 × 进度
const circumference = Math.PI * 80; // 周长
const progress = this.unlockedCount / this.totalCount; // 0~1
// strokeDashArray = [circumference]
// strokeDashOffset = circumference * (1 - progress)
关键点:strokeDashOffset 的单位与周长一致(都是 vp),偏移量越大,可见弧越短。这个公式可以直接复用到任何 Circle 进度环。
四、成就卡片:@Builder 封装
4.1 卡片结构
每个成就是一张卡片,用 @Builder 封装成 AchievementCard:
@Builder
AchievementCard(achievement: Achievement) {
Row() {
// 成就图标(圆形底 + Emoji)
Column() {
Text(achievement.icon).fontSize(32)
}
.width(60).height(60)
.backgroundColor(achievement.unlocked ? '#F0F9FF' : '#F5F5F5')
.borderRadius(30)
.justifyContent(FlexAlign.Center)
// 成就信息
Column() {
Row() {
Text(achievement.name).fontSize(16).fontWeight(FontWeight.Bold)
if (achievement.unlocked) {
Text('✓').fontSize(14).fontColor('#2ECC71').margin({ left: 8 })
}
}
Text(achievement.description).fontSize(12).fontColor('#7F8C8D').margin({ top: 4 })
if (achievement.unlocked && achievement.unlockTime) {
Text(`解锁时间: ${this.formatTime(achievement.unlockTime)}`)
.fontSize(10).fontColor('#95A5A6').margin({ top: 4 })
}
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 16 })
// 右侧状态:✓ 或 🔒
if (achievement.unlocked) {
Text('✓').fontSize(24).fontColor('#2ECC71')
} else {
Text('🔒').fontSize(24)
}
}
.width('100%').padding(16)
.backgroundColor('#FFFFFF').borderRadius(12)
.margin({ top: 8 })
.shadow({ radius: 2, color: 'rgba(0,0,0,0.05)', offsetY: 1 })
}
卡片内部有三处已解锁 / 未解锁的双态展示:
- 图标底色:解锁
#F0F9FF(浅蓝)vs 未解锁#F5F5F5(浅灰); - 名称旁:解锁显示绿色
✓; - 右侧状态:解锁
✓vs 未解锁🔒。
4.2 列表复用
列表用 List + ForEach + AchievementCard 组合:
List({ space: 8 }) {
ForEach(this.getFilteredAchievements(), (achievement: Achievement) => {
ListItem() {
this.AchievementCard(achievement)
}
})
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, top: 8, bottom: 16 })
提示:
@Builder方法(this.AchievementCard(achievement))在ListItem里被反复调用,卡片内部无@State、纯展示,性能开销极低。如果卡片数量继续膨胀,可升级为@Reusable(本系列第 67 篇)。
五、标签栏:全部 / 已解锁 / 未解锁
5.1 ForEach 动态标签
三个标签由一个数组生成,选中态用 selectedTab === index 判断:
Row() {
ForEach(['全部', '已解锁', '未解锁'], (tab: string, index: number) => {
Column() {
Text(tab)
.fontSize(14)
.fontColor(this.selectedTab === index ? '#3498DB' : '#7F8C8D')
.fontWeight(this.selectedTab === index ? FontWeight.Bold : FontWeight.Normal)
// 选中态下划线
if (this.selectedTab === index) {
Divider()
.width(20).height(2).color('#3498DB')
.margin({ top: 4 })
}
}
.layoutWeight(1)
.onClick(() => {
this.selectedTab = index;
})
})
}
.width('100%').height(48)
.backgroundColor('#FFFFFF')
.margin({ top: 16, left: 16, right: 16 })
.borderRadius(12)
两个细节值得注意:
- 下划线用
Divider条件渲染:选中才出现,未选中直接if掉,不占位; - 点击切换
selectedTab:状态一改,标签样式和列表内容同时更新。
5.2 筛选逻辑
getFilteredAchievements(): Achievement[] {
if (this.selectedTab === 0) {
return this.achievements; // 全部
} else if (this.selectedTab === 1) {
return this.achievements.filter(a => a.unlocked); // 已解锁
} else {
return this.achievements.filter(a => !a.unlocked); // 未解锁
}
}
筛选是派生数据,不是状态——每次渲染时根据 selectedTab 现算,干净无副作用。
六、解锁检查逻辑:AchievementManager 的四个 check
游戏页在合适的时机调用检查函数,命中即解锁。以得分成就为例:
// 检查得分成就
checkScoreAchievement(score: number): Achievement[] {
const unlocked: Achievement[] = [];
if (score >= 1000) {
const achievement = this.unlock(AchievementType.SCORE_1000);
if (achievement) unlocked.push(achievement);
}
if (score >= 5000) {
const achievement = this.unlock(AchievementType.SCORE_5000);
if (achievement) unlocked.push(achievement);
}
if (score >= 10000) {
const achievement = this.unlock(AchievementType.SCORE_10000);
if (achievement) unlocked.push(achievement);
}
return unlocked;
}
四个检查函数覆盖四类行为:
| 函数 | 触发时机 | 示例成就 |
|---|---|---|
checkScoreAchievement(score) | 单局结束 | 初露锋芒 / 得分大师 |
checkComboAchievement(combo) | 连击结算 | 连击新手 / 连击大师 |
checkLevelAchievement(level) | 合并升级 | 猫咪进化 / 传奇诞生 |
checkGamesAchievement(totalGames) | 局数累计 | 游戏新手 / 游戏大师 |
核心的 unlock 是 private 方法,保证只有检查函数能改状态:
// 解锁成就
private unlock(type: AchievementType): Achievement | null {
const achievement = this.achievements.get(type);
if (achievement && !achievement.unlocked) {
achievement.unlocked = true;
achievement.unlockTime = Date.now();
// 触发回调
if (this.onAchievementUnlocked) {
this.onAchievementUnlocked(achievement);
}
return achievement;
}
return null;
}
unlock 的幂等保护很关键:已解锁的成就不会二次触发回调,避免连击结算时反复弹动画。
七、自定义 Toast 组件
页面没有用系统 promptAction.showToast,而是手写了一个带状态色的 Toast,支持成功/失败/信息三种类型:
// Toast提示类型
enum ToastType {
SUCCESS = 'success',
ERROR = 'error',
INFO = 'info'
}
// Toast提示数据
interface ToastData {
message: string;
type: ToastType;
visible: boolean;
}
渲染组件:
@Builder
ToastComponent() {
Row() {
Row() {
// 图标:✓ / ✗ / ℹ
Text(this.toastData.type === ToastType.SUCCESS ? '✓' :
this.toastData.type === ToastType.ERROR ? '✗' : 'ℹ')
.fontSize(18).fontColor(Color.White).margin({ right: 8 })
// 消息
Text(this.toastData.message).fontSize(14).fontColor(Color.White)
}
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor(
this.toastData.type === ToastType.SUCCESS ? '#2ECC71' :
this.toastData.type === ToastType.ERROR ? '#E74C3C' : '#3498DB'
)
.borderRadius(24)
.shadow({ radius: 8, color: 'rgba(0,0,0,0.2)', offsetY: 4 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ bottom: 100 })
.transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
}
显示与自动隐藏用定时器控制:
private showToast(message: string, type: ToastType = ToastType.INFO) {
this.toastData = { message, type, visible: true };
// 清除之前的定时器
if (this.toastTimer !== -1) {
clearTimeout(this.toastTimer);
}
// 2秒后自动隐藏
this.toastTimer = setTimeout(() => {
this.toastData.visible = false;
}, 2000);
}
提示:
TransitionEffect.OPACITY.animation({ duration: 300 })给 Toast 加了 300ms 的淡入淡出。if (this.toastData.visible)为 true 时组件出现带入场动画,为 false 时离场。手动 Toast 相比系统 Toast 的优势是可控的样式与动画。
八、分享面板:ShareSheet
8.1 面板结构
分享面板是底部弹出的半屏面板,包含平台图标按钮和取消按钮:
@Builder
ShareSheet() {
Column() {
// 遮罩层(点击关闭)
Column()
.width('100%').height('100%')
.backgroundColor('rgba(0,0,0,0.4)')
.onClick(() => { this.hideShareSheet(); })
}
.width('100%').height('100%')
.position({ x: 0, y: 0 })
// 分享对话框
Column() {
// 拖动指示器
Row().width(40).height(4).backgroundColor('#D0D0D0').borderRadius(2)
// 标题
Text('分享成就').fontSize(20).fontWeight(FontWeight.Bold)
Text(`我在猫猫大作战中已解锁 ${this.unlockedCount}/${this.totalCount} 个成就!`)
.fontSize(14).fontColor('#7F8C8D').margin({ top: 8 })
// 平台图标(两行布局)
Row() {
ForEach(getSharePlatforms().slice(0, 3), (platformInfo: SharePlatformInfo) => {
this.PlatformButton(platformInfo)
})
}
.width('100%').justifyContent(FlexAlign.SpaceEvenly).margin({ bottom: 16 })
Row() {
ForEach(getSharePlatforms().slice(3, 5), (platformInfo: SharePlatformInfo) => {
this.PlatformButton(platformInfo)
})
}
.width('100%').justifyContent(FlexAlign.SpaceEvenly)
// 取消按钮
Button('取消')
.fontSize(16).fontColor('#7F8C8D')
.backgroundColor('#F5F5F5').borderRadius(24)
.width('90%').height(48)
.margin({ top: 20, bottom: 20 })
.onClick(() => { this.hideShareSheet(); })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 24, topRight: 24 })
.position({ x: 0, y: '100%' })
.translate({ y: -360 })
.scale({ x: this.dialogAnimationScale, y: this.dialogAnimationScale })
.opacity(this.dialogAnimationOpacity)
.transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
}
8.2 入场/离场动画
面板的弹出与收起用 animateTo 控制缩放与透明度:
// 显示分享对话框(带动画)
private showShareSheet() {
this.showShareDialog = true;
animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
this.dialogAnimationScale = 1;
this.dialogAnimationOpacity = 1;
});
}
// 隐藏分享对话框(带动画)
private hideShareSheet() {
animateTo({ duration: 200, curve: Curve.EaseIn }, () => {
this.dialogAnimationScale = 0.9;
this.dialogAnimationOpacity = 0;
});
setTimeout(() => {
this.showShareDialog = false;
this.dialogAnimationScale = 0;
this.dialogAnimationOpacity = 0;
}, 200);
}
8.3 平台按钮与分享动作
@Builder
PlatformButton(platformInfo: SharePlatformInfo) {
Column() {
Column() {
Text(platformInfo.icon).fontSize(32)
}
.width(64).height(64)
.backgroundColor(platformInfo.color + '20') // 添加透明度
.borderRadius(32)
.justifyContent(FlexAlign.Center)
.border({ width: 2, color: platformInfo.color })
Text(platformInfo.name).fontSize(12).fontColor('#7F8C8D').margin({ top: 8 })
}
.onClick(async () => {
if (this.isSharing) return; // 防止重复点击
this.isSharing = true;
// ... 按平台类型调用 shareManager 分享
})
}
platformInfo.color + '20' 是颜色透明度后缀技巧:#3498DB20 表示 20% 透明度的主题色,用来做图标底色的同色系浅底,视觉统一又省事。
九、成就持久化闭环
成就状态最终要落盘,下次启动才能恢复。存储层在 DataStorage:
// 保存统计数据
async saveStats(stats: GameStats): Promise<void> {
if (!this.preferences) return;
try {
const statsData = JSON.stringify(stats);
await this.preferences.put('gameStats', statsData);
await this.preferences.flush();
} catch (error) {
console.error('Failed to save stats:', error);
}
}
// 获取统计数据
async getStats(): Promise<GameStats> {
const defaultStats: GameStats = {
highScore: 0, totalGames: 0, totalScore: 0,
maxCombo: 0, highestLevel: CatLevel.SMALL,
achievements: []
};
if (!this.preferences) return defaultStats;
// ... get + JSON.parse
}
完整闭环:
- 游戏页触发
checkScoreAchievement等检查 →unlock置位 + 回调弹动画; - 存档时
achievementManager.exportAchievements()导出已解锁 id 数组 →saveStats落盘; - 成就页
aboutToAppear→getStats()→loadAchievements()恢复 → 渲染。
// 导出成就数据(存档用)
exportAchievements(): string[] {
return this.getUnlockedAchievements().map(a => a.id);
}
// 加载成就数据(恢复用)
loadAchievements(data: string[]): void {
data.forEach(id => {
const achievement = this.achievements.get(id as AchievementType);
if (achievement) {
achievement.unlocked = true;
}
});
}
导出是「存 id 数组」,恢复是「按 id 回填」——只持久化最小信息(id),完整配置仍从 AchievementConfigs 读取,避免重复存储。
十、踩坑记录:进度环不显示 / 圆环变形
10.1 忘写 fill(Color.Transparent)
Circle 默认有填充色,直接画进度环会得到一个实心圆:
// ❌ 错误:实心圆,进度环效果消失
Circle().width(80).height(80).stroke('#3498DB').strokeWidth(8)
// ✅ 正确:透明填充 + 描边
Circle().width(80).height(80)
.fill(Color.Transparent)
.stroke('#3498DB').strokeWidth(8)
10.2 进度永远 0% 或 100%
strokeDashOffset 算错会导致进度环「全空」或「全满」:
// ❌ 常见错误:方向搞反
.strokeDashOffset(Math.PI * 80 * (this.unlockedCount / this.totalCount))
// 进度越大偏移越大 → 可见弧越短 → 效果反了
// ✅ 正确:1 - 进度 才是剩余可见比例
.strokeDashOffset(Math.PI * 80 * (1 - this.unlockedCount / this.totalCount))
提示:调试进度环时先固定
progress = 0.5,期望看到「半圆」。不是半圆就检查strokeDashArray是否等于周长、strokeDashOffset方向是否反了。
十一、性能与最佳实践
Map存成就、O(1) 查询:AchievementManager用Map<AchievementType, Achievement>,14 个成就无所谓,但扩展到上百个时比数组find快一个量级。- 纯展示卡片不要放
@State:AchievementCard是@Builder纯函数式渲染,内部无状态,复用成本最低。需要交互再拆成独立@Component。 - 筛选用派生函数而不是存副本:
getFilteredAchievements()每次现算,绝不维护一个「筛选后数组」的@State——避免两处数据不同步。 - 解锁回调只触发一次:
unlock里的!achievement.unlocked判断保证幂等,连击结算高频调用也不会重复弹动画。 - 持久化只存最小信息:存 id 数组、读配置表,是「配置与状态分离」的典型实践。
总结
本篇以「猫猫大作战」成就殿堂页面为锚点,完整拆解了成就系统的五个层次:数据模型(枚举 + 配置表 + 单例)、进度环(Circle + strokeDashArray/strokeDashOffset 的数学推导)、卡片复用(@Builder 双态样式)、交互(标签筛选、自定义 Toast、分享面板动画)与持久化闭环(导出 id 数组、恢复回填)。核心方法论:状态只在数据层变更,UI 永远跟随 @State 派生渲染。
下一篇预告:我们将继续「猫猫大作战」的页面级实战,拆解设置页面的开关项、语言切换与数据管理——Switch 组件、preferences 设置项封装与多语言资源切换的综合演练。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库
dazhuozha/entry/src/main/ets/pages/AchievementPage.ets - Circle 圆形组件官方文档
- List 列表组件官方文档
- @Builder 装饰器官方指南
- 偏好数据存储 (Preferences) 官方指南
- animateTo 显式动画官方文档
- TransitionEffect 转场效果参考
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md
更多推荐



所有评论(0)