鸿蒙纪·梦始卷#10 | 电子木鱼 - 功德记录列表

356 阅读6分钟

《鸿蒙纪元》张风捷特烈 计划打造的一套 HarmonyOS 开发系列教程合集。致力于创作优质的鸿蒙原生学习资源,帮助开发者进入纯血鸿蒙的开发之中。本系列的所有代码将开源在 HarmonyUnit 项目中:

github: github.com/toly1994328…
gitee: gitee.com/toly1994328…

鸿蒙纪元 系列文章列表可在《文章总集》 或 【github 项目首页】 查看。


上一篇,我们简单了解通过 bindSheet 弹出底部抽屉框,实现了 选择音效选择图片 基本功能,丰富了电子木鱼的玩法。本文将进一步完善功能,实现 记录功德列表 的功能:


1. 功德记录的需求

我们在上一篇中,为木鱼增加了一个属性点: 每次功德增加的取值范围 。现在想要记录每次点击时的功德数据,以便让用户了解功德增加的历史。对于一个需求而言,最重要的是分析其中需要的数据,以及如何维护这些数据。

效果如下,点击左上角按钮,跳转到功德记录的面板。功德记录界面是一个可以滑动的列表,每个条目展示当前功德的 木鱼图片音效名称时间功德数 信息。在这个需求中,我们还能了解鸿蒙开发中到如何跳转到新界面:

木鱼主页功德记录界面

2. 功德数据模型

根据界面中的数据,可以封装一个类来维护,如下 MeritRecord 类。其中:

  • interface 关键字表示定义数据字段接口,便于作为入参构造 MeritRecord 对象;
  • readonly 关键字表示字段只读,对象无法修改改字段。一条木鱼的历史记录记录着真实可信的数据,是不会也不应被篡改的,所以 readonly 在这里很符合场景:
interface MeritRecordParams {
  timestamp: number; // 记录的时间戳
  value: number; // 功德数
  image: ResourceStr; // 图片资源
  audio: string; // 音效名称
}

export class MeritRecord {
  readonly timestamp: number;
  readonly value: number;
  readonly image: ResourceStr;
  readonly audio: string;

  constructor(data: MeritRecordParams) {
    this.timestamp = data.timestamp;
    this.value = data.value;
    this.image = data.image;
    this.audio = data.audio;
  }
}

对于功德记录的需求而言,需要在 MuyuBloc 中新加的数据也很明确: 维护 MeritRecord 数组。如下所示,records 列表需要在每次点击时记录信息,另外 lastRecord 用于记录最后一次的记录,用于展示每次动画文字的信息:

---->[muyu/bloc/MuyuBloc.ets]----
@Track records: MeritRecord[] = [];
@Track lastRecord?: MeritRecord | null;

tick(): void {
  if (!this.ready) {
    return;
  }
  this.player?.play(this.soundIds[this.activeAudioIndex]);
  let image: ImageOption = this.imageOptions[this.activeImageIndex];
  let audio: AudioOption = this.audioOptions[this.activeAudioIndex];
  let value = Math.floor(Math.random() * (image.max - image.min + 1)) + image.min;
  
  this.lastRecord = new MeritRecord({
    timestamp: Date.now(),
    value: value,
    image: image.src,
    audio: audio.name,
  });
  
  this.records.push(this.lastRecord!);
  this.counter += value;
}

这样数据层就完成了,接下来看一下如何处理新界面的跳转。


3. 使用 Navigation 组件进行界面跳转

界面的跳转很好理解,就是将一个新界面推 (push) 到当前界面之上,从而展示新界面。一般推入的界面可以弹出(pop) 展示下层界面:

官方的文档目前已经不推荐使用 router 体系的路由跳转,推荐使用 Navigation 组件。

首先需要配置路由表,在 module.json5 中增加 routerMap 字段,指向 profile 里的 router_map.json,文件名可以任意,只要两处一致即可:

在路由配置表中,routerMap 节点放置路由列表,配置名称文件路径构建函数

{
  "routerMap": [
    {
      "name": "muyu",
      "pageSourceFile": "src/main/ets/pages/muyu/view/MuyuPage.ets",
      "buildFunction": "pageBuilder"
    },
    {
      "name": "merit_record",
      "pageSourceFile": "src/main/ets/pages/muyu/view/MeritRecordPage.ets",
      "buildFunction": "pageBuilder"
    }
  ]
}

界面构造器就是一个通过 @Builder 注解、返回界面对象的函数,如下所示:

--->[/muyu/view/MuyuPage.ets]----
@Builder
export function pageBuilder() {
  MeritRecordPage()
}

另外推入的界面一般通过 NavDestination 组件包裹,可以感知一些路由的生命周期,以及操作方法:


此时 Index 入口中,通过 Navigation 组件提供路由功能。需要在构造时传入 NavPathStack 对象,该对象可以操作路由栈。这里通过 @Provide 注解可以向下层的组件传递状态数据,便于子组件访问 pageStack 对象。在 onAppear 回调中,可以通过 pushPathByName 跳转到木鱼主界面:

---->[Index.ets]----
@Entry
@Component
struct Index {
  @StorageProp('bottomRectHeight')
  bottomRectHeight: number = 0;
  @Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.pageStack){}
      .padding({ bottom: px2vp(this.bottomRectHeight) })
      .onAppear(() => this.pushHome())
  }

  pushHome(): void {
    this.pageStack.pushPathByName("muyu", null, false);
  }
}

下层的 MuYuPage 组件通过 @Consume 注解可以访问到上层注入的对象,现在只需要点击历史记录按钮,通过 pageStack 进行跳转即可:

pushPathByName 的第二参是推入路由 传递的参数,这里新界面需要功德记录的数据

--->[/muyu/view/MuyuPage.ets]----
@Component
export struct MuYuPage {
  @Consume('NavPathStack') pageStack: NavPathStack;
  
  toHistory() {
    this.pageStack.pushPathByName('merit_record', this.model.records.reverse());
  }
  ...
}

4. 功德记录界面

在 view 文件夹中创建 MeritRecordPage.ets 文件,使用 NavDestination 包裹整个界面内容,在 onReady 回调中可以感知导航的上下文,从中可以获取上级界面传递的参数,以此初始化 MeritRecord 数组:

@Builder
export function pageBuilder() {
  MeritRecordPage()
}

@Component
export struct MeritRecordPage {

  pathStack?: NavPathStack;
  @State records: MeritRecord[] = []
  
  findParam(context: NavDestinationContext): void {
    this.records = context.pathInfo.param as MeritRecord[];
  }
  
  build() {
    NavDestination() {
      /// 构建界面具体内容
    }
    .onReady((ctx) => this.findParam(ctx)).hideTitleBar(true)
  }
}

接下来就是列表界面的构建代码,建议大家在写界面时,尽可能细分一下。比如这个组件可以封装一下来单独维护,以后想修改就很方便定位到代码位置。

另外,不要写一点代码运行一下查看效果,毕竟目前鸿蒙开发的热重载还没有很完善,对于跳转进入的二级界面,运行查看效果比较费劲。拆分成新组件的好处还有:你可以通过 @Preview 注解,通过 Previewer 面板在开发过程中,实时查看效果,有助于效率提升:

列表内容是可滑动的,可以使用 List 组件进行构建,通过 ForEach 遍历构建 MeritRecordItem 子条目即可。条目的独立封装,也可以让构建的逻辑更加清晰。想一想,如果 MeritRecordItem 的构建逻辑全部塞在 ForEach 里面,代码回是多么混乱。

List() {
  ForEach(this.records, (record: MeritRecord) => {
    ListItem() { MeritRecordItem({ record }) }
  }, (item: MeritRecord) => `${item.timestamp}`)
}

尾声

到这里,电子木鱼的基本功能完成了,这里提交一个小里程碑 v16-电子木鱼-功能完成 。目前我们只是简单地了解一下 Navigation 导航,以后还会有机会详细地介绍它。

另外,现在的数据都是存储在内存中的,应用退出之后无论是选项,还是功德记录都会重置。想要数据持久化存储,在后面的 数据的持久化存储 章节会再继续完善,木鱼项目先告一段落。

更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。关注 公众号 并回复 鸿蒙纪元 可领取最新的 xmind 脑图电子版,让我们一起成长,变得更强。我们下次再见~