实现方法
1、把歌词解析并转成数组并渲染成多个text
2、监听音频播放事件,跳转到相对应的歌词
(1)定义歌词解析类Lyric.ts
const timeExp:RegExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]/g
enum STATUS {
STATE_PAUSE = 0,
STATE_PLAYING = 1
}
interface TagRegInterface {
title?:string,
artist?:string,
album?:string,
offset?:string,
by?:string,
}
export interface LineInterface {
time?:number,
txt:string,
lineNum?:number
}
const tagRegMap:TagRegInterface = {
title: 'ti',
artist: 'ar',
album: 'al',
offset: 'offset',
by: 'by'
}
export default class Lyric {
private lrc:string = '';
private tags:TagRegInterface = {};
public lines:Array<LineInterface> = []
private state:STATUS = STATUS.STATE_PAUSE;
private curLine:number = 0;
private curNum:number = 0;
private startStamp:number = 0;
private timer:number = 0;
private pauseStamp:number;
private handler:(data:LineInterface) => void
constructor(lrc:string, handlder:(data:LineInterface) => void) {
this.lrc = lrc;
this.handler = handlder;
this._init();
}
_init() {
this._initTag()
this._initLines()
}
_initTag() {
for (let tag in tagRegMap) {
const matches = this.lrc.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i'))
this.tags[tag] = matches && matches[1] || ''
}
}
_initLines() {
const lines:Array<string> = this.lrc.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
let result:Array<string>|null = timeExp.exec(line)
if (result) {
const txt = line.replace(timeExp, '').trim()
if (txt) {
this.lines.push({
time: parseInt(result[1]) * 60 * 1000 + parseInt(result[2]) * 1000 + (parseInt(result[3]) || 0) * 10,
txt
})
}
}
}
this.lines.sort((a, b) => {
return a.time - b.time
})
}
_findCurNum(time) {
for (let i = 0; i < this.lines.length; i++) {
if (time <= this.lines[i].time) {
return i
}
}
return this.lines.length - 1
}
_callHandler(i) {
if (i < 0) {
return
}
this.handler({
txt: this.lines[i].txt,
lineNum: i
})
}
_playRest() {
let line = this.lines[this.curNum]
let delay = line.time - (+new Date() - this.startStamp)
this.timer = setTimeout(() => {
this._callHandler(this.curNum++)
if (this.curNum < this.lines.length && this.state === STATUS.STATE_PLAYING) {
this._playRest()
}
}, delay)
}
play(startTime:number = 0, skipLast:boolean = false) {
if (!this.lines.length) {
return
}
this.state = STATUS.STATE_PLAYING
this.curNum = this._findCurNum(startTime)
this.startStamp = +new Date() - startTime
if (!skipLast) {
this._callHandler(this.curNum - 1)
}
if (this.curNum < this.lines.length) {
clearTimeout(this.timer)
this._playRest()
}
}
togglePlay() {
const now:number = +new Date()
if (this.state === STATUS.STATE_PLAYING) {
this.stop()
this.pauseStamp = now
} else {
this.state = STATUS.STATE_PLAYING
this.play((this.pauseStamp || now) - (this.startStamp || now), true)
this.pauseStamp = 0
}
}
stop() {
this.state =STATUS.STATE_PAUSE
clearTimeout(this.timer)
}
seek(offset) {
this.play(offset)
}
}
2、引入歌词解析类并把歌词解析成数据渲染在页面上,监听音频播放,设置歌词跳转到到对应的时间轴,触发歌词回调函数,得到当前歌词的下标,List组件跳转到对应的下标,当前的歌词显示白色
import * as colors from '../../theme/color';
import * as size from '../../theme/size';
import router from '@ohos.router';
import { MusicInterface } from '../interface/Index';
import display from '@ohos.display';
import {HOST,MUSIC_STORAGE} from '../../config/constant';
import Lyric,{LineInterface} from '../../utils/Lyric';
import media from '@ohos.multimedia.media';
import { formatSecond } from '../../utils/common';
@Entry
@Component
struct MusicPlayerPage {
@State lyric:Lyric = null;
@State musicItem:MusicInterface = null;
@State currentLineNum:number = 0;
@State angle:number = 0;
@State duration:number = 0;
@State currentTime:number = 0;
@State progress:number = 0;
private scroller: Scroller = new Scroller()
@StorageLink(MUSIC_STORAGE) avPlayer:media.AVPlayer | null = null;
private circleSize:number = px2vp(display.getDefaultDisplaySync().width * 0.8)
async aboutToAppear() {
const params = router.getParams(); // 获取传递过来的参数对象
this.musicItem = params['musicItem'] as MusicInterface; // 获取info属性的值
if(this.avPlayer == null){
this.avPlayer = await media.createAVPlayer();
AppStorage.SetOrCreate<media.AVPlayer>(MUSIC_STORAGE, this.avPlayer);
this.setAVPlayerCallback()
this.avPlayer.url = HOST + this.musicItem.localPlayUrl;
}
if (!this.musicItem.lyrics) return;// 如果有歌词
this.lyric = new Lyric(this.musicItem.lyrics, ({ lineNum = 0 }) => {
// 滚动到相对应的歌词
this.scroller.scrollToIndex(lineNum);
// 当前播放的歌词下标
this.currentLineNum = lineNum;
})
}
// 注册avplayer回调函数
setAVPlayerCallback() {
// seek操作结果回调函数
this.avPlayer.on('seekDone', (seekDoneTime) => {
console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
})
// error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
this.avPlayer.on('error', (err) => {
this.avPlayer.reset(); // 调用reset重置资源,触发idle状态
})
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state:string) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
this.avPlayer.release(); // 调用release接口销毁实例对象
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
this.avPlayer.prepare().then(() => {
}, (err) => {
console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`);
});
break;
case 'prepared': // prepare调用成功后上报该状态机
this.avPlayer.play(); // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
console.log('playing')
break
case 'paused': // pause成功调用后触发该状态机上报
this.avPlayer.play(); // 再次播放接口开始播放
break;
case 'completed': // 播放结束后触发该状态机上报
this.avPlayer.stop(); //调用播放结束接口
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
this.avPlayer.reset(); // 调用reset接口初始化avplayer状态
break;
case 'released':
break;
default:
break;
}
});
// 当前播放时长
this.avPlayer.on('timeUpdate',(millisecond:number)=>{
this.currentTime = millisecond;
this.angle += 5;
if(this.angle === 360)this.angle = 0;
if(this.duration){
this.progress = Math.ceil((this.currentTime / this.duration) * 100)
}
this.lyric?.seek(Math.floor(millisecond));// 歌词跳转到对应的时间线
})
// 总时长
this.avPlayer.on('durationUpdate',(millisecond:number)=>{
this.duration = millisecond
})
}
build(){
Column({space:size.pagePadding}){
Text(this.musicItem.songName)
.margin({top:size.pagePadding,bottom:size.pagePadding})
.fontColor(colors.blockColor)
.fontSize(size.bigFontSize)
Row(){
Row(){
Image(HOST + this.musicItem.cover)
.width('100%')
.aspectRatio(1)
.borderRadius(this.circleSize - size.smallPadding * 12)
}.linearGradient({
direction: GradientDirection.Top, // 渐变方向
repeating: true, // 渐变颜色是否重复
colors: [[0x000000, 0.0], [0x333333, 0.5], [0x000000, 1]] // 数组末尾元素占比小于1时满足重复着色效果
})
.width(this.circleSize - size.smallPadding * 2)
.aspectRatio(1)
.rotate({ angle: this.angle })
.padding(size.smallPadding * 5)
.borderRadius(this.circleSize - size.smallPadding * 2)
}
.border({
width: size.smallPadding,
color: colors.playerOuterCircleColor,
style: BorderStyle.Solid
})
.borderRadius(this.circleSize)
.width('80%')
.aspectRatio(1)
// 歌词
if(this.lyric?.lines.length > 0){
// 歌词列表
List({space:size.miniPadding,scroller:this.scroller}) {
ForEach(this.lyric?.lines, (item: LineInterface, index: number) => {
ListItem(){
Text(item.txt)
.fontColor(colors.blockColor)
.opacity(this.currentLineNum === index ? 1 : 0.5)
.alignSelf(ItemAlign.Center)
}.width('100%')
})
}
.width('100%')
.layoutWeight(1)
}else{
Row(){
Text("暂无歌词").fontColor(colors.blockColor).opacity(0.5)
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
}
Text(this.musicItem.authorName).width('80%').fontColor(colors.blockColor)
Row(){
Image($r('app.media.icon_music_collect')).width(size.middlIconSize).aspectRatio(1)
Image($r('app.media.icon_music_down')).width(size.middlIconSize).aspectRatio(1)
Image($r('app.media.icon_music_comments')).width(size.middlIconSize).aspectRatio(1)
Image($r('app.media.icon_music_white_menu')).width(size.middlIconSize).aspectRatio(1)
}.width('80%').justifyContent(FlexAlign.SpaceBetween)
Row({space:size.smallPadding}){
Text(this.currentTime ? formatSecond(this.currentTime/1000) : '00:00').fontColor(colors.blockColor)
Slider({value:this.progress})
.selectedColor(colors.blockColor)
.layoutWeight(1)
Text(this.duration ? formatSecond(this.duration/1000) : '00:00').fontColor(colors.blockColor)
}.width('80%').alignItems(VerticalAlign.Center)
Row(){
Image($r('app.media.icon_music_order')).width(size.middlIconSize).aspectRatio(1)
Image($r('app.media.icon_music_prev')).width(size.middlIconSize).aspectRatio(1)
Row(){
Image($r('app.media.icon_music_play_white')).width(size.middlIconSize).aspectRatio(1)
}
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.width(size.bigAvaterSize)
.aspectRatio(1)
.borderRadius(size.bigAvaterSize)
.border({
width: 2,
color: colors.blockColor,
style: BorderStyle.Solid
})
Image($r('app.media.icon_music_next')).width(size.middlIconSize).aspectRatio(1)
Image($r('app.media.icon_music_play_menu')).width(size.middlIconSize).aspectRatio(1)
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
.margin({bottom:size.pagePadding})
}
.backgroundImage(HOST + this.musicItem.cover,ImageRepeat.NoRepeat)
.backgroundImagePosition({x:0,y:0})
.backgroundImageSize({width:'100%',height:'100%'})
.backdropBlur(200)
.alignItems(HorizontalAlign.Center)
.height('100%')
.width('100%')
}
}