|
|
@@ -5,7 +5,7 @@ import { ListAdapter } from '../extensions/ListAdapter';
|
|
|
import { LyricLine } from '../bean/LyricLine';
|
|
|
import { LyricWord } from '../bean/LyricWord';
|
|
|
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.
|
|
|
@@ -190,25 +190,33 @@ export struct LyricView2 {
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
.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 })
|
|
|
- .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(() => {
|
|
|
if (this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0) {
|
|
|
if (this.seekIndex != -1 && this.seekIndex != this.currentIndex) {
|
|
|
@@ -223,13 +231,22 @@ export struct LyricView2 {
|
|
|
}
|
|
|
.width('100%')
|
|
|
.height('100%')
|
|
|
+ .layoutWeight(1)
|
|
|
+ .chainAnimation(true)
|
|
|
.scrollBar(BarState.Off)
|
|
|
.fadingEdge(true,{fadingEdgeLength:LengthMetrics.percent(20)})
|
|
|
+ .edgeEffect(EdgeEffect.Spring)
|
|
|
+ .contentEndOffset(this.h / 3)
|
|
|
+ .contentStartOffset(this.isUserTouching?this.h / 3:0)
|
|
|
.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)
|
|
|
.onScrollIndex((_, __, center) => {
|
|
|
const now = Date.now()
|
|
|
@@ -283,7 +300,14 @@ export struct LyricView2 {
|
|
|
})
|
|
|
.fontColor(index == this.currentIndex ? this.textHighlightColor : this.textColor)
|
|
|
.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?
|
|
|
(index == this.currentIndex ?Visibility.Visible:Visibility.None)
|
|
|
:Visibility.Visible)
|
|
|
@@ -303,7 +327,118 @@ export struct LyricView2 {
|
|
|
.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
|
|
|
@@ -323,6 +458,10 @@ export struct LyricView2 {
|
|
|
.visibility(this.isSingleLine?
|
|
|
(index == this.currentIndex ?Visibility.Visible:Visibility.None)
|
|
|
:Visibility.Visible)
|
|
|
+ .shaderStyle(index == this.currentIndex ?{
|
|
|
+ direction: GradientDirection.Right,
|
|
|
+ colors: this.getWordByWordLyricLyricItemLinearGradient(item, word, index)
|
|
|
+ }:undefined)
|
|
|
.animation({
|
|
|
// 动画播放速度
|
|
|
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){
|
|
|
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
|
|
|
SeekLine() {
|
|
|
Row() {
|