《鸿蒙纪元》 是 张风捷特烈 计划打造的一套 HarmonyOS 开发系列教程合集。致力于创作优质的鸿蒙原生学习资源,帮助开发者进入纯血鸿蒙的开发之中。本系列的所有代码将开源在 HarmonyUnit 项目中:
github
: github.com/toly1994328…
gitee
: gitee.com/toly1994328…
鸿蒙纪元 系列文章列表可在《文章总集》 或 【github 项目首页】 查看。
上一篇,我们简单了解了一下鸿蒙开发中通过 SoundPool
播放短音效,从而实现了 电子木鱼 的基本功能。本文将进一步完善功能,看一下鸿蒙开发中如何弹出底部框,实现 切换音效和图片
的功能:
1. 选择木鱼界面分析
现在想要的效果如下所示,点击第二个按钮时,从底部弹出木鱼图片的选项。从界面上来说,有如下几个要点:
- 需要展示木鱼样式的基本信息,包括名称、图片及每次功德数范围。
- 需要将当前展示选择的木鱼边上蓝色边线,表示选择状态。
- 选择切换木鱼时,同时更新主界面木鱼样式。
木鱼界面 | 选择界面 |
---|---|
我们先梳理一下木鱼的数据信息:根据目前的需求设定,木鱼需要 名称
、图片资源
、增加功德范围
的数据,这些数据可以通过一个类型进行维护,比如下面的 ImageOption
类:这种单纯记录数据信息的类我称之为数据模型,放在 model
文件夹中:
---->[muyu/model/image_option.ets]----
export class ImageOption {
name: string = ''; // 名称
src: ResourceStr = ''; // 资源
min: number = 0; // 每次点击时功德最小值
max: number = 0; // 每次点击时功德最大值
constructor(name: string, src: ResourceStr, min: number, max: number) {
this.name = name;
this.src = src;
this.min = min;
this.max = max;
}
}
业务层 MuyuBloc 需要持有界面所依赖的状态数据,如下所示:增加 activeImageIndex
表示当前选中的木鱼索引,用于视图中边框高亮处理;imageOptions
时木鱼图片信息数据 ImageOption 列表,用于视图中弹框中的内容;
---->[muyu/bloc/MuyuBloc.ets]----
export class MuyuBloc {
@Track activeImageIndex: number = 0;
@Track imageOptions: ImageOption[] = [
new ImageOption('基础版', $r('app.media.muyu'), 1, 3),
new ImageOption('尊享版', $r('app.media.muyu2'), 3, 6),
];
imageSrc(): ResourceStr {
return this.imageOptions[this.activeImageIndex].src;
}
changeImage(index: number): void {
this.activeImageIndex = index;
}
//略同...
另外 changeImage
的行为会修改激活索引,在用户点击选择时触发;这里提供了一个 imageSrc
方法,便于视图层方便获取当前激活的图片,这样主界面的图片展示,只需要修改一行代码即可:
2. 底部弹框与图片选择
首先,需要在木鱼界面最上层组件通过 bindSheet
来绑定抽屉,其中:
- 第一参数,可以通过
$$
符双向绑定一个布尔值状态类。这样每当isShowSheet
改变时,就可以控制底部抽屉的打开和关闭; - 第二参数是抽屉界面展示的内容,需要通过
@Builder
构建,相当于一个插槽; - 第三参数是抽屉的配置参数,这里简单设置一下抽屉的高度。
---->[muyu/view/MuyuPage.ets]----
@State isShowSheet: boolean = false;
build() {
Column() {
//略同...
}.backgroundColor('#fafafa')
.bindSheet($$this.isShowSheet, this.sheetBuilder(), {
height: 400,
showClose: false,
})
}
@Builder sheetBuilder() {
//TODO build sheet
}
下面来完成木鱼选择界面的布局,这个组件相对独立,我将其封装为 MuyuSelectorSheet
组件。首先从界面上来看,它需要 ImageOption
列表和激活的索引;另外,它还需要响应点击的事件,通知构建者那个索引的条目被点击了。
通过 type 关键字,可以定义函数类型,比如这里 IndexSelector 是一个接收索引的回调函数
这样在使用 MuyuSelectorSheet 时,构造函数中可以通过 onSelected
参数处理时间,并通过回调参数感知用户交互的激活索引。触发 MuyuBloc
的相关功能即可:
--->[muyu/view/MuyuSelectorSheet.ets]---
import { ImageOption } from "../model/image_option";
type IndexSelector = (index: number) => void;
@Component
export struct MuyuSelectorSheet {
@Prop activeIndex: number = 0;
private options: ImageOption[] = []
private onSelected?: IndexSelector;
构建类似布局的条目列表,可以通过 ForEach
组件进行构建。对于单体的构建,一般会进行拆分。你可以通过一个组件来处理,也可以拆为一个 @Builder
构建器:
@Component
export struct MuyuSelectorSheet {
@Prop activeIndex: number = 0;
private options: ImageOption[] = []
private onSelected?: IndexSelector;
build() {
Column() {
Row() {
Text('选择木鱼').fontSize('18fp').fontWeight(FontWeight.Bold)
}.height(52).justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center)
Row() {
ForEach(this.options, (item: ImageOption, index: number) => this.buildItem(item, index),);
}.margin({ top: 24 })
}
.padding({ left: 16, right: 16 })
.width('100%')
.height('100%')
.borderRadius(8)
.backgroundColor(Color.White)
}
}
比如这里通过 buildItem
构建器,构建每个木鱼样式的展示:
@Builder buildItem(item: ImageOption, index: number) {
Row() {
Column() {
Text(item.name).fontSize('16fp').fontWeight(FontWeight.Bold)
Image(item.src)
.width(120)
.height(120)
.borderRadius(4)
.backgroundColor(Color.White)
.margin({ top: 16, bottom: 16 })
Text(`每次功德 +${item.min} ~ ${item.max}`).fontSize('14fp').fontColor('#b0b0b0')
}
}
.alignItems(VerticalAlign.Center)
.padding({ left: 24, right: 16, top: 12, bottom: 12 })
.layoutWeight(1)
.margin({ right: 8 })
.border(index == this.activeIndex ? { width: 1, radius: 6, color: Color.Blue } : {})
.onClick(() => this.onSelected!(index));
}
此时,将 MuyuSelectorSheet 放入之前的 sheetBuilder 中参与构建即可。
注意,不能直接放入,需要套 Column、Row 之类的容器,不知为啥,不然报错。
@Builder sheetBuilder() {
Column() {
MuyuSelectorSheet({
activeIndex: this.model.activeImageIndex,
options: this.model.imageOptions,
onSelected: (index) => {
this.model.changeImage(index);
this.isShowSheet = false;
}
)
}
}
3. 音效选择功能
音效的选择也是类似的,首先准备数据资源,AudioOption 中有名字和资源两个属性。点击条目右侧时可以试听音效:
主界面 | 音效选择 |
---|---|
--->[muyu/model/audio_option.ets]----
export class AudioOption {
name: string = '';
src: string = '';
constructor(name: string, src: string) {
this.name = name;
this.src = src;
}
}
MuyuBloc 中也需要定义声音相关的数据,比如 AudioOption 列表、激活的声音索引;另外不想每次点击都加载一下声音资源,这里准备了 soundIds
列表,在 loadSounds 中统一加载。
changeVoice 方法用于切换到用户选择的音频索引;previewVoice 用于预览对应索引的音频。这样数据逻辑层面就完成了。下面看一下视图构建逻辑:
---->[muyu/bloc/MuyuBloc.ets]----
private soundIds: number[] = [];
@Track activeAudioIndex: number = 0;
@Track audioOptions: AudioOption[] = [
new AudioOption('空灵弥音', 'muyu_1.mp3'),
new AudioOption('落子禅定', 'muyu_2.mp3'),
new AudioOption('滴水明镜', 'muyu_3.mp3'),
];
changeVoice(index: number): void {
this.activeAudioIndex = index;
}
previewVoice(index: number): void {
this.player?.play(this.soundIds[index]);
}
async loadSounds() {
this.soundIds = [];
for (let i = 0; i < this.audioOptions.length; i++) {
let audio: AudioOption = this.audioOptions[i];
let id = await this.loadSoundId(audio.src);
this.soundIds.push(id);
}
}
同样,音频选择也进行独立封装,通过 VoiceSelectorSheet 组件进行构建,构建时传入激活索引、音频信息列表;以及两个点击事件分别通知外界 点击选中
和 点击预览
的事件:
import { AudioOption } from "../model/audio_option";
type IndexSelector = (index: number) => void;
@Component
export struct VoiceSelectorSheet {
@Prop activeIndex: number = 0;
private options: AudioOption[] = []
private onSelected?: IndexSelector;
private onPreview?: IndexSelector;
@Builder
voiceItem(item: AudioOption, index: number) {
Row() {
Text(item.name).layoutWeight(1).fontColor(this.activeIndex==index?Color.Blue:Color.Black)
Button({ type: ButtonType.Normal, stateEffect: true }) {
SymbolGlyph($r('sys.symbol.music_fill'))
.fontSize(20) .fontColor([Color.Blue]) .fontWeight(FontWeight.Bold)
}
.width(36).height(36) .borderRadius(4) .backgroundColor(Color.White)
.onClick(() => this.onPreview!(index))
}.height(46).alignItems(VerticalAlign.Center).padding({ left: 24, right: 16 })
.onClick(() => this.onSelected!(index));
}
build() {
Column() {
Row() {
Text('选择音效').fontSize('18fp').fontWeight(FontWeight.Bold)
}.height(52).justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center)
ForEach(this.options, (item: AudioOption, index: number) => this.voiceItem(item, index),);
}
.width('100%').height('100%').borderRadius(8).backgroundColor(Color.White)
}
}
最后,我们需要定义一个状态量,表示点击的是切换音频还是切换图片:然后在 sheetBuilder
构建函数中根据状态区分构建哪个组件。到这里,提交一个小里程碑:v15-电子木鱼-切换音频、图片
尾声
本章通过弹出框展示切换音效和木鱼样式,可以进一步理解数据和界面视图之间的关系:数据为界面提供内容信息、界面交互操作更改数据信息。
另外,两个选项的面板通过自定义组件进行封装隔离,面板在需要的数据通过构造函数传入、界面中的事件通过回调函数交由外界执行更新数据逻辑。也就是说,封装的 Widget 在意的是如何构建展示内容,专注于做一件事,与界面构建无关的任务,交由使用者处理。
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。关注 公众号
并回复 鸿蒙纪元 可领取最新的 xmind 脑图电子版,让我们一起成长,变得更强。我们下次再见~