
回复
上一篇实现了一句歌词颜色渐变效果,这篇在上一篇的基础上,完整实现歌词的动态展示,并且实现播放和暂停功能。
最终效果:
歌词解析:
我们把歌词先按行分割,记录每行歌词的开始时间和歌词内容,然后再将每行的歌词按照上一篇的方法分割,依次执行属性动画。
[00:00.000]义勇军进行曲
[00:07.00]
[00:08.44] 起来 [00:09.64] 不愿做 [00:10.84] 奴隶的 [00:12.04] 人们
[00:12.65]
[00:12.85] 把我们的 [00:14.05] 血肉 [00:15.25] 筑成我们 [00:16.45] 新的长城
[00:17.89]
[00:18.99] 中华民族 [00:20.19] 到了 [00:21.39] 最危险的 [00:22.59] 时候
[00:24.26]
[00:25.34] 每个人 [00:26.54] 被迫着 [00:27.74] 发出 [00:28.94] 最后的 [00:30.14] 吼声
[00:30.36]
[00:31.16] 起来 [00:32.36] 起来 [00:33.56] 起来
[00:34.25]
[00:35.37] 我们万众一心 [00:36.57] 冒着敌人的 [00:37.77] 炮火 [00:38.97] 前进
[00:39.27]
[00:40.79] 冒着敌人的 [00:41.99] 炮火 [00:43.19] 前进 [00:44.39] 前进 [00:45.59] 前进 [00:46.79] 进
注意
解析歌词时,需要读取歌词前后的时间,计算动画时长,需要注意时间格式,有的歌词格式下载下来可能时00:00:000,修改时间计算的正则表达式即可,保持歌词内容和解析的统一。
动画优化:
上篇说到animateTo的刷新和V2冲突,如果要实现动画的暂停功能,这里我们使用createAnimator方法,自定义一个动画,然后执行play、pause方法,即可实现动画的播放、暂停功能。
createAnimator()参数:AnimatorOptions
名称 | 说明 |
---|---|
duration | 动画播放的时长 |
easing | 动画插值曲线 |
delay | 动画延时播放时长 |
fill | 是否恢复到初始状态 |
direction | 动画播放模式 |
iterations | 动画播放次数 |
begin | 动画插值起点 |
end | 动画插值终点 |
createAnimator()执行方法和回调
名称 | 说明 |
---|---|
play | 启动动画 |
finish | 结束动画,会触发onFinish回调 |
pause | 暂停动画 |
cancel | 取消动画,会触发onCancel回调 |
onFrame | 接收到帧时回调 |
onFinish | 动画完成时回调 |
onCancel | 动画被取消时回调 |
onRepeat | 动画重复时回调 |
歌词组件封装源码:
// 一行中有单独时间分割的字或词语
export class Word {
index: number = 0;
text: string = '';
startTime: number = 0;
durationTime: number = 0;
}
// 一行歌词
export class LrcLine {
lineStartTime: number = 0;
words: Word[] = [];
}
@ComponentV2
struct LyricView{
private scroller: Scroller = new Scroller();
@Require @Param lrcTitle:string
@Require @Param lrcName:string
@Local mLrcEntryList: LrcLine[] = [];
@Local currentIndex: number = 0; //每词在每句中索引
@Local colorStopValue: number = 0;
@Local currentLineIndex: number = 0; //每句歌词索引
@Local lrcDuration: number = 0; // 歌词持续时间
private lrcAnimator: AnimatorResult | undefined = undefined
@Consumer() displaying: boolean = false
@Monitor('displaying')
play(){
if (this.displaying) {
//播放
this.lrcAnimator?.play()
}else {
//播放
this.lrcAnimator?.pause()
}
}
playLrc(duration: number) {
this.lrcAnimator = this.getUIContext().createAnimator({
duration: duration,
easing: 'linear',
delay: 0,
fill: 'none',
direction: 'normal',
iterations: 1,
begin: 0,
end: 1
})
this.lrcAnimator.onFinish = () => {
this.colorStopValue = 0
this.currentIndex++;
let currentLine = this.mLrcEntryList[this.currentLineIndex]
if (this.currentIndex < currentLine.words.length) {
this.lrcDuration = currentLine.words[this.currentIndex].durationTime;
this.playLrc(this.lrcDuration);
} else {
this.currentIndex = 0
this.currentLineIndex++
if (this.currentLineIndex < this.mLrcEntryList.length) {
let nextLine = this.mLrcEntryList[this.currentLineIndex]
this.lrcDuration = nextLine.words[this.currentIndex].durationTime;
this.playLrc(this.lrcDuration);
}else {
this.currentLineIndex = 0
this.displaying = false
}
}
}
this.lrcAnimator.onFrame = (value: number) => {
this.colorStopValue=value
}
if (this.displaying) this.lrcAnimator.play()
}
async aboutToAppear(): Promise<void> {
let lrcString = await getContext(this).resourceManager.getRawFileContent(this.lrcName);
let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });
let stringData = textDecoder.decodeToString(lrcString, { stream: false });
this.processLrc(stringData.split('\n'));
}
processLrc(lyric: string[]): void {
let lrcList: LrcLine[] = [];
//读取歌词文件中的时间 时间格式为00:00:00
let wordRegex: RegExp = /\[(\d{2}):(\d{2})\.(\d{2})\]([^[\]]+)/g;
let wordIndex = 0;
lyric.forEach((line: string) => {
let words: Word[] = [];
let lineStartTime: number = 0;
let match: RegExpExecArray | null;
while ((match = wordRegex.exec(line)) !== null) {
let mm: number = Number.parseInt(match[1], 10);
let ss: number = Number.parseInt(match[2], 10);
let ms: number = Number.parseInt(match[3], 10);
let text: string = match[4].trim();
let time: number = mm * 60 * 1000 + ss * 1000 + ms * 10;
if (words.length === 0) {
lineStartTime = time;
}
let nextTime = this.extractAllTimestampsFromLyrics(lyric)[wordIndex+1]
let lineTimeDuration: number = nextTime !== undefined ? (nextTime! - time) : 1000;
words.push({
index: wordIndex,
text: text,
startTime: time,
durationTime: lineTimeDuration
});
wordIndex++
}
if (words.length > 0) {
lrcList.push({
lineStartTime: lineStartTime,
words: words
});
}
});
this.mLrcEntryList = lrcList;
}
extractAllTimestampsFromLyrics(lyric: string[]): number[] {
const timestamps: number[] = [];
for (const line of lyric) {
let regex = /\[(\d{2}):(\d{2})\.(\d{2})\]/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(line)) !== null) {
let mm = parseInt(match[1], 10);
let ss = parseInt(match[2], 10);
let ms = parseInt(match[3], 10);
let timeMs = mm * 60 * 1000 + ss * 1000 + ms * 10;
timestamps.push(timeMs);
}
}
// 升序排序(确保时间顺序正确)
return timestamps.sort((a, b) => a - b);
}
build() {
Column(){
Text(this.lrcTitle)
.fontColor(Color.White)
.fontSize(24)
.fontWeight(700)
.opacity(0.86)
.height(40)
Stack() {
List({ initialIndex: 0, scroller: this.scroller }) {
ForEach(this.mLrcEntryList, (line: LrcLine, lineIndex: number) => {
ListItem() {
Column() {
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
ForEach(line.words, (word: Word, wordIndex: number) => {
Row() {
Text(word.text)
.fontSize(this.currentLineIndex==lineIndex?20:16)
.blendMode(BlendMode.DST_IN, BlendApplyType.OFFSCREEN)
}
.margin({
right: word.text.match(/[a-zA-Z0-9]/) ? 4 : 0,
bottom: 2
})
.blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN)
.linearGradient({
direction: GradientDirection.Right,
colors: (() => {
if (this.currentLineIndex !== lineIndex) {
// 当前行未激活,全部白色
return [['#FFFFFFFF', 0], ['#FFFFFFFF', 1.0]];
}
if (wordIndex < this.currentIndex) {
// 已播放的字:全红
return [['#FF0000', 0.0], ['#FF0000', 1.0]];
} else if (wordIndex === this.currentIndex) {
// 正在播放的字:红色渐变(从 0 到 this.colorStopValue)
return [
['#FF0000', 0.0],
['#FF0000', this.colorStopValue],
['#FFFFFF', this.colorStopValue],
['#FFFFFF', 1.0]
];
} else {
// 未播放的字:全白
return [['#FFFFFFFF', 0], ['#FFFFFFFF', 1.0]];
}
})()
})
.onAppear(() => {
this.lrcDuration = line.words[wordIndex].durationTime
if (lineIndex === this.currentLineIndex && wordIndex === 0) {
this.playLrc(line.words[wordIndex].durationTime);
}
})
})
}
.width('90%')
.padding(10)
}
.width('100%')
}
.opacity(lineIndex === this.currentLineIndex ? 1 : 0.8)
})
}
.height('auto')
}
}
}
aboutToDisappear(): void {
this.lrcAnimator?.finish();
this.lrcAnimator = undefined;
}
}
歌词组件使用
import { util } from '@kit.ArkTS';
import { AnimatorResult } from '@kit.ArkUI';
@Entry
@ComponentV2
struct LyricTest {
@Provider() displaying: boolean = false;
build() {
Column({space:20}) {
LyricView({lrcTitle:'义勇军进行曲',lrcName:'sing.lrc'})
Button(this.displaying?'暂停':'开始').fontSize(24).onClick(()=>{
this.displaying=!this.displaying
})
}
.width('100%')
.height('100%')
.linearGradient({
// 渐变方向
direction: GradientDirection.Bottom,
// 渐变颜色是否重复
repeating: true,
// 数组末尾元素占比小于1时满足重复着色效果
colors: [['#17233c', 0.0], ['#2b3755', 0.6], ['#181e34', 1]]
})
}
}