Преглед изворни кода

修复歌词的卡拉ok效果

onecold пре 7 месеци
родитељ
комит
9ad10777f3
2 измењених фајлова са 307 додато и 19 уклоњено
  1. 26 2
      lib/src/main/ets/bean/LyricLine.ts
  2. 281 17
      lib/src/main/ets/view/LyricView2.ets

+ 26 - 2
lib/src/main/ets/bean/LyricLine.ts

@@ -21,8 +21,32 @@ export class LyricLine {
         this.translation = translation || ''
         this.translation = translation || ''
     }
     }
 
 
-    // 新增方法:判断是否有逐字歌词
+    /**
+     * 判断该行是否包含有效的逐字歌词数据
+     * 判断标准:
+     * 1. words 数组不为空
+     * 2. 每个 word 对象都有有效的开始时间(>= 0)
+     * 3. 每个 word 对象都有有效的时长(> 0)
+     * 4. 至少有一个 word 包含非空文本
+     * @returns true 表示是逐字歌词格式,false 表示是普通歌词格式
+     */
     hasWords(): boolean {
     hasWords(): boolean {
-        return this.words.length > 0
+        // 如果 words 数组为空,肯定不是逐字歌词
+        if (!this.words || this.words.length === 0) {
+            return false;
+        }
+
+        // 检查是否有至少一个有效的 word 对象
+        let hasValidWord = false;
+        for (let i = 0; i < this.words.length; i++) {
+            const word = this.words[i];
+            // word 必须有:有效的开始时间、有效的时长、非空文本
+            if (word && word.startTime >= 0 && word.duration > 0 && word.word && word.word.length > 0) {
+                hasValidWord = true;
+                break;
+            }
+        }
+
+        return hasValidWord;
     }
     }
 }
 }

+ 281 - 17
lib/src/main/ets/view/LyricView2.ets

@@ -5,7 +5,7 @@ import { ListAdapter } from '../extensions/ListAdapter';
 import { LyricLine } from '../bean/LyricLine';
 import { LyricLine } from '../bean/LyricLine';
 import { LyricWord } from '../bean/LyricWord';
 import { LyricWord } from '../bean/LyricWord';
 import { transverter, TransverterType, TransverterLanguage } from "@nutpi/chinese_transverter"
 import { transverter, TransverterType, TransverterLanguage } from "@nutpi/chinese_transverter"
-import { LengthMetrics } from '@kit.ArkUI';
+import { curves, LengthMetrics } from '@kit.ArkUI';
 
 
 /**
 /**
  * A component to display the lyric with scroll animation.
  * A component to display the lyric with scroll animation.
@@ -190,25 +190,33 @@ export struct LyricView2 {
                             this.NormalLyricLine(item, index)
                             this.NormalLyricLine(item, index)
                         }
                         }
 
 
+                        if (this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0
+                            && this.enableSeek && this.isUserTouching){
+                            this.JumpProgress()
+                        }
 
 
-                            Text(this.scrollDurationText)
-                                .fontSize(this.textSize)
-                                .fontColor(this.seekUIColor)
-                                .textAlign( TextAlign.End)
-                                .width(100)
-                                .visibility(this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0
-                                      && this.enableSeek && this.isUserTouching?Visibility.Visible:Visibility.Hidden)
 
 
                     }
                     }
                     .align(Alignment.End)
                     .align(Alignment.End)
 
 
                 }
                 }
                 .padding(8)
                 .padding(8)
-                .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
-                    TransitionEffect.scale({ x: 0, y: 0 })  ))
+                // .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
+                //     TransitionEffect.scale({ x: 0, y: 0 })  ))
+                .transition(
+                    TransitionEffect
+                        .scale({ x: 0.9, y: 0.9 })
+                        .combine(TransitionEffect
+                            .opacity(0.1)
+                        )
+                        .animation({
+                            duration: 150,
+                            curve: Curve.EaseInOut,
+                        })
+                )
                 .border({ radius: 12 })
                 .border({ radius: 12 })
-                .backgroundColor(this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0
-                    && this.enableSeek && this.isUserTouching ? '#80a9a9a9' : '#00000000')
+                // .backgroundColor(this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0
+                //     && this.enableSeek && this.isUserTouching ? '#80a9a9a9' : '#00000000')
                 .onClick(() => {
                 .onClick(() => {
                     if (this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0) {
                     if (this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0) {
                         if (this.seekIndex != -1 && this.seekIndex != this.currentIndex) {
                         if (this.seekIndex != -1 && this.seekIndex != this.currentIndex) {
@@ -223,13 +231,22 @@ export struct LyricView2 {
         }
         }
         .width('100%')
         .width('100%')
         .height('100%')
         .height('100%')
+        .layoutWeight(1)
+        .chainAnimation(true)
         .scrollBar(BarState.Off)
         .scrollBar(BarState.Off)
         .fadingEdge(true,{fadingEdgeLength:LengthMetrics.percent(20)})
         .fadingEdge(true,{fadingEdgeLength:LengthMetrics.percent(20)})
+        .edgeEffect(EdgeEffect.Spring)
+        .contentEndOffset(this.h / 3)
+        .contentStartOffset(this.isUserTouching?this.h / 3:0)
         .cachedCount(this.cacheSize)
         .cachedCount(this.cacheSize)
-        .transition(TransitionEffect.asymmetric(
-             this.isSingleLine? TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }):
-             TransitionEffect.move(TransitionEdge.BOTTOM).animation({ duration: 500 }),
-            TransitionEffect.scale({ x: 0, y: 0 })  ))
+        .animation({
+            curve: curves.springCurve(100, 10, 80, 10),
+            duration: 500
+        })
+        // .transition(TransitionEffect.asymmetric(
+        //      this.isSingleLine? TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }):
+        //      TransitionEffect.move(TransitionEdge.BOTTOM).animation({ duration: 500 }),
+        //     TransitionEffect.scale({ x: 0, y: 0 })  ))
         .visibility(this.isLoadingData ? Visibility.Hidden : Visibility.Visible)
         .visibility(this.isLoadingData ? Visibility.Hidden : Visibility.Visible)
         .onScrollIndex((_, __, center) => {
         .onScrollIndex((_, __, center) => {
             const now = Date.now()
             const now = Date.now()
@@ -283,7 +300,14 @@ export struct LyricView2 {
                 })
                 })
                 .fontColor(index == this.currentIndex ? this.textHighlightColor : this.textColor)
                 .fontColor(index == this.currentIndex ? this.textHighlightColor : this.textColor)
                 .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
                 .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
-                .animation({ duration: this.animDuration, curve: Curve.FastOutSlowIn })
+                .animation({
+                    duration: 150,
+                    curve: Curve.Linear
+                })
+                .blendMode(
+                    index == this.currentIndex ? BlendMode.DST_IN : undefined,
+                    index == this.currentIndex ? BlendApplyType.OFFSCREEN : undefined
+                )
                 .visibility(this.isSingleLine?
                 .visibility(this.isSingleLine?
                     (index == this.currentIndex ?Visibility.Visible:Visibility.None)
                     (index == this.currentIndex ?Visibility.Visible:Visibility.None)
                     :Visibility.Visible)
                     :Visibility.Visible)
@@ -303,7 +327,118 @@ export struct LyricView2 {
                 .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex ?'95%': '85%')
                 .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex ?'95%': '85%')
             }
             }
         }
         }
+        // 在 Row 上应用渐变
+        .linearGradient(index == this.currentIndex ? {
+            direction: GradientDirection.Right,
+            colors: this.getLyricItemLinearGradient(item, index)
+        } : undefined)
+        .blendMode(
+            index == this.currentIndex ? BlendMode.SRC_OVER : undefined,
+            index == this.currentIndex ? BlendApplyType.OFFSCREEN : undefined
+        )
+
+    }
+
+
+
+    /**
+     * 计算卡拉OK渐变 - 同色系从浅到深的平滑过渡
+     */
+    getLyricItemLinearGradient(item: LyricLine, index: number): [ResourceColor, number][] {
+        // 只对当前播放行且包含逐字数据的行应用卡拉OK效果
+        if (index !== this.currentIndex || !item.hasWords() || item.words.length === 0) {
+            //console.info('heanup', `getLyricItemLinearGradient - 非高亮行或无逐字数据: index=${index}, currentIndex=${this.currentIndex}, hasWords=${item.hasWords()}, wordsCount=${item.words.length}`)
+            return [[Color.Transparent, 0.0], [Color.Transparent, 1.0]]
+        }
+
+        // 计算该行歌词的总时长
+        let lyricDuration: number
+        if (index < this.listAdapter.totalCount() - 1) {
+            const nextLine = this.listAdapter.getData(index + 1)
+            lyricDuration = nextLine.beginTime - item.beginTime
+        } else {
+            // 最后一行,使用 nextTime(如果有)或者估计时长
+            lyricDuration = item.nextTime > item.beginTime ? item.nextTime - item.beginTime : 5000
+        }
+
+        //console.info('heanup', `getLyricItemLinearGradient - index=${index}, lyricDuration=${lyricDuration}, currentMediaPosition=${this.currentMediaPosition}, itemBeginTime=${item.beginTime}`)
+
+        if (lyricDuration <= 0) {
+            console.info('heanup', `getLyricItemLinearGradient - 歌词时长<=0, 返回透明`)
+            return [[Color.Transparent, 0.0], [Color.Transparent, 1.0]]
+        }
+
+        // 计算当前播放进度(0-1之间)
+        let diff = this.currentMediaPosition - item.beginTime
+        let value = diff / lyricDuration
+
+        value = Math.max(0, Math.min(1, value))
+
+        //console.info('heanup', `getLyricItemLinearGradient - diff=${diff}, value=${value}, textHighlightColor=${this.textHighlightColor}, textColor=${this.textColor}`)
+
+        return [[this.textHighlightColor, 0.0],
+            [this.textHighlightColor, value],
+            [this.textColor, value],
+            [this.textColor, 1.0]]
+
+    }
+
+    /**
+     * 计算逐字歌词的卡拉OK渐变效果
+     * 该方法针对逐字歌词格式,根据当前播放进度和每个字的时间信息计算渐变
+     * @param item 当前歌词行
+     * @param word 当前字的信息
+     * @param index 当前行索引
+     * @returns 渐变颜色数组
+     */
+    getWordByWordLyricLyricItemLinearGradient(item: LyricLine, word: LyricWord, index: number): [ResourceColor, number][] {
+        // 非高亮行或无效数据,返回透明
+        if (index !== this.currentIndex || !word || !word.word) {
+            console.info('heanup', `getWordByWordLyricLyricItemLinearGradient - 非高亮行或无效word: index=${index}, currentIndex=${this.currentIndex}`)
+            return [[Color.Transparent, 0.0], [Color.Transparent, 1.0]]
+        }
+
+        // 计算该字的播放进度
+        const wordEndTime = word.startTime + word.duration
+        const wordDuration = word.duration
+
+        // 异常情况处理
+        if (wordDuration <= 0) {
+            console.info('heanup', `getWordByWordLyricLyricItemLinearGradient - word时长<=0: startTime=${word.startTime}, duration=${word.duration}`)
+            // 如果时长无效,检查当前播放位置是否已到达开始时间
+            if (this.currentMediaPosition >= word.startTime) {
+                // 已开始播放,全部高亮
+                return [[this.textHighlightColor, 0.0], [this.textHighlightColor, 1.0]]
+            } else {
+                // 未开始播放,全部普通颜色
+                return [[this.textColor, 0.0], [this.textColor, 1.0]]
+            }
+        }
+
+        // 计算当前在该字内的播放进度(0-1之间)
+        let progress = 0
+        if (this.currentMediaPosition < word.startTime) {
+            // 还没播放到这个字
+            progress = 0
+        } else if (this.currentMediaPosition >= wordEndTime) {
+            // 这个字已经播放完
+            progress = 1
+        } else {
+            // 正在播放这个字,计算进度
+            const diff = this.currentMediaPosition - word.startTime
+            progress = diff / wordDuration
+            progress = Math.max(0, Math.min(1, progress))
+        }
 
 
+        console.info('heanup', `getWordByWordLyricLyricItemLinearGradient - word=${word.word}, progress=${progress}, currentPos=${this.currentMediaPosition}, wordStart=${word.startTime}, wordEnd=${wordEndTime}`)
+
+        // 返回卡拉OK渐变效果
+        // 0.0 到 progress:高亮色(已播放部分)
+        // progress 到 1.0:普通色(未播放部分)
+        return [[this.textHighlightColor, 0.0],
+            [this.textHighlightColor, progress],
+            [this.textColor, progress],
+            [this.textColor, 1.0]]
     }
     }
 
 
     @Builder
     @Builder
@@ -323,6 +458,10 @@ export struct LyricView2 {
                         .visibility(this.isSingleLine?
                         .visibility(this.isSingleLine?
                             (index == this.currentIndex ?Visibility.Visible:Visibility.None)
                             (index == this.currentIndex ?Visibility.Visible:Visibility.None)
                             :Visibility.Visible)
                             :Visibility.Visible)
+                        .shaderStyle(index == this.currentIndex ?{
+                            direction: GradientDirection.Right,
+                            colors: this.getWordByWordLyricLyricItemLinearGradient(item, word, index)
+                        }:undefined)
                         .animation({
                         .animation({
                             // 动画播放速度
                             // 动画播放速度
                             tempo: 0.8,
                             tempo: 0.8,
@@ -355,6 +494,81 @@ export struct LyricView2 {
     }
     }
 
 
 
 
+    // @Builder
+    // Normalyric(item: LyricLine, index: number) {
+    //     Column() {
+    //         // 完全复制 LyricList 的实现结构
+    //         Row({ space: 10 }) {
+    //             Text(item.text)
+    //                 .maxLines(index == this.currentIndex ? 1 : 2)
+    //                 .textOverflow({ overflow: TextOverflow.Ellipsis })
+    //                 .padding(5)
+    //                 .textAlign(this.alignMode == 'center' ? TextAlign.Center : TextAlign.Start)
+    //                 .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
+    //                 .width('100%')
+    //                 .fontColor(index == this.currentIndex ? this.textHighlightColor : this.textColor)
+    //                 .fontSize(index == this.currentIndex ? this.textSize * this.controller.getHighlightScale() : this.textSize)
+    //                 .blendMode(
+    //                     index == this.currentIndex ? BlendMode.DST_IN : undefined,
+    //                     index == this.currentIndex ? BlendApplyType.OFFSCREEN : undefined
+    //                 )
+    //                 // 关键:添加线性动画,使渐变平滑过渡
+    //                 .animation({
+    //                     duration: 500,
+    //                     curve: Curve.Linear
+    //                 })
+    //         }
+    //         // 在 Row 上应用渐变
+    //         .linearGradient(index == this.currentIndex ? {
+    //             direction: GradientDirection.Right,
+    //             colors: this.getLyricItemLinearGradient(item, index)
+    //         } : undefined)
+    //         .blendMode(
+    //             index == this.currentIndex ? BlendMode.SRC_OVER : undefined,
+    //             index == this.currentIndex ? BlendApplyType.OFFSCREEN : undefined
+    //         )
+    //         .borderRadius(5)
+    //         .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
+    //         .width(this.alignMode == 'center' ? '100%' : (index == this.currentIndex ? '95%' : '85%'))
+    //         .visibility(this.isSingleLine ?
+    //             (index == this.currentIndex ? Visibility.Visible : Visibility.None)
+    //             : Visibility.Visible)
+    //
+    //         // 中文翻译(整行显示)
+    //         if (item.translation) {
+    //             Row({ space: 0 }) {
+    //                 Text(item.translation)
+    //                     .fontSize(index == this.currentIndex ? this.textSize * this.controller.getHighlightScale() : this.textSize)
+    //                     .fontColor(this.currentMediaPosition >= item.beginTime ?
+    //                         index == this.currentIndex ? this.textHighlightColor : this.textColor : this.textColor)
+    //                     .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
+    //                     .margin({ top: 4 })
+    //                     .blendMode(
+    //                         index == this.currentIndex ? BlendMode.DST_IN : undefined,
+    //                         index == this.currentIndex ? BlendApplyType.OFFSCREEN : undefined
+    //                     )
+    //                     // 关键:添加线性动画,使渐变平滑过渡
+    //                     .animation({
+    //                         duration: 150,
+    //                         curve: Curve.Linear
+    //                     })
+    //             }
+    //             .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
+    //             .width(this.alignMode == 'center' ? '100%' : (index == this.currentIndex ? '95%' : '85%'))
+    //             // 在 Row 上应用渐变
+    //             .linearGradient(index == this.currentIndex ? {
+    //                 direction: GradientDirection.Right,
+    //                 colors: this.getLyricItemLinearGradient(item, index)
+    //             } : undefined)
+    //             .blendMode(
+    //                 index == this.currentIndex ? BlendMode.SRC_OVER : undefined,
+    //                 index == this.currentIndex ? BlendApplyType.OFFSCREEN : undefined
+    //             )
+    //         }
+    //     }
+    // }
+
+
 
 
     isTopBottomLine(index:number){
     isTopBottomLine(index:number){
         return  index === 0 || index === this.listAdapter.totalCount()  - 1;
         return  index === 0 || index === this.listAdapter.totalCount()  - 1;
@@ -399,6 +613,56 @@ export struct LyricView2 {
 
 
     }
     }
 
 
+    @Builder
+    JumpProgress(){
+        Row(){
+            Row(){
+                Divider()
+                    .width(10)
+                    .strokeWidth(2)
+                    .color(Color.Transparent)
+                    .foregroundBlurStyle(BlurStyle.BACKGROUND_REGULAR,
+                        { colorMode: ThemeColorMode.LIGHT, adaptiveColor: AdaptiveColor.DEFAULT, scale: 1.0 })
+
+                Row({space: 10}){
+                    Text(this.scrollDurationText)
+                        .fontSize(12)
+                        .fontWeight(FontWeight.Medium)
+                        .fontColor(Color.White)
+
+                    SymbolGlyph($r('sys.symbol.play'))
+                        .fontSize(13)
+                        .fontColor([Color.White])
+                        .alignSelf(ItemAlign.Center)
+                }
+                .borderRadius(10)
+                .alignItems(VerticalAlign.Center)
+                .height(32)
+                .padding(10)
+                .backgroundColor(Color.Transparent)
+                .backgroundBlurStyle(BlurStyle.BACKGROUND_REGULAR,
+                    { colorMode: ThemeColorMode.LIGHT, adaptiveColor: AdaptiveColor.DEFAULT, scale: 1.0 })
+            }
+            .transition(
+                TransitionEffect
+                    .scale({ x: 0.7, y: 0.7 })
+                    .combine(TransitionEffect
+                        .opacity(0.1)
+                    )
+                    .animation({
+                        duration: 150,
+                        curve: Curve.EaseInOut,
+                    })
+            )
+        }
+        .hitTestBehavior(HitTestMode.Transparent)
+        .width(110)
+        .justifyContent(FlexAlign.End)
+        .backgroundColor(Color.Transparent)
+
+    }
+
+
     @Builder
     @Builder
     SeekLine() {
     SeekLine() {
         Row() {
         Row() {