
1.1 需求概述
实现一个具备完整播放控制功能的音乐播放器,支持播放列表管理、播放/暂停切换、上一首/下一首切歌、进度拖拽、音量调节、三种播放模式(顺序播放、随机播放、单曲循环)以及当前播放高亮显示。
1.2 数据结构设计
typescript
interface Song {
title: string; // 歌曲名
artist: string; // 歌手
duration: number; // 时长(秒)
}
8 首经典华语歌曲以内联数组形式硬编码,避免了网络请求和数据加载的复杂性。选择 Song 接口而非 class 的原因在于:ArkTS 中接口(interface)在编译时擦除,零运行时开销;而 class 需要额外的实例化开销。对于纯数据模型,接口是更优的选择。
1.3 核心状态分析
该应用共使用了 7 个 @State 变量和 1 个私有计时器变量:
| 状态 | 类型 | 作用 | 初始值 | 触发 UI 更新的条件 |
|---|---|---|---|---|
playlist |
Song\[\] | 播放列表数据 | 8首预设歌曲 | 数据变化(当前为静态) |
currentIndex |
number | 当前播放歌曲索引 | 0 | 切歌时 |
isPlaying |
boolean | 播放/暂停状态 | false | 点击播放/暂停 |
progress |
number | 进度百分比 0-100 | 0 | 每秒递增或拖拽 |
volumeValue |
number | 音量 0-100 | 80 | 滑动音量条 |
currentTime |
number | 当前播放进度(秒) | 0 | 每秒递增 |
isShuffle |
boolean | 随机播放开关 | false | 点击随机按钮 |
isLoopOne |
boolean | 单曲循环开关 | false | 点击循环按钮 |
timerId |
number(priv) | 计时器 ID | -1 | 不绑定 UI |
状态组合与播放模式映射:
| isShuffle | isLoopOne | 播放模式 |
|---|---|---|
| false | false | 顺序播放(默认) |
| false | true | 单曲循环 |
| true | false | 随机播放 |
| true | true | 随机循环(随机+循环) |
1.4 三种播放模式的编码策略
ArkTS 对布尔状态组合的支持使得我们只需要两个 boolean 变量即可编码四种状态。在 nextSong() 和 prevSong() 方法中,根据这两个布尔值决定切歌逻辑:
typescript
nextSong(): void {
this.stopTimer();
this.currentTime = 0;
this.progress = 0;
if (this.isLoopOne) {
// 单曲循环:不改变索引,currentTime=0 意味着重新播放
} else if (this.isShuffle) {
// 随机播放:从列表中随机选取一首
this.currentIndex = Math.floor(Math.random() * this.playlist.length);
} else {
// 顺序播放:索引 +1,到达末尾通过取模回到 0
this.currentIndex = (this.currentIndex + 1) % this.playlist.length;
}
if (this.isPlaying) {
this.startTimer();
}
}
这里有一个容易被忽视的细节:previousSong() 方法中,为了防止索引越界,需要加上 this.playlist.length 再取模:
typescript
prevSong(): void {
this.stopTimer();
this.currentTime = 0;
this.progress = 0;
if (this.isShuffle) {
this.currentIndex = Math.floor(Math.random() * this.playlist.length);
} else {
// 关键:+ length 保证取模结果为非负数
this.currentIndex = (this.currentIndex - 1 + this.playlist.length) % this.playlist.length;
}
if (this.isPlaying) {
this.startTimer();
}
}
1.5 定时器驱动的播放进度
播放进度的核心是 setInterval 驱动的秒级计时器:
typescript
startTimer(): void {
this.stopTimer(); // 先清理旧的定时器(幂等安全)
this.timerId = setInterval(() => {
this.currentTime++;
// 计算百分比进度
this.progress = (this.currentTime / this.currentSong.duration) * 100;
// 到达歌曲末尾,自动切歌
if (this.currentTime >= this.currentSong.duration) {
this.nextSong();
}
}, 1000);
}
stopTimer(): void {
if (this.timerId !== -1) {
clearInterval(this.timerId);
this.timerId = -1;
}
}
关键设计原则: stopTimer() 在 startTimer() 中首先调用,确保多次调用 startTimer() 不会产生多个定时器。这种"先停后开"的模式是 ArkTS 定时器管理的标准实践,可以避免定时器 ID 覆盖导致的内存泄漏。
1.6 进度条双向绑定
播放器进度条的交互是双向的:
- 定时器 正向驱动 进度条(每秒 +1)
- 用户拖拽进度条 反向驱动 播放时间
typescript
Slider({
value: this.progress,
min: 0,
max: 100,
step: 1,
style: SliderStyle.OutSet
})
.width('60%')
.onChange((value: number) => this.onSliderChange(value))
onSliderChange(value: number): void {
this.progress = value;
// 根据百分比反算当前时间
this.currentTime = Math.floor((value / 100) * this.currentSong.duration);
// 不停止定时器,松手后继续从新位置播放
}
1.7 音量控制与 UI 图标联动
音量控制使用带图标的 Slider:
typescript
Row() {
Text('🔉').fontSize(16)
Slider({ value: this.volumeValue, min: 0, max: 100, step: 1 })
.width('60%')
.onChange((value: number) => { this.volumeValue = value; })
Text('🔊').fontSize(16)
}
1.8 播放列表的当前歌曲高亮
typescript
ListItem() {
Row() {
Column() {
Text(song.title)
.fontWeight(index === this.currentIndex ? FontWeight.Bold : FontWeight.Normal)
Text(song.artist).fontSize(13).fontColor('#888')
}
Blank()
Text(this.formatTime(song.duration)).fontSize(13).fontColor('#999')
if (index === this.currentIndex) {
Text('🎵').fontSize(16).margin({ left: 8 }) // 当前播放标记
}
}
.backgroundColor(index === this.currentIndex ? '#E8F0FE' : '#FAFAFA')
.borderRadius(8)
.onClick(() => this.selectSong(index))
}
1.9 设计亮点
- @Builder 封装播放按钮 :将播放/暂停按钮抽取为独立的
@Builder函数,因为它在 UI 中需要复用时保持良好的封装性 - 状态组合编码:两个 boolean 变量编码四种播放模式,是有限状态机思想在 ArkTS 中的应用
- 生命周期安全 :
aboutToDisappear中清理定时器,防止页面退出后定时器仍在运行 - 旋转音符动画 :
.animation({ iterations: -1 })实现无限循环旋转,模拟黑胶唱片效果