HarmonyOS鸿蒙Next中RichEditor实现类似歌词滚动,当前歌词高亮的效果

HarmonyOS鸿蒙Next中RichEditor实现类似歌词滚动,当前歌词高亮的效果 我想用RichEditor实现歌词滚动播放的效果,里面有每一句歌词的开始时间和结束时间,然后根据计时器的走向,找到高亮的那句话进行高亮,持续的播放一点点滚动到底部。有什么实现建议?

3 回复

可以尝试这个demo

在定时器执行过程中进行计算当前所在行数

Bean.ets:

import measure from '@ohos.measure';
import { display } from '@kit.ArkUI';

export class Bean {
  static readonly textArray: Array<TextBean> = [];

  static init() {
    let widthComp = px2vp(display.getDefaultDisplaySync().width); // 组件的宽度,范例是全屏
    console.log('lcs --- widthComp', widthComp)
    for (let i = 0; i < 100; i++) {
      let text = `我是第 ${i} 句话,`;
      Bean.textArray.push({
        label: text,
        startTime: 1000 * i,
        endTime: 1000 * i + 1000,
        width: -1,
        line: -1 // 默认是 -1标识不折行,其他值标识折行在几行
      })
    }
    console.log('lcs ---', JSON.stringify(Bean.textArray));
  }
}

export interface TextBean {
  label: string;
  startTime: number;
  endTime: number;
  width: number;
  line?: number;
}

export function calcWidth(text: string): number {
  let textWidth: number = measure.measureText({
    textContent: text,
    fontSize: 16
  });
  return px2vp(textWidth);
}

export function find(arr: Array<TextBean>, target: number, left: number, right: number): number {
  if (left > right) {
    return -1;
  }
  let mid:number = Math.floor((left + right) / 2);
  if (target >= arr[mid].startTime && target < arr[mid].endTime) {
    return arr[mid].line ?? 0;
  } else if(arr[mid].startTime > target){
    return find(arr, target, left, mid - 1);
  }else{
    return find(arr, target, mid + 1, right);
  }
}

Index.ets:

import { Bean, calcWidth, find, TextBean } from './Bean';
import { Player } from './Player';
import { display, LengthMetrics } from '@kit.ArkUI';

@Entry
@Component
struct main {
  scroller: Scroller = new Scroller();
  duration: number = 99000;
  @State currentTime: number = 0;
  player?: Player
  textHeight: number = 16; // 文字高度
  columHeight: number = 0;
  lines: number = 0;
  widthComp: number = 0;
  // controller: TextController = new TextController();
  @Watch('lineChanged') @State centerLine: number = 0;
  controller: RichEditorController = new RichEditorController();
  private start: number = 0;
  private end: number = 5;
  @State currentTabIndex: number = 0

  aboutToAppear(): void {
    Bean.init(); // 初始化数据
    this.widthComp = px2vp(display.getDefaultDisplaySync().width);
  }

  onPageShow(): void {
    this.start = 0
    this.end = 0
    let w: number = calcWidth(Bean.textArray[0].label)
    let line = 1

    this.player = new Player((time: number) => {
      this.currentTime = time;
      if ((Bean.textArray[this.currentTabIndex]) &&
        this.currentTime >= Bean.textArray[this.currentTabIndex].startTime &&
        this.currentTime < Bean.textArray[this.currentTabIndex].endTime) {
        this.controller.updateSpanStyle({
          start: this.start,
          end: this.end,
          textStyle:
          {
            fontColor: Color.Black
          }
        })
        this.start = this.end
        this.end += Bean.textArray[this.currentTabIndex].label.length
        let width: number = 0
        this.controller.updateSpanStyle({
          start: this.start,
          end: this.end,
          textStyle:
          {
            fontColor: Color.Blue
          }
        })
        this.controller.getSpans({
          start: this.start,
          end: this.end
        }).forEach(item => {
          console.info("text span: " + (item as RichEditorTextSpanResult).value)
          width = calcWidth((item as RichEditorTextSpanResult).value)
        })
        this.currentTabIndex += 1
        w = w + width
        if (w > this.widthComp) {
          w = w - this.widthComp; // 多出的部分设为初始值
          line += 1;
        }
      }
      if (line >= Math.floor(this.lines / 2)) {
        // 大于一半就开始滚动
        this.centerLine = line;
      }
    })
    this.player.setTimer();
  }

  lineChanged() {
    this.scroller.scrollBy(0, (this.textHeight + 5)); // 滚动行高 + 间距lineSpacing
  }

  build() {
    Column() {
      Row() {
        Text('文字高亮实例')
          .fontColor(Color.White)
          .fontSize(16)
      }
      .height(44)
      .width('100%')
      .backgroundColor(Color.Green)
      .padding({
        left: 16
      })

      Scroll(this.scroller) {
        Stack() {
          Column() {
            RichEditor({ controller: this.controller })
              .onReady(() => {
                for (let i = 0; i <= Bean.textArray.length; i++) {
                  this.controller.addTextSpan(Bean.textArray[i]?.label || '', {
                    style: {
                      fontSize: 16
                    }
                  })
                }
              })
              .hitTestBehavior(HitTestMode.None)
              .onAreaChange(() => {
                let layoutManager = this.controller.getLayoutManager();
                let lineCount = "layoutManager LineCount: " + layoutManager.getLineCount()
                this.lines = layoutManager.getLineCount()
              })
              .padding(0)
              .width('100%')
          }
          .width('100%')
          .justifyContent(FlexAlign.Start)
        }
      }
      .scrollBar(BarState.Off)
      .height('50%')
      .backgroundColor(Color.White)
      .onAreaChange((oldValue: Area, newValue: Area) => {
        this.columHeight = Number(newValue.height);
        this.lines = this.columHeight / (this.textHeight + 5);
        console.log('lcs --- lines', this.lines);
      })

      Row() {
        Text('查看原文')
        Text('复制金句')
        Text('智能字幕')
      }
      .height(50)
      .backgroundColor(Color.Green)
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)

      Slider({
        direction: Axis.Horizontal,
        value: this.currentTime,
        min: 0,
        max: this.duration,
        style: SliderStyle.OutSet
      })
        .blockColor(Color.White)
        .trackColor(Color.Gray)
        .selectedColor(Color.White)
        .showTips(false)
        .trackThickness(2)
        .blockSize({
          width: 14,
          height: 14
        })
        .onChange((value: number, mode: SliderChangeMode) => {
          this.currentTime = value;
          if (mode == SliderChangeMode.Begin || mode === SliderChangeMode.Moving) {

          }
          if (mode == SliderChangeMode.End) {
            this.currentTime = value;
          }
        })
    }
  }
}

更多关于HarmonyOS鸿蒙Next中RichEditor实现类似歌词滚动,当前歌词高亮的效果的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS鸿蒙Next中,使用RichEditor实现歌词滚动和高亮效果可以这样做:

  1. 数据绑定:将歌词数据与RichEditor绑定,使用@State@Link装饰器管理当前播放行。

  2. 滚动控制:通过ScrollerController控制滚动位置,结合当前行索引计算偏移量,调用scrollTo方法自动滚动。

  3. 高亮样式:使用RichEditor的spanStyle对当前行文字设置不同颜色(foregroundColor)和字体大小(fontSize),非当前行使用默认样式。

  4. 定时刷新:利用定时器或播放进度回调更新currentLineIndex,触发UI刷新。

关键代码片段:

[@State](/user/State) currentLine: number = 0;
scrollerController.scrollTo({xOffset: 0, yOffset: lineHeight * currentLine})

在HarmonyOS Next中实现RichEditor的歌词滚动效果,可以通过以下方案实现:

  1. 数据结构设计:
  • 使用List<LyricLine>存储歌词数据,每个LyricLine包含:
    • 开始时间(startTime)
    • 结束时间(endTime)
    • 歌词文本(content)
    • 是否高亮标志(isHighlight)
  1. 核心实现步骤:
// 1. 初始化RichEditor
let richEditor = new RichEditor(context);
richEditor.setContent(lyricsToHtml(lyricLines));

// 2. 计时器逻辑
let timer = setInterval(() => {
  const currentTime = getCurrentPlayerTime();
  updateHighlight(currentTime);
}, 100);

// 3. 高亮更新函数
function updateHighlight(currentTime) {
  lyricLines.forEach(line => {
    line.isHighlight = (currentTime >= line.startTime && currentTime < line.endTime);
  });
  
  richEditor.setContent(lyricsToHtml(lyricLines));
  scrollToHighlightedLine();
}

// 4. 歌词转HTML
function lyricsToHtml(lines) {
  return lines.map(line => 
    `<p style="color:${line.isHighlight?'red':'black'}">${line.content}</p>`
  ).join('');
}
  1. 滚动控制:
  • 使用RichEditor的scrollTo方法定位到当前高亮行
  • 可以计算高亮行位置后调用:
richEditor.scrollTo({y: targetYPosition});
  1. 性能优化:
  • 避免频繁重绘,只在时间区间切换时更新UI
  • 使用requestAnimationFrame优化滚动动画
  • 对长歌词列表进行分页加载

注意:实际实现时需要根据HarmonyOS Next的API调整,特别是RichEditor的具体用法可能会有所不同。

回到顶部