HarmonyOS 鸿蒙Next开发模板/组件分享 – 极简播放器(simple_audio)
HarmonyOS 鸿蒙Next开发模板/组件分享 – 极简播放器(simple_audio)
跑跑“码”特–HarmonyOS 模板组件开发挑战赛-极简播放器
《极简播放器》研发背景
在HarmonyOS Next应用开发中,如果我们想要使用播放音乐、并且提供基本的功能按钮,往往都需要开发者实现以下过程
- 熟悉和开发媒体相关的kit
- 使用ArkUI完成相关的布局和功能设计
但是纵观各种这样的播放器,核心功能还是如下几个
- 播放&暂停
- 上一首&下一首
- 单曲播放&列表播放&循环播放&随机播放
- 音量大小调整
- 播放进度调整
如酷狗音乐:
基于以上需求,便开发出这一款极简的播放器。
效果展示
功能介绍
考虑到以上的需求,这里提供的主要功能如下
- 播放&暂停
- 上一首&下一首
- 单曲播放&列表播放&循环播放&随机播放
- 音量大小调整
- 播放进度调整
关键技术
- ArkUI
- ArkTS
- 封装-class
- AVPlayer
- 防抖
目录结构
这里考虑后面将该播放器提交上架,所以是采用了静态共享包Har的方式来开发的。主要的目录结构如下
├─components
│ simple_audio.ets // 核心UI组件
├─type
│ index.ets // 用到的相关类型
└─utils
index.ets // 工具代码
示例代码
utils
// 时间格式化
// time单位的毫秒 要输出分钟和秒 格式 00:00
export const sa_timeFormat = (time: number) => {
// 分钟
const minutes = Math.floor(time / 1000 / 60)
// 秒
const seconds = (Math.floor(time / 1000) % 60).toString().padStart(2, '0')
return `${minutes}:${seconds}`
}
// 防抖工具函数
export const sa_throttle = <T = null>(callback: Function, time: number) => {
let tid = -1
const retFunction = (...args: T[]) => {
if (tid !== -1) {
clearTimeout(tid)
}
tid = setTimeout(() => {
callback(args)
}, time)
}
return retFunction
}
// 获取指定范围的随机数
export const getRandomFromRange = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1) + min)
}
type
用到的类型相关代码
import { media } from '@kit.MediaKit'
export enum SAPlayMode {
/**
* 列表播放
*/
order,
/**
* 单曲播放
*/
single,
/**
* 重复播放
*/
repeat,
/**
* 随机播放
*/
random
}
/**
* 播放模式相关的图标和文字
*/
export interface SAPlayModeIcon {
url: ResourceStr
mode: SAPlayMode
name: string
}
// 支持的播放的资源url
export type SongItemUrl = string | media.AVFileDescriptor | media.AVDataSrcDescriptor
simple_audio
播放器组件的代码
import { SAPlayMode, SAPlayModeIcon, SongItemUrl } from '../type'
import { getRandomFromRange, sa_throttle, sa_timeFormat } from '../utils'
import { EfAVPlayer, EfAVPlayerState } from '[@yunkss](/user/yunkss)/ef_audio'
import { promptAction } from '@kit.ArkUI'
@Component
export struct simple_audio {
/**
* 播放模式
*/
@State
saPlayMode: SAPlayMode = SAPlayMode.order
/**
* 播放模式的图标
*/
@State
saPlayModeIcon: ResourceStr = $r("app.media.order")
/**
* 播放模式图标列表
*/
@State
playModeIcons: SAPlayModeIcon[] = [
{
mode: SAPlayMode.order,
url: $r("app.media.order"),
name: "顺序播放"
},
{
mode: SAPlayMode.single,
url: $r("app.media.single"),
name: "单曲循环"
},
{
mode: SAPlayMode.repeat,
url: $r("app.media.repeat"),
name: "列表循环"
},
{
mode: SAPlayMode.random,
url: $r("app.media.random"),
name: "随机播放"
},
]
/**
* 播放模式的下标
*/
@State
@Watch("_onPlayModeIndex")
playModeIndex: number = 0
/**
* 歌曲时长 单位毫秒
*/
@State
duration: number = 0
/**
* 歌曲播放进度 单位毫秒
*/
@State
currentTime: number = 0
/**
* 是否正在播放
*/
@State
isPlay: boolean = true
/**
* 显示播放模式的弹窗
*/
@State
showPlayModePopup: boolean = false
showPlayModePopupTid: number = -1
/**
* 要播放的歌曲列表
*/
@Prop
songList: SongItemUrl[] = []
/**
* 正在播放的歌曲列表的下标
*/
@Link
@Watch("_onPlayIndexChange")
songPlayIndex: number
/**
* 正在播放的歌曲的地址
*/
@State
songPlayUrl: SongItemUrl = this.songList[this.songPlayIndex]
/**
* 封装了播放功能的AVPlay库
*/
efAVPlay: EfAVPlayer = new EfAVPlayer()
/**
* 初始化播放的方法
*/
initPlay = sa_throttle(() => {
this.efAVPlay.onError(stateError => {
this.onError(stateError)
console.log("stateError", JSON.stringify(stateError))
})
this.efAVPlay.onStateChange(state => {
this._onStateChange(state)
switch (state) {
case "playing":
this.isPlay = true
break
case "completed":
// 播放完毕
this._onNext()
// 切换下一首
break;
default:
break;
}
})
this.efAVPlay.onTimeUpdate(time => {
this.duration = this.efAVPlay.duration
this.currentTime = time
this.onTimeUpdate(time)
})
this.efAVPlay.setUrl(this.songPlayUrl as string)
}, 300)
/**
* 是否显示音量大小控件
*/
@State
showVolumn: boolean = false
/**
* 音量属性 0-1 包含小数
*/
@Prop
@Watch("_onVolumnChange")
volume: number = 1
/**
* 是否静音
*/
@Prop
isMuted: boolean = false
/**
* 下一首
*/
_onNext = async () => {
if (!(this.saPlayMode === SAPlayMode.order && this.songPlayIndex + 1 >= this.songList.length)) {
await this.efAVPlay.reset()
}
switch (this.saPlayMode) {
case SAPlayMode.order:
if (this.songPlayIndex + 1 >= this.songList.length) {
promptAction.showToast({ message: `已经是最后一首` })
return
} else {
this.songPlayIndex++
}
break;
case SAPlayMode.single:
await this.efAVPlay.reset()
break
case SAPlayMode.repeat:
if (this.songPlayIndex + 1 >= this.songList.length) {
this.songPlayIndex = 0
} else {
this.songPlayIndex++
}
break
case SAPlayMode.random:
this.songPlayIndex = getRandomFromRange(0, this.songList.length - 1)
break
default:
break;
}
this._onPlayIndexChange()
this.efAVPlay.setUrl(this.songPlayUrl as string)
.then(() => {
this.onNext(this.songPlayIndex)
})
}
/**
* 上一首
*/
_onPrevious = async () => {
await this.efAVPlay.reset()
if (this.songPlayIndex - 1 < 0) {
this.songPlayIndex = this.songList.length - 1
} else {
this.songPlayIndex--
}
this._onPlayIndexChange()
this.efAVPlay.setUrl(this.songPlayUrl as string)
.then(() => {
this.onPrevious(this.songPlayIndex)
})
}
/**
* 下一首
*/
onNext: (index: number) => void = () => {
}
/**
* 上一首
*/
onPrevious: (index: number) => void = () => {
}
/**
* 静音
*/
onMuted: () => void = () => {
}
/**
* 错误监听
*/
onError: (error: Error) => void = () => {
}
/**
* 实时播放
*/
onTimeUpdate: (time: number) => void = () => {
}
/**
* 调整音量
*/
onVolumnChange: (v: number) => void = () => {
}
_onVolumnChange(v: number) {
// this.efAVPlay
this.efAVPlay.setPlayOptions({ volume: this.volume })
this.onVolumnChange(this.volume)
}
volumnBuilder() {
Column({ space: 2 }) {
Slider({
value: this.efAVPlay.volume,
min: 0,
max: 1,
step: 0.01,
direction: Axis.Vertical,
reverse: true
})
.height(100)
.onChange((v, e) => {
if (e === SliderChangeMode.Moving) {
this.volume = v
this.efAVPlay.setPlayOptions({ volume: v })
if (this.volume === 0) {
this.isMuted = true
this.onMuted()
} else {
this.isMuted = false
}
}
})
.onTouch(v => {
if (v.type === TouchType.Up) {
this.showVolumn = false
}
})
Text(`${Math.floor(this.volume * 100)}%`)
.fontSize(12)
.fontColor("#666")
}
.padding({ bottom: 5 })
}
/**
* 切换歌曲
*/
onPlayIndexChange: (index: number) => void = () => {
}
_onPlayIndexChange() {
this.songPlayUrl = this.songList[this.songPlayIndex]
this.onPlayIndexChange(this.songPlayIndex)
}
aboutToAppear() {
this.efAVPlay.init()
.then(async () => {
this._onPlayIndexChange()
if (this.isPlay) {
this.initPlay()
}
})
}
aboutToDisappear() {
this.efAVPlay.release()
}
/**
* 播放进度改变
*/
onSeek: (time: number) => void = () => {
}
/**
* 设置播放进度
*/
setSeek = (time: number) => {
this.efAVPlay.seek(time)
this.onSeek(time)
}
/**
* 暂停
*/
onPaused: () => void = () => {
}
/**
* 暂停
*/
_onPaused = () => {
this.efAVPlay.pause()
this.onPaused()
}
/**
* 播放
*/
onPlay: () => void = () => {
}
/**
* 播放
*/
_onPlay = () => {
this.efAVPlay.play()
this.onPlay()
}
/**
* 切换播放状态
*/
togglePlay = () => {
this.isPlay = !this.isPlay
if (this.isPlay) {
this._onPlay()
} else {
this._onPaused()
}
}
/**
* 播放模式改变
*/
onPlayModeIndex: (mode: SAPlayMode) => void = () => {
}
_onPlayModeIndex() {
this.saPlayMode = this.playModeIcons[this.playModeIndex].mode
this.saPlayModeIcon = this.playModeIcons[this.playModeIndex].url
this.onPlayModeIndex(this.saPlayMode)
}
/**
* 播放状态改变 和 AVPlayer内置的state一致
*/
onStateChange: (state: EfAVPlayerState) => void = () => {
}
/**
* 播放状态改变 和 AVPlayer内置的state一致
*/
_onStateChange = (state: EfAVPlayerState) => {
this.onStateChange(state)
}
/**
* 切换播放模式
*/
togglePlayMode = () => {
if (this.playModeIndex + 1 >= this.playModeIcons.length) {
this.playModeIndex = 0
} else {
this.playModeIndex++
this.showPlayModePopup = true
}
if (this.showPlayModePopupTid !== -1) {
clearTimeout(this.showPlayModePopupTid)
}
this.showPlayModePopupTid = setTimeout(() => {
this.showPlayModePopup = false
this.showPlayModePopupTid = -1
}, 1000)
// this.onPlayModeIndex()
}
build() {
Column() {
// 1 进度条
Row() {
Slider({ min: 0, max: this.duration, value: this.currentTime })
.layoutWeight(1)
.blockColor("#eee")
.trackColor("#eef")
.selectedColor("#bee")
.onChange(this.setSeek)
}
// 2 播放时间
Row() {
Text(sa_timeFormat(this.currentTime))
.fontColor("#aaa")
.fontSize(12)
Text(sa_timeFormat(this.duration))
.fontColor("#aaa")
.fontSize(12)
}
.justifyContent(FlexAlign.SpaceBetween)
.width("100%")
.padding({ left: 15, right: 15 })
// 3 按钮
Row() {
// 播放模式
Row() {
Image(this.saPlayModeIcon)
.setsetIconStyle()
.onClick(this.togglePlayMode)
.bindPopup($$this.showPlayModePopup, { message: this.playModeIcons[this.playModeIndex].name })
}
.setImageRowIconStyle()
// 上一首
Row() {
Image($r("app.media.previous"))
.setsetIconStyle()
}
.setImageRowIconStyle()
.onClick(this._onPrevious)
// 播放&暂停
Row() {
Image(this.isPlay ? $r("app.media.paused") : $r("app.media.play"))
.setsetIconStyle()
.onClick(this.togglePlay)
}
.setImageRowIconStyle()
.scale({ x: 1.2, y: 1.2 })
// 下一首
Row() {
Image($r("app.media.next"))
.setsetIconStyle()
}
.setImageRowIconStyle()
.onClick(this._onNext)
// 静音
Row() {
Image(this.isMuted ? $r("app.media.muted") : $r("app.media.voice"))
.setsetIconStyle()
}
.setImageRowIconStyle()
.onClick(() => {
this.showVolumn = true
})
.bindPopup($$this.showVolumn, {
builder: this.volumnBuilder(),
width: 40,
placement: Placement.Top
})
}.width("100%")
.padding({ left: 15, right: 15 })
.justifyContent(FlexAlign.SpaceAround)
.margin({ top: 20 })
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
.padding(5)
}
}
@Extend(Row)
function setImageRowIconStyle() {
.width(50)
.aspectRatio(1)
.borderRadius(25)
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.border({
width: 1, color: "#eee"
})
}
@Extend(Image)
function setsetIconStyle() {
.width(30)
.fillColor("#666")
}
props
属性 | 类型 | 说明 |
---|---|---|
songList | SongItemUrl[] | 要播放的歌曲列表 |
volume | number | 音量属性 0-1 包含小数 |
isMuted | boolean | 是否静音 |
efAVPlay | EfAVPlayer | ef_auido 核心类 |
event
事件 | 说明 |
---|---|
onNext | 下一首 |
onPrevious | 上一首 |
onMuted | 静音 |
onError | 错误监听 |
onTimeUpdate | 实时播放 |
onVolumnChange | 音量调整 |
onPlayIndexChange | 歌曲切换 |
onSeek | 播放进度 |
onPaused | 暂停 |
onPlay | 播放 |
onPlayModeIndex | 播放模式改变 |
onStateChange | 播放状态改变 |
后期规划
- 提供不同的主体皮肤
- 提供歌词显示
- 提供歌曲封面显示
- 提供播放列表显示
注意
- 在播放网络歌曲时,记得添加网络权限
- 已经给当前想开通了奔溃服务
更多关于HarmonyOS 鸿蒙Next开发模板/组件分享 – 极简播放器(simple_audio)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
更多关于HarmonyOS 鸿蒙Next开发模板/组件分享 – 极简播放器(simple_audio)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
针对帖子标题“HarmonyOS 鸿蒙Next开发模板/组件分享 – 极简播放器(simple_audio)”,以下是与鸿蒙系统直接相关的回答:
在HarmonyOS鸿蒙系统中,极简播放器(simple_audio)作为一个开发模板或组件,通常包含了播放音频所需的基本功能和界面设计。这个组件可能利用了鸿蒙系统的原生API来实现音频的播放控制,如播放、暂停、停止等,并且可能支持后台播放和通知栏控制。
开发者在使用这个模板时,可以关注以下几个方面:
- 组件集成:确保极简播放器组件已正确集成到项目中,包括相关的资源文件和代码文件。
- 功能实现:检查播放器组件是否实现了播放、暂停、音量调节等基本功能,并测试这些功能是否正常工作。
- 界面定制:根据需要,可以定制播放器的界面,以符合应用的整体风格和用户体验。
- 系统兼容性:确保极简播放器组件在不同版本的HarmonyOS系统上都能正常工作,没有兼容性问题。
如果在集成或使用极简播放器组件时遇到问题,可以查阅鸿蒙系统的官方文档或开发者社区,以获取更多帮助和指导。如果问题依旧没法解决请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html,