HarmonyOS 动感歌词效果

711 阅读13分钟

AudioBox - HarmonyOS 动态歌词播放器

一款支持动态歌词显示的音乐播放器,实现了精美的歌词渲染和动画效果。

实现效果

动态歌词效果

动态歌词效果

功能特点

  • 🎵 音频播放控制
  • 📝 动态歌词显示
  • 🌈 平滑渐变动画
  • 📱 自适应布局
  • ⚡ 高性能渲染

项目结构

entry/src/main/ets/
├─components/          # UI组件
|  ├─LyricView         # 歌词显示组件
│  └─PlayerControls    # 播放控制组件
├─model/               # 数据模型
│  └─LyricAnimationState # 动画状态管理
├─pages/               # 页面
│  └─Index             # 主页面
├─services/            # 服务
│  └─AudioService      # 音频服务
└─utils/               # 工具类
   ├─AnimationUtils    # 动画工具
   ├─LrcUtils          # 歌词解析工具
   └─NetUtils          # 网络工具

实现原理

1. 整体架构

项目采用 MVC 架构模式:

Model: LyricAnimationState(动画状态管理)
View: LyricView(歌词渲染)
Controller: Index(业务逻辑)

工作流程:

graph LR
    A[音频播放] --> B[进度更新]
    B --> C[状态管理]
    C --> D[歌词渲染]
    D --> E[动画效果]

2. 核心功能实现

2.1 动态歌词渲染与渐变效果

动态歌词渲染是本项目的核心功能,需要实现文本渲染、渐变动画和布局计算。这里使用自定义渲染器 DrawModifier 来实现,它能够提供底层的绘制能力,让我们可以精确控制文本的渲染效果。

  1. 初始化和参数设置
// entry/src/main/ets/components/LyricView.ets
class LyricDrawModifier extends DrawModifier {
  private progress: number = 0
  private text: string = ''
  private fontSize: number = 24
  private maxWidth: number = 0

  constructor(progress: number, text: string, fontSize: number) {
    super()
    this.progress = progress || 0
    this.text = text || ''
    this.fontSize = fontSize || 24
  }
  // ...
}

在初始化阶段,我们需要设置三个关键参数:

  • progress:当前歌词的播放进度,用于控制渐变效果的位置
  • text:需要渲染的歌词文本内容
  • fontSize:文字大小,会影响整体布局和渲染效果

这些参数都提供了默认值,确保在参数缺失时也能正常工作。特别是 fontSize 的默认值 24,是经过多次测试后选定的,在大多数设备上都能获得较好的显示效果。

  1. 渲染准备和布局计算
// entry/src/main/ets/components/LyricView.ets
drawContent(context: DrawContext): void {
  try {
    // 初始化画布尺寸
    const width = vp2px(context.size.width)
    const height = vp2px(context.size.height)
    this.maxWidth = width * 0.9

    // 计算文本布局
    const fontHeight = vp2px(this.fontSize)
    const lineHeight = fontHeight * 1.2
    const totalTextHeight = lineHeight * lines.length
    const startY = (height - totalTextHeight) / 2 + fontHeight
    // ...
  } catch (error) {
    console.error('LyricDrawModifier error:', error)
  }
}

布局计算是整个渲染过程的基础,这里有几个关键点:

  • 使用 vp2px 进行单位转换,确保在不同设备上显示一致
  • 设置文本区域宽度为容器宽度的 90%,留出边距
  • 行高设置为字体高度的 1.2 倍,提供更好的可读性
  • 通过 totalTextHeight 计算整体高度,实现垂直居中
  1. 渐变效果创建
// entry/src/main/ets/components/LyricView.ets
private createGradientEffect(textX: number, lineWidth: number, progress: number): drawing.ShaderEffect {
  const startPt: common2D.Point = { x: textX, y: 0 }
  const endPt: common2D.Point = { x: textX + lineWidth, y: 0 }
  
  const colors = [
    0xFF1AFF36, // 活跃颜色
    0xFF1AFF36,
    0xFF666666, // 普通颜色
    0xFF666666
  ]
  const positions = [0, progress, progress, 1]
  
  return drawing.ShaderEffect.createLinearGradient(
    startPt,
    endPt,
    colors,
    drawing.TileMode.CLAMP,
    positions
  )
}

渐变效果是实现歌词高亮的核心,这里的实现考虑了以下几点:

  • 使用线性渐变创建平滑的过渡效果
  • 通过 progress 参数控制渐变位置,实现动态高亮
  • 颜色选择考虑了对比度,确保文本清晰可读
  • 使用 TileMode.CLAMP 处理超出范围的渐变
  1. 文本绘制
// entry/src/main/ets/components/LyricView.ets
private drawText(context: DrawContext, line: string, x: number, y: number, font: drawing.Font) {
  const points: common2D.Point[] = []
  let currentX = x

  // 计算每个字符的位置
  for (let i = 0; i < line.length; i++) {
    const char = line[i]
    const charWidth = font.measureText(char, drawing.TextEncoding.TEXT_ENCODING_UTF8)
    points.push({ x: currentX, y: y })
    currentX += charWidth
  }

  // 创建并绘制文本
  const textBlob = drawing.TextBlob.makeFromPosText(line, line.length, points, font)
  context.canvas.drawTextBlob(textBlob, 0, 0)
}

文本绘制采用了逐字符定位的方式,这样做有几个优势:

  • 能够精确控制每个字符的位置,实现完美的对齐效果
  • 支持中英文混排,每个字符都能获得正确的宽度
  • 使用 TextBlob 进行批量渲染,提高性能
  • 通过 measureText 确保不同字符的间距准确
2.2 动画状态管理

动画状态管理是实现流畅动画效果的关键。我们需要管理当前播放进度、字体大小变化和透明度过渡等状态。通过响应式编程和状态监听,确保UI能够实时反映播放状态的变化。

  1. 状态定义与初始化
// entry/src/main/ets/model/LyricAnimationState.ets
@ObservedV2
export class LyricAnimationState {
  @Trace currentIndex: number = 0
  @Trace scrollOffset: number = 0
  @Trace fontSizes: number[] = []
  @Trace opacities: number[] = []
  
  private readonly normalFontSize: number = 16
  private readonly activeFontSize: number = 20
  private readonly lineHeight: number = 40
  private readonly visibleLines: number = 5
}

这里定义了动画所需的核心状态:

  • currentIndex:当前播放的歌词索引
  • scrollOffset:滚动偏移量
  • fontSizes:每行歌词的字体大小
  • opacities:每行歌词的透明度
  • 使用 @ObservedV2@Trace 实现响应式更新
  1. 字体大小管理
// entry/src/main/ets/model/LyricAnimationState.ets
private updateFontSizes(length: number) {
  this.fontSizes = new Array(length).fill(0)
    .map((_item: number, i) => {
      const distance = Math.abs(i - this.currentIndex)
      if (distance === 0) return this.activeFontSize
      if (distance === 1) return this.normalFontSize - 1
      return this.normalFontSize - 2
    })
}

字体大小的变化遵循以下规则:

  • 当前行使用较大字号 (activeFontSize)
  • 相邻行略小于普通字号
  • 其他行使用最小字号
  • 通过距离计算实现平滑的大小过渡
  1. 透明度控制
// entry/src/main/ets/model/LyricAnimationState.ets
private updateOpacities(length: number) {
  this.opacities = new Array(length).fill(0).map((_item: number, i) => {
    const distance = Math.abs(i - this.currentIndex)
    if (distance === 0) return 1
    if (distance === 1) return 0.6
    if (distance === 2) return 0.4
    return 0.2
  })
}

透明度设计考虑了以下因素:

  • 当前行保持完全不透明
  • 相邻行有较高透明度
  • 远离当前行的歌词逐渐淡出
  • 通过多级透明度创造层次感
  1. 状态更新协调
// entry/src/main/ets/model/LyricAnimationState.ets
updateState(index: number, lyrics: ILrcModel[]) {
  if (index === this.currentIndex && this.fontSizes.length > 0) {
    return
  }
  
  this.currentIndex = index
  this.updateFontSizes(lyrics.length)
  this.updateOpacities(lyrics.length)
  this.calculateScrollOffset(index, lyrics.length)
}

状态更新的设计原则:

  • 避免不必要的更新,提高性能
  • 确保各个动画效果的同步性
  • 统一管理所有状态的更新
  • 在状态变化时自动触发UI更新
2.3 智能文本换行处理

长歌词文本的自动换行是提升用户体验的关键。我们需要实现一个智能的文本分行算法,通过精确计算每个字符的渲染宽度来决定换行位置。这个算法需要同时支持中英文混排的场景。

  1. 文本宽度计算
// entry/src/main/ets/components/LyricView.ets
private getCharWidth(char: string, font: drawing.Font): number {
  return font.measureText(
    char,
    drawing.TextEncoding.TEXT_ENCODING_UTF8
  )
}

字符宽度计算的关键点:

  • 使用 UTF-8 编码确保中文字符正确处理
  • 通过 measureText 获取精确的渲染宽度
  • 支持中英文混排场景
  1. 换行位置判断
// entry/src/main/ets/components/LyricView.ets
private calculateLines(text: string, fontSize: number, maxWidth: number): number {
  try {
    const font = new drawing.Font()
    font.setSize(vp2px(fontSize))
    let currentWidth = 0
    let lineCount = 1

    for (let i = 0; i < text.length; i++) {
      const charWidth = this.getCharWidth(text[i], font)
      if (currentWidth + charWidth > vp2px(maxWidth * 0.9)) {
        lineCount++
        currentWidth = charWidth
      } else {
        currentWidth += charWidth
      }
    }

    return lineCount
  } catch (error) {
    console.error('Calculate lines error:', error)
    return 1
  }
}

换行算法的设计考虑:

  • 预留 10% 的边距空间
  • 在字符边界处进行换行
  • 实时累加字符宽度
  • 异常处理确保稳定性
  1. 文本分行处理
// entry/src/main/ets/components/LyricView.ets
private splitTextIntoLines(font: drawing.Font, maxWidth: number): string[] {
  const lines: string[] = []
  let currentLine = ''
  let currentWidth = 0

  for (let i = 0; i < this.text.length; i++) {
    const char = this.text[i]
    const charWidth = this.getCharWidth(char, font)

    if (currentWidth + charWidth > maxWidth && currentLine) {
      lines.push(currentLine)
      currentLine = char
      currentWidth = charWidth
    } else {
      currentLine += char
      currentWidth += charWidth
    }
  }

  if (currentLine) {
    lines.push(currentLine)
  }

  return lines
}

分行处理的实现要点:

  • 动态收集每行文本
  • 精确控制行宽
  • 处理最后一行
  • 保持单词完整性
  1. 布局适配
// entry/src/main/ets/components/LyricView.ets
@Builder
private LyricItem(lyric: ILrcModel, index: number) {
  Column() {
    Text(lyric.text)
      .fontSize(this.animationState.fontSizes[index] ?? 24)
      .width('90%')
      .height(this.lineHeight * this.calculateLines(
        lyric.text,
        this.animationState.fontSizes[index] ?? 24,
        320
      ))
  }
}

布局适配的关键点:

  • 根据行数动态计算高度
  • 保持一致的行间距
  • 考虑字体大小变化
  • 确保文本居中对齐
2.4 滚动动画控制

歌词滚动是动态歌词显示的重要组成部分。我们需要精确计算滚动位置,并通过动画使滚动过程平滑自然。这里使用 Scroller 组件配合 AnimationUtils 来实现流畅的滚动效果。

  1. 滚动容器设置
// entry/src/main/ets/components/LyricView.ets
build() {
  Scroll(this.scroller) {
    Column({ space: 8 }) {
      // 顶部空白,用于第一行歌词居中
      Column()
        .height('50%')
        .width('100%')

      ForEach(this.lyrics, (lyric: ILrcModel, index: number) => {
        this.LyricItem(lyric, index)
      }, (item: ILrcModel) => JSON.stringify(item))

      // 底部空白,用于最后一行歌词居中
      Column()
        .height('50%')
        .width('100%')
    }
  }
  .scrollable(ScrollDirection.Vertical)
  .scrollBar(BarState.Off)
  .edgeEffect(EdgeEffect.None)
}

滚动容器的设计考虑:

  • 添加顶部和底部空白区域实现居中效果
  • 禁用滚动条提升视觉体验
  • 关闭边缘效果避免视觉干扰
  • 使用 space 控制歌词间距
  1. 滚动位置计算
// entry/src/main/ets/components/LyricView.ets
@Monitor('animationState.currentIndex')
updateScroll() {
  let targetOffset = 0
  for (let i = 0; i < this.animationState.currentIndex; i++) {
    const lineCount = this.calculateLines(
      this.lyrics[i].text,
      this.animationState.fontSizes[i] ?? 24,
      320
    )
    targetOffset += this.lineHeight * lineCount
  }
  targetOffset += this.animationState.currentIndex * 4
  // ...
}

位置计算的关键点:

  • 考虑每行实际占用的高度
  • 累加行间距
  • 处理多行歌词的情况
  • 实时响应索引变化
  1. 动画配置
// entry/src/main/ets/utils/AnimationUtils.ets
export class AnimationUtils {
  private static readonly DEFAULT_DURATION: number = 300

  static createScrollAnimation(): AnimationOption {
    return {
      duration: this.DEFAULT_DURATION,
      curve: Curve.EaseInOut,
      delay: 0,
      iterations: 1,
      playMode: PlayMode.Normal
    }
  }
}

动画参数的设计原则:

  • 使用缓动曲线实现平滑过渡
  • 设置合适的动画时长
  • 避免动画延迟
  • 确保动画只执行一次
  1. 滚动执行
// entry/src/main/ets/components/LyricView.ets
private executeScroll(targetOffset: number) {
  this.scroller.scrollTo({
    xOffset: 0,
    yOffset: targetOffset,
    animation: AnimationUtils.createScrollAnimation()
  })
}

滚动执行的注意事项:

  • 使用动画过渡避免生硬跳转
  • 保持水平位置不变
  • 确保滚动位置准确
  • 处理边界情况
2.5 播放进度控制

播放进度控制是实现歌词同步显示的基础。我们需要监听音频播放进度,计算当前应该显示的歌词,并控制渐变动画的进度。这个功能涉及音频播放器状态管理和UI更新的协调。

  1. 进度条组件
// entry/src/main/ets/components/PlayerControls.ets
build() {
  Column() {
    Row() {
      Text(this.formatTime(this.currentTime))
        .fontSize(12)
        .fontColor('#999999')

      Slider({
        value: this.currentTime,
        min: 0,
        max: this.duration,
        step: 1,
        style: SliderStyle.OutSet
      })
        .layoutWeight(1)
        .margin({ left: 8, right: 8 })
        .selectedColor('#FF6B00')
        .onChange((value: number) => {
          if (this.onSeek) {
            this.onSeek(value)
          }
        })

      Text(this.formatTime(this.duration))
        .fontSize(12)
        .fontColor('#999999')
    }
  }
}

进度条设计要点:

  • 显示当前时间和总时长
  • 支持拖动调整进度
  • 实时响应进度变化
  • 保持界面美观
  1. 播放控制按钮
// entry/src/main/ets/components/PlayerControls.ets
@Builder
private PlayButton() {
  Button({ type: ButtonType.Circle, stateEffect: true }) {
    SymbolGlyph(this.isPlaying ? 
      $r('sys.symbol.pause_fill') : 
      $r('sys.symbol.play_fill')
    )
      .fontSize(24)
      .fontColor(['#FFFFFF'])
  }
  .width(48)
  .height(48)
  .backgroundColor('#FF6B00')
  .onClick(() => {
    if (this.onPlayPause) {
      this.onPlayPause()
    }
  })
}

按钮实现考虑:

  • 使用系统图标确保一致性
  • 添加点击反馈效果
  • 优化按钮大小和间距
  • 使用醒目的主题色
  1. 进度同步
// entry/src/main/ets/pages/Index.ets
private updateLyricIndex() {
  for (let i = 0; i < this.lyrics.length; i++) {
    if (i === this.lyrics.length - 1 ||
      (this.currentTime >= this.lyrics[i].time &&
        this.currentTime < this.lyrics[i].nextTime!)) {
      this.lyricState.updateState(i, this.lyrics)
      break
    }
  }
}

同步机制的关键点:

  • 实时监听播放进度
  • 精确定位当前歌词
  • 处理边界情况
  • 触发状态更新
  1. 播放状态管理
// entry/src/main/ets/pages/Index.ets
private async handlePlayPause() {
  if (!this.avPlayer) {
    return
  }

  try {
    if (this.isPlaying) {
      await this.avPlayer.pause()
    } else {
      await this.avPlayer.play()
    }
    this.isPlaying = !this.isPlaying
  } catch (error) {
    console.error('Failed to play/pause:', error)
  }
}

状态管理的实现要点:

  • 异步处理播放控制
  • 错误处理和恢复
  • 状态同步更新
  • 防止状态不一致

3. 性能优化

在实现动态歌词显示功能时,我们采取了多项性能优化措施,以确保应用运行流畅,动画效果平滑。

3.1 渲染优化
  1. 避免不必要的重绘
// entry/src/main/ets/components/LyricView.ets
@Builder
LyricItem(lyric: Lyric, index: number) {
  if (!this.shouldRenderLyric(index)) {
    return // 超出可视区域的歌词不渲染
  }
  // ... 渲染歌词内容
}

private shouldRenderLyric(index: number): boolean {
  const visibleRange = 5 // 可视区域上下各保留5行
  return Math.abs(index - this.currentIndex) <= visibleRange
}

优化要点:

  • 仅渲染可视区域的歌词
  • 合理设置缓冲区大小
  • 减少节点数量
  1. 文本测量缓存
// entry/src/main/ets/components/LyricDrawModifier.ets
private measureText(text: string): TextMetrics {
  const key = `${text}_${this.fontSize}`
  if (this.textMetricsCache.has(key)) {
    return this.textMetricsCache.get(key)!
  }
  
  const metrics = this.ctx.measureText(text)
  this.textMetricsCache.set(key, metrics)
  return metrics
}

缓存策略:

  • 缓存常用文本尺寸
  • 键值包含字体大小
  • 及时清理过期缓存
3.2 动画性能
  1. 使用硬件加速
// entry/src/main/ets/components/LyricView.ets
Column() {
  // ... 其他内容
}
.clip(true) // 启用硬件加速
.markAnchor({ x: 0, y: 0 })
.transition({ type: TransitionType.All, opacity: 0.3 })

加速效果:

  • 开启GPU渲染
  • 减少主线程负担
  • 提升动画流畅度
  1. 动画帧优化
// entry/src/main/ets/model/LyricAnimationState.ets
private updateAnimation() {
  const now = Date.now()
  if (now - this.lastUpdateTime < 16) { // 限制60fps
    return
  }
  
  // ... 更新动画状态
  this.lastUpdateTime = now
}

帧率控制:

  • 合理控制刷新率
  • 避免过度绘制
  • 平衡性能和效果
3.3 内存管理
  1. 资源释放
// entry/src/main/ets/pages/Index.ets
aboutToDisappear() {
  if (this.avPlayer) {
    this.avPlayer.release()
    this.avPlayer = undefined
  }
  
  // 清理其他资源
  this.clearCaches()
}

内存优化:

  • 及时释放播放器
  • 清理缓存数据
  • 避免内存泄漏
  1. 状态重置
// entry/src/main/ets/model/LyricAnimationState.ets
public reset() {
  this.fontSizes = []
  this.opacities = []
  this.scrollOffset = 0
  this.textMetricsCache.clear()
}

状态管理:

  • 重置动画状态
  • 清空缓存数据
  • 释放临时资源

4. 最佳实践

在开发过程中,我们总结了一些最佳实践,可以帮助其他开发者更好地实现类似功能。

4.1 错误处理
  1. 优雅降级
// entry/src/main/ets/components/LyricDrawModifier.ets
private drawContent(ctx: CanvasRenderingContext2D) {
  try {
    // ... 绘制逻辑
  } catch (error) {
    console.error('Failed to draw lyrics:', error)
    this.fallbackToSimpleText(ctx) // 降级到简单文本渲染
  }
}

private fallbackToSimpleText(ctx: CanvasRenderingContext2D) {
  ctx.fillStyle = '#000000'
  ctx.textAlign = 'center'
  ctx.fillText(this.text, this.width / 2, this.height / 2)
}

处理策略:

  • 捕获异常情况
  • 提供备选方案
  • 保持基本功能
  1. 参数验证
// entry/src/main/ets/model/LyricAnimationState.ets
public updateState(index: number, lyrics: Lyric[]) {
  if (index < 0 || !lyrics?.length) {
    console.warn('Invalid parameters:', { index, lyricsLength: lyrics?.length })
    return
  }
  
  // ... 更新状态
}

验证要点:

  • 检查必要参数
  • 边界值处理
  • 日志记录
4.2 代码组织
  1. 模块化设计
// entry/src/main/ets/model/types.ets
export interface Lyric {
  text: string
  time: number
  nextTime?: number
}

// entry/src/main/ets/utils/timeFormat.ets
export function formatTime(seconds: number): string {
  const minutes = Math.floor(seconds / 60)
  const remainingSeconds = Math.floor(seconds % 60)
  return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
}

组织原则:

  • 功能模块分离
  • 接口清晰定义
  • 工具函数复用
  1. 状态管理
// entry/src/main/ets/model/PlayerState.ets
@Observed
export class PlayerState {
  private _isPlaying: boolean = false
  private _currentTime: number = 0
  
  @Watch('_isPlaying')
  onPlayingChange() {
    // ... 处理播放状态变化
  }
  
  public togglePlay() {
    this._isPlaying = !this._isPlaying
  }
}

状态设计:

  • 集中状态管理
  • 响应式更新
  • 单向数据流
4.3 用户体验
  1. 加载状态
// entry/src/main/ets/pages/Index.ets
build() {
  Column() {
    if (this.isLoading) {
      LoadingProgress()
        .width(32)
        .height(32)
    } else if (this.error) {
      Text(this.error)
        .fontSize(14)
        .fontColor('#FF0000')
    } else {
      // ... 正常内容
    }
  }
}

体验优化:

  • 显示加载状态
  • 错误信息提示
  • 平滑过渡效果
  1. 交互反馈
// entry/src/main/ets/components/PlayerControls.ets
Button() {
  // ... 按钮内容
}
.stateEffect(true)
.onClick(() => {
  vibrate({ type: VibrationType.Light })
  this.handleClick()
})

反馈方式:

  • 视觉反馈
  • 触感反馈
  • 即时响应

5. 常见问题

在开发过程中,我们遇到了一些常见问题,这里总结了相应的解决方案。

5.1 渲染相关
  1. 字体大小不一致

问题:在不同设备上,歌词渲染的字体大小可能出现不一致的情况。

解决方案:

// entry/src/main/ets/components/LyricDrawModifier.ets
private initContext() {
  const pixelRatio = getDevicePixelRatio()
  this.fontSize = Math.round(this.baseFontSize * pixelRatio)
  this.ctx.font = `${this.fontSize}px sans-serif`
}

private getDevicePixelRatio(): number {
  return window.devicePixelRatio || 1
}

关键点:

  • 考虑设备像素比
  • 字体大小取整
  • 统一字体设置
  1. 渐变效果异常

问题:在某些情况下,渐变效果可能出现断层或不平滑的情况。

解决方案:

// entry/src/main/ets/components/LyricDrawModifier.ets
private createGradient(progress: number) {
  const gradient = this.ctx.createLinearGradient(
    0, 0, 0, this.height
  )
  
  // 添加过渡缓冲区
  const buffer = 0.1
  gradient.addColorStop(0, this.inactiveColor)
  gradient.addColorStop(Math.max(0, progress - buffer), this.inactiveColor)
  gradient.addColorStop(progress, this.activeColor)
  gradient.addColorStop(Math.min(1, progress + buffer), this.inactiveColor)
  gradient.addColorStop(1, this.inactiveColor)
  
  return gradient
}

优化要点:

  • 添加过渡缓冲
  • 处理边界情况
  • 确保颜色连续
5.2 性能相关
  1. 滚动卡顿

问题:歌词滚动时可能出现卡顿现象,影响用户体验。

解决方案:

// entry/src/main/ets/components/LyricView.ets
@State scrollConfig: ScrollConfig = {
  scroller: new Scroller(),
  scrollDuration: 300,
  curve: Curve.EaseInOut
}

private updateScroll() {
  const offset = this.calculateOffset()
  animateTo({
    duration: this.scrollConfig.scrollDuration,
    curve: this.scrollConfig.curve,
    onFinish: () => {
      this.isScrolling = false
    }
  }, () => {
    this.scrollOffset = offset
  })
}

优化方向:

  • 使用动画曲线
  • 控制滚动时长
  • 避免频繁更新
  1. 内存占用过高

问题:长时间使用后,应用的内存占用可能持续增长。

解决方案:

// entry/src/main/ets/pages/Index.ets
private setupMemoryMonitor() {
  setInterval(() => {
    if (this.textMetricsCache.size > 100) {
      this.textMetricsCache.clear()
    }
    
    // 清理其他缓存
    this.cleanupResources()
  }, 60000) // 每分钟检查一次
}

private cleanupResources() {
  // 清理不再需要的资源
  this.unusedImages = []
  this.audioBuffers = []
  
  // 触发GC
  if (globalThis.gc) {
    globalThis.gc()
  }
}

处理策略:

  • 定期清理缓存
  • 释放未使用资源
  • 控制缓存大小
5.3 兼容性相关
  1. 设备适配

问题:在不同尺寸的设备上,布局可能出现异常。

解决方案:

// entry/src/main/ets/components/LyricView.ets
build() {
  Column() {
    // ... 其他内容
  }
  .width('100%')
  .height(this.getAdaptiveHeight())
  .padding(this.getDevicePadding())
}

private getAdaptiveHeight(): string {
  // 根据设备类型返回合适的高度
  return this.isTablet ? '60%' : '50%'
}

private getDevicePadding(): Padding {
  // 根据设备返回合适的内边距
  return this.isTablet ? 24 : 16
}

适配要点:

  • 使用相对单位
  • 动态计算尺寸
  • 设备类型判断
  1. 系统版本兼容

问题:在不同系统版本上,某些API可能不可用。

解决方案:

// entry/src/main/ets/utils/compatibility.ets
export function checkApiSupport(api: string): boolean {
  if (typeof globalThis[api] === 'undefined') {
    console.warn(`API ${api} is not supported`)
    return false
  }
  return true
}

// 使用示例
if (checkApiSupport('vibrate')) {
  vibrate({ type: VibrationType.Light })
} else {
  // 使用替代方案
}

处理方式:

  • 特性检测
  • 降级处理
  • 错误提示

总结

本项目实现了一个基于 HarmonyOS 的动态歌词显示组件,主要包含以下特点:

核心功能

  • 自定义渲染器实现平滑的歌词渐变效果
  • 智能文本换行支持中英文混排
  • 精确的播放进度同步和动画控制
  • 流畅的滚动动画和状态管理

技术要点

  • 使用 DrawModifier 实现高性能文本渲染
  • 采用响应式编程管理动画状态
  • 实现智能的文本分行算法
  • 优化内存占用和渲染性能

开发心得

  • 合理的架构设计是项目成功的基础
  • 性能优化需要贯穿整个开发过程
  • 良好的错误处理确保应用稳定性

希望这个项目能为其他开发者提供参考,帮助大家更好地开发 HarmonyOS 应用。