onecold 5 месяцев назад
Родитель
Сommit
62b8d0a0d6
2 измененных файлов с 147 добавлено и 103 удалено
  1. 27 18
      entry/src/main/ets/view/LocalMusic.ets
  2. 120 85
      lib/src/main/ets/view/LyricView2.ets

+ 27 - 18
entry/src/main/ets/view/LocalMusic.ets

@@ -506,7 +506,7 @@ export struct LocalMusic {
 
   // 分页相关状态
   @State private currentPage: number = 0          // 当前页码
-  @State private pageSize: number = 80            // 每页数量
+  @State private pageSize: number = 53            // 每页数量
   @State private totalCount: number = 0           // 总记录数
   @State private hasMoreData: boolean = true      // 是否还有更多数据
   @State private isLoadingPage: boolean = false   // 是否正在加载分页数据
@@ -10780,6 +10780,9 @@ export struct LocalMusic {
   @Consume CONTROL_PlayStatus: number ;
   @State PROGRESS_MAX_VALUE: number = 100;
   @State updateProgressTimer: number = 0;
+  private readonly progressUpdateIntervalMs: number = 120;
+  private readonly lyricUpdateMinDeltaMs: number = 24;
+  private lastLyricRenderPosition: number = Number.NaN;
   @State isHide: boolean = false; //是否因此播放器的按钮,默认是进入横屏模式隐藏,点击视频的时候显示
   @State name: string = '';
   // @State loop: boolean = false;
@@ -14708,6 +14711,21 @@ export struct LocalMusic {
     }
   }
 
+  private updateLyricPosition(lyricPosition: number): void {
+    if (!Number.isNaN(this.lastLyricRenderPosition) &&
+      Math.abs(lyricPosition - this.lastLyricRenderPosition) < this.lyricUpdateMinDeltaMs) {
+      return;
+    }
+    this.lastLyricRenderPosition = lyricPosition;
+    this.lyricController.updatePosition(lyricPosition);
+    if(this.showPipLyric){
+      this.lyricControllerXF.updatePosition(lyricPosition);
+    }
+    if(this.isShowSingleLineLyric){
+      this.lyricControllerSingle.updatePosition(lyricPosition);
+    }
+  }
+
   private setProgress() {
     if (this.castControllerWrapper && this.isCastPlaying) {
       return;
@@ -14736,18 +14754,13 @@ export struct LocalMusic {
     if (position > duration) {
       position = duration;
     }
+    const lyricPosition = position + this.timeOffset * 1000;
     this.isCurrentTime = true;
-    this.lyricController.updatePosition(position + this.timeOffset * 1000)
-    if(this.showPipLyric){
-      this.lyricControllerXF.updatePosition(position + this.timeOffset * 1000);
-    }
-    if(this.isShowSingleLineLyric){
-      this.lyricControllerSingle.updatePosition(position + this.timeOffset * 1000);
-    }
+    this.updateLyricPosition(lyricPosition);
 
     // 蓝牙歌词更新逻辑
     if (this.bluetoothLyricEnabled && this.isBluetoothConnected && this.lyricContent && this.currentSong) {
-      const currentLyricLine = LyricUtil.getCurrentLyricLine(this.lyricContent, position + this.timeOffset * 1000);
+      const currentLyricLine = LyricUtil.getCurrentLyricLine(this.lyricContent, lyricPosition);
       // 仅在歌词行变化时更新,避免频繁调用
       if (currentLyricLine !== this.lastBluetoothLyricLine) {
         this.lastBluetoothLyricLine = currentLyricLine;
@@ -14825,7 +14838,7 @@ export struct LocalMusic {
       if (!that.mDestroyPage) {
         that.setProgress();
       }
-    }, 300);
+    }, this.progressUpdateIntervalMs);
   }
 
   private stopProgressTask() {
@@ -14852,6 +14865,7 @@ export struct LocalMusic {
   private async play(url: string,startOffset?:number) {
     this.lyricController.setLyric(null)
     this.lyricControllerSingle.setLyric(null)
+    this.lastLyricRenderPosition = Number.NaN;
     this.isShowBgLiuGUANG = PreferencesUtil.getBooleanSync('isShowBgLiuGUANG', false)
     console.info('onecold this.url 1= '+url)
     if(!url.toLowerCase().startsWith('http')){
@@ -15198,7 +15212,7 @@ export struct LocalMusic {
           let timeoutPlay = 0
           if (StrUtil.isNotEmpty(this.videoUrl) &&
             (this.videoUrl.toLowerCase().endsWith('.rmvb') || this.videoUrl.toLowerCase().endsWith('.flac'))) {
-            timeoutPlay = 1000
+            timeoutPlay = 300
           }
 
           //跳过片头片尾关键代码,以下是跳过片头的关键代码
@@ -16179,13 +16193,8 @@ export struct LocalMusic {
     this.isCurrentTime = true;
     this.currentTime = this.stringForTime(position);
     this.isCurrentTime = false;
-    this.lyricController.updatePosition(position + this.timeOffset * 1000);
-    if(this.showPipLyric){
-      this.lyricControllerXF.updatePosition(position + this.timeOffset * 1000);
-    }
-    if(this.isShowSingleLineLyric){
-      this.lyricControllerSingle.updatePosition(position + this.timeOffset * 1000);
-    }
+    const lyricPosition = position + this.timeOffset * 1000;
+    this.updateLyricPosition(lyricPosition);
   }
   /**
    * Gesture method onActionUpdate.

+ 120 - 85
lib/src/main/ets/view/LyricView2.ets

@@ -1,11 +1,11 @@
-import { duration2text, printD, printW } from '../extensions/Extension';
+import { duration2text, printW } from '../extensions/Extension';
 import { LyricController } from '../LyricController';
 import { Lyric } from '../bean/Lyric';
 import { ListAdapter } from '../extensions/ListAdapter';
 import { LyricLine } from '../bean/LyricLine';
 import { LyricWord } from '../bean/LyricWord';
 import { transverter, TransverterType, TransverterLanguage } from "@nutpi/chinese_transverter"
-import { curves, LengthMetrics } from '@kit.ArkUI';
+import { LengthMetrics } from '@kit.ArkUI';
 
 /**
  * A component to display the lyric with scroll animation.
@@ -73,6 +73,11 @@ export struct LyricView2 {
     @State isHightLightCenter: boolean = true
     @State currentMediaPosition: number = 0
     @State transverterType: number = 0
+    private lastPositionUpdateTs: number = 0
+    private lastPositionForRender: number = -1
+    private readonly positionUpdateThrottleMs: number = 33
+    private readonly minPositionDeltaMs: number = 8
+    private readonly karaokeTransitionWidth: number = 0.12
 
     private onDataChangedListener = (lyric: Lyric | null) => {
         clearTimeout(this.loadTimeout)
@@ -88,6 +93,13 @@ export struct LyricView2 {
             this.currentMediaPosition = mediaPosition
             return
         }
+        const now = Date.now()
+        if (now - this.lastPositionUpdateTs < this.positionUpdateThrottleMs &&
+            Math.abs(mediaPosition - this.lastPositionForRender) < this.minPositionDeltaMs) {
+            return
+        }
+        this.lastPositionUpdateTs = now
+        this.lastPositionForRender = mediaPosition
         this.currentMediaPosition = mediaPosition
         this.onPositionChanged(mediaPosition)
     }
@@ -244,9 +256,6 @@ export struct LyricView2 {
                     this.seekIndex = center;
                     let targetPosition = this.listAdapter.getData(center).beginTime;
                     this.scrollDurationText = duration2text(targetPosition);
-                } else {
-                    // 处理 center 不在范围内的情况
-                    console.error(`Index out of range: ${center}`);
                 }
                 // this.seekIndex = center
                 // let targetPosition = this.listAdapter.getData(center).beginTime
@@ -286,7 +295,8 @@ export struct LyricView2 {
     NormalLyricLine(item: LyricLine, index: number) {
         Column(){
             Text(item.text)
-                .fontSize(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?this.textSize*this.controller.getHighlightScale():this.textSize)
+                .fontSize(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?
+                    this.textHighlightSize : this.textSize)
                 .textAlign(this.alignMode == 'center' ? TextAlign.Center : TextAlign.Start)
                 .fontColor(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? this.textHighlightColor : this.textColor)
                 .fontWeight(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
@@ -295,10 +305,6 @@ export struct LyricView2 {
                     (index >= this.currentIndex && index <= this.currentIndex + 1 ? Visibility.Visible : Visibility.None)
                     : Visibility.Visible)
                 .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?'95%': '80%')
-                .animation({
-                    duration: 150,
-                    curve: Curve.Linear
-                })
                 .blendMode(
                     index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendMode.DST_IN : undefined,
                     index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendApplyType.OFFSCREEN : undefined
@@ -308,7 +314,8 @@ export struct LyricView2 {
             if (item.translation)  {
 
             Text(item.translation)
-                .fontSize(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?this.textSize*this.controller.getHighlightScale():this.textSize)
+                .fontSize(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?
+                    this.textHighlightSize : this.textSize)
                 .fontColor(this.currentMediaPosition >= item.beginTime ?
                     index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? this.textHighlightColor : this.textColor : this.textColor)
                 .fontWeight(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
@@ -322,10 +329,6 @@ export struct LyricView2 {
                     index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendApplyType.OFFSCREEN : undefined
                 )
                 .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?'95%': '80%')
-                .animation({
-                    duration: 150,
-                    curve: Curve.Linear
-                })
 
             }
         }
@@ -346,6 +349,41 @@ export struct LyricView2 {
     /**
      * 计算卡拉OK渐变 - 同色系从浅到深的平滑过渡
      */
+    private clamp01(value: number): number {
+        return Math.max(0, Math.min(1, value))
+    }
+
+    private smoothStep01(value: number): number {
+        const x = this.clamp01(value)
+        return x * x * (3 - 2 * x)
+    }
+
+    private createKaraokeGradient(progress: number, transitionWidth: number = this.karaokeTransitionWidth): [ResourceColor, number][] {
+        const p = this.clamp01(progress)
+        if (p <= 0) {
+            return [[this.textColor, 0.0], [this.textColor, 1.0]]
+        }
+        if (p >= 1) {
+            return [[this.textHighlightColor, 0.0], [this.textHighlightColor, 1.0]]
+        }
+
+        const width = Math.max(0.02, Math.min(0.3, transitionWidth))
+        const half = width / 2
+        const transitionStart = this.clamp01(p - half)
+        const transitionEnd = this.clamp01(p + half)
+
+        return [[this.textHighlightColor, 0.0],
+            [this.textHighlightColor, transitionStart],
+            [this.textColor, transitionEnd],
+            [this.textColor, 1.0]]
+    }
+
+    private getAdaptiveWordTransitionWidth(wordDuration: number): number {
+        const safeDuration = Math.max(wordDuration, 80)
+        const shortWordBoost = this.clamp01((260 - safeDuration) / 260)
+        return Math.max(0.08, Math.min(0.24, this.karaokeTransitionWidth + shortWordBoost * 0.08))
+    }
+
     getLyricItemLinearGradient(item: LyricLine, index: number): [ResourceColor, number][] {
         // 只对当前播放行且包含逐字数据的行应用卡拉OK效果
         if (index !== this.currentIndex  || item.words.length === 0) {
@@ -366,7 +404,6 @@ export struct LyricView2 {
         //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]]
         }
 
@@ -374,12 +411,9 @@ export struct LyricView2 {
         let diff = this.currentMediaPosition - item.beginTime
         let value = diff / lyricDuration
 
-        value = Math.max(0, Math.min(1, value))
+        value = this.smoothStep01(value)
 
-        return [[this.textHighlightColor, 0.0],
-            [this.textHighlightColor, value],
-            [this.textColor, value],
-            [this.textColor, 1.0]]
+        return this.createKaraokeGradient(value, 0.1)
     }
 
     /**
@@ -397,47 +431,39 @@ export struct LyricView2 {
             return [[Color.White, 0.0], [Color.White, 1.0]]
         }
 
-        // 计算该字的播放进度
-        const wordEndTime = word.startTime + word.duration
-        const wordDuration = word.duration
+        const rawDuration = Math.max(word.duration, 0)
+        const effectiveDuration = rawDuration > 0 ? rawDuration : 180
+        const preRoll = Math.min(90, Math.max(20, effectiveDuration * 0.16))
+        const postRoll = Math.min(110, Math.max(26, effectiveDuration * 0.24))
+        const smoothStart = word.startTime - preRoll
+        const smoothEnd = word.startTime + effectiveDuration + postRoll
+        const width = this.getAdaptiveWordTransitionWidth(effectiveDuration)
 
-        // 异常情况处理
-        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]]
-            }
+        if (this.currentMediaPosition <= smoothStart) {
+            return this.createKaraokeGradient(0, width)
         }
-
-        // 计算当前在该字内的播放进度(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))
+        if (this.currentMediaPosition >= smoothEnd) {
+            return this.createKaraokeGradient(1, width)
         }
 
-        console.info('heanup', `getWordByWordLyricLyricItemLinearGradient - word=${word.word}, progress=${progress}, currentPos=${this.currentMediaPosition}, wordStart=${word.startTime}, wordEnd=${wordEndTime}`)
+        const linearProgress = this.clamp01((this.currentMediaPosition - smoothStart) / (smoothEnd - smoothStart))
+        const easedProgress = this.smoothStep01(linearProgress)
+        const blendedProgress = this.clamp01(easedProgress * 0.88 + linearProgress * 0.12)
+        return this.createKaraokeGradient(blendedProgress, width)
+    }
 
-        // 返回卡拉OK渐变效果
-        // 0.0 到 progress:高亮色(已播放部分)
-        // progress 到 1.0:普通色(未播放部分)
-        return [[this.textHighlightColor, 0.0],
-            [this.textHighlightColor, progress],
-            [this.textColor, progress],
-            [this.textColor, 1.0]]
+    private getWordPlaybackState(word: LyricWord): number {
+        const duration = Math.max(word.duration, 0)
+        const effectiveDuration = duration > 0 ? duration : 180
+        const endTime = word.startTime + effectiveDuration
+        const tailWindow = Math.min(90, Math.max(24, effectiveDuration * 0.2))
+        if (this.currentMediaPosition <= word.startTime) {
+            return 0
+        }
+        if (this.currentMediaPosition >= endTime + tailWindow) {
+            return 2
+        }
+        return 1
     }
 
     @Builder
@@ -445,26 +471,23 @@ export struct LyricView2 {
         Column() {
             Row({ space: 0 }) {
                 ForEach(item.words, (word: LyricWord, wordIndex: number) => {
-
-
                     Text(word.word)
-                        .fontSize(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?this.textSize*this.controller.getHighlightScale():this.textSize)
-                        .fontColor(this.currentMediaPosition >= word.startTime ?
-                            index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? this.textHighlightColor : this.textColor : this.textColor)
-                        .fontWeight(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText)? FontWeight.Bold : this.textWeight)
+                        .fontSize(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?
+                            this.textHighlightSize : this.textSize)
+                        .fontColor(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) &&
+                            this.getWordPlaybackState(word) == 2 ? this.textHighlightColor : this.textColor)
+                        .fontWeight(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?
+                            FontWeight.Bold : this.textWeight)
                         .margin(isEnglish(word.word) ?{ right:4 }:{})
                         .visibility(this.isSingleLine?
                             (index >= this.currentIndex && index <= this.currentIndex + 1 ? Visibility.Visible : Visibility.None)
                             : Visibility.Visible)
                         .padding(this.isSingleLine?0:{ top:5,bottom:5 })
-                        .shaderStyle(index == this.currentIndex ?{
+                        .shaderStyle(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) &&
+                            this.getWordPlaybackState(word) == 1 ?{
                             direction: GradientDirection.Right,
                             colors: this.getWordByWordLyricLyricItemLinearGradient(item, word, index)
                         }:undefined)
-                        .animation({
-                            duration: 150,
-                            curve: Curve.Linear
-                        })
                 })
             }
             .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
@@ -474,7 +497,8 @@ export struct LyricView2 {
             if (item.translation)  {
                 Row({ space: 0 }) {
                     Text(item.translation)
-                        .fontSize(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?this.textSize*this.controller.getHighlightScale():this.textSize)
+                        .fontSize(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?
+                            this.textHighlightSize : this.textSize)
                         .fontColor(this.currentMediaPosition >= item.beginTime ?
                             index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? this.textHighlightColor : this.textColor : this.textColor)
                         .fontWeight(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText)? FontWeight.Bold : this.textWeight)
@@ -483,10 +507,6 @@ export struct LyricView2 {
                         .visibility(this.isSingleLine?
                             (index >= this.currentIndex && index <= this.currentIndex + 1 ? Visibility.Visible : Visibility.None)
                             : Visibility.Visible)
-                        .animation({
-                            duration: 150,
-                            curve: Curve.Linear
-                        })
                 }
                 .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
                 .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?'95%': '85%')
@@ -638,7 +658,6 @@ export struct LyricView2 {
     }
 
     private getIndex(position: number): number {
-
         let size = this.listAdapter.totalCount()
         if (size === 0) return 0 // 空列表保护
 
@@ -647,25 +666,33 @@ export struct LyricView2 {
             return 0
         }
 
-        let first = this.listAdapter.getData(0).beginTime
+        const first = this.listAdapter.getData(0).beginTime
         if (position < first) {
             return 0
         }
-        let last = this.listAdapter.getData(size - 1).beginTime
-        if (position > last) {
-            return size - 1
+        const lastIndex = size - 1
+        const last = this.listAdapter.getData(lastIndex).beginTime
+        if (position >= last) {
+            return lastIndex
         }
-        for (let i = 0; i < size - 1; i++) {
-            let line = this.listAdapter.getData(i)
-            if (position >= line.beginTime && position < line.nextTime) {
-                return i
+
+        let left = 0
+        let right = lastIndex
+        while (left <= right) {
+            const mid = (left + right) >> 1
+            const beginTime = this.listAdapter.getData(mid).beginTime
+            if (beginTime <= position) {
+                left = mid + 1
+            } else {
+                right = mid - 1
             }
         }
-        return this.currentIndex
+
+        return Math.max(0, Math.min(lastIndex, right))
     }
 
     private animateToIndex(index: number) {
-        printD('animate to index= ' + index)
+        // printD('animate to index= ' + index)
         this.currentIndex = index
         if (this.isUserTouching) {
             return
@@ -695,6 +722,14 @@ export struct LyricView2 {
         if (this.currentLyric && this.currentLyric.isPlainText) {
             return
         }
+
+        if (this.currentIndex >= 0 && this.currentIndex < this.listAdapter.totalCount()) {
+            const currentLine = this.listAdapter.getData(this.currentIndex)
+            if (mediaPosition >= currentLine.beginTime && mediaPosition < currentLine.nextTime) {
+                return
+            }
+        }
+
         let index = this.getIndex(mediaPosition)
         if (index != this.currentIndex) {
             this.animateToIndex(index)
@@ -718,4 +753,4 @@ function  isEnglish(text: string, mode: string = 'strict', threshold: number = 0
 function checkByPercentage(text: string, threshold: number): boolean {
     const validChars = text.match(/[a-zA-Z\s.,!?'"-]/g)  || [];
     return (validChars.length  / text.length)  >= threshold;
-}
+}