arkts鸿蒙音乐滚动歌词实例

1 篇文章 0 订阅
1 篇文章 0 订阅

实现方法

1、把歌词解析并转成数组并渲染成多个text

2、监听音频播放事件,跳转到相对应的歌词

(1)定义歌词解析类Lyric.ts

const timeExp:RegExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]/g

enum STATUS {
  STATE_PAUSE = 0,
  STATE_PLAYING = 1
}

interface TagRegInterface {
  title?:string,
  artist?:string,
  album?:string,
  offset?:string,
  by?:string,
}

export interface LineInterface {
  time?:number,
  txt:string,
  lineNum?:number
}

const tagRegMap:TagRegInterface = {
  title: 'ti',
  artist: 'ar',
  album: 'al',
  offset: 'offset',
  by: 'by'
}

export default class Lyric {
  private lrc:string = '';
  private tags:TagRegInterface = {};
  public lines:Array<LineInterface> = []
  private state:STATUS = STATUS.STATE_PAUSE;
  private curLine:number = 0;
  private curNum:number = 0;
  private startStamp:number = 0;
  private timer:number = 0;
  private pauseStamp:number;
  private handler:(data:LineInterface) => void

  constructor(lrc:string, handlder:(data:LineInterface) => void) {
    this.lrc = lrc;
    this.handler = handlder;
    this._init();
  }

  _init() {
    this._initTag()

    this._initLines()
  }

  _initTag() {
    for (let tag in tagRegMap) {
      const matches = this.lrc.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i'))
      this.tags[tag] = matches && matches[1] || ''
    }
  }

  _initLines() {
    const lines:Array<string> = this.lrc.split('\n')
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]
      let result:Array<string>|null = timeExp.exec(line)
      if (result) {
        const txt = line.replace(timeExp, '').trim()
        if (txt) {
          this.lines.push({
            time: parseInt(result[1]) * 60 * 1000 + parseInt(result[2]) * 1000 + (parseInt(result[3]) || 0) * 10,
            txt
          })
        }
      }
    }

    this.lines.sort((a, b) => {
      return a.time - b.time
    })
  }

  _findCurNum(time) {
    for (let i = 0; i < this.lines.length; i++) {
      if (time <= this.lines[i].time) {
        return i
      }
    }
    return this.lines.length - 1
  }

  _callHandler(i) {
    if (i < 0) {
      return
    }
    this.handler({
      txt: this.lines[i].txt,
      lineNum: i
    })
  }

  _playRest() {
    let line = this.lines[this.curNum]
    let delay = line.time - (+new Date() - this.startStamp)

    this.timer = setTimeout(() => {
      this._callHandler(this.curNum++)
      if (this.curNum < this.lines.length && this.state === STATUS.STATE_PLAYING) {
        this._playRest()
      }
    }, delay)
  }

  play(startTime:number = 0, skipLast:boolean = false) {
    if (!this.lines.length) {
      return
    }
    this.state = STATUS.STATE_PLAYING

    this.curNum = this._findCurNum(startTime)
    this.startStamp = +new Date() - startTime

    if (!skipLast) {
      this._callHandler(this.curNum - 1)
    }

    if (this.curNum < this.lines.length) {
      clearTimeout(this.timer)
      this._playRest()
    }
  }

  togglePlay() {
    const now:number = +new Date()
    if (this.state === STATUS.STATE_PLAYING) {
      this.stop()
      this.pauseStamp = now
    } else {
      this.state = STATUS.STATE_PLAYING
      this.play((this.pauseStamp || now) - (this.startStamp || now), true)
      this.pauseStamp = 0
    }
  }

  stop() {
    this.state =STATUS.STATE_PAUSE
    clearTimeout(this.timer)
  }

  seek(offset) {
    this.play(offset)
  }
}

2、引入歌词解析类并把歌词解析成数据渲染在页面上,监听音频播放,设置歌词跳转到到对应的时间轴,触发歌词回调函数,得到当前歌词的下标,List组件跳转到对应的下标,当前的歌词显示白色

import * as colors from '../../theme/color';
import * as size from '../../theme/size';
import router from '@ohos.router';
import { MusicInterface } from '../interface/Index';
import display from '@ohos.display';
import {HOST,MUSIC_STORAGE} from '../../config/constant';
import Lyric,{LineInterface} from '../../utils/Lyric';
import media from '@ohos.multimedia.media';
import { formatSecond } from '../../utils/common';

@Entry
@Component
struct MusicPlayerPage {
  @State lyric:Lyric = null;
  @State musicItem:MusicInterface = null;
  @State currentLineNum:number = 0;
  @State angle:number = 0;
  @State duration:number = 0;
  @State currentTime:number = 0;
  @State progress:number = 0;
  private scroller: Scroller = new Scroller()

  @StorageLink(MUSIC_STORAGE) avPlayer:media.AVPlayer | null  = null;

  private circleSize:number = px2vp(display.getDefaultDisplaySync().width * 0.8)

  async aboutToAppear() {
    const params = router.getParams(); // 获取传递过来的参数对象
    this.musicItem = params['musicItem'] as MusicInterface; // 获取info属性的值

    if(this.avPlayer == null){
      this.avPlayer = await media.createAVPlayer();
      AppStorage.SetOrCreate<media.AVPlayer>(MUSIC_STORAGE, this.avPlayer);
      this.setAVPlayerCallback()
      this.avPlayer.url = HOST + this.musicItem.localPlayUrl;
    }

    if (!this.musicItem.lyrics) return;// 如果有歌词

    this.lyric = new Lyric(this.musicItem.lyrics, ({ lineNum = 0 }) => {
      // 滚动到相对应的歌词
      this.scroller.scrollToIndex(lineNum);
      // 当前播放的歌词下标
      this.currentLineNum = lineNum;
    })
  }

  // 注册avplayer回调函数
  setAVPlayerCallback() {
    // seek操作结果回调函数
    this.avPlayer.on('seekDone', (seekDoneTime) => {
      console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
    })
    // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
    this.avPlayer.on('error', (err) => {
      this.avPlayer.reset(); // 调用reset重置资源,触发idle状态
    })
    // 状态机变化回调函数
    this.avPlayer.on('stateChange', async (state:string) => {
      switch (state) {
        case 'idle': // 成功调用reset接口后触发该状态机上报
          this.avPlayer.release(); // 调用release接口销毁实例对象
          break;
        case 'initialized': // avplayer 设置播放源后触发该状态上报
          this.avPlayer.prepare().then(() => {
          }, (err) => {
            console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`);
          });
          break;
        case 'prepared': // prepare调用成功后上报该状态机
          this.avPlayer.play(); // 调用播放接口开始播放
          break;
        case 'playing': // play成功调用后触发该状态机上报
          console.log('playing')
          break
        case 'paused': // pause成功调用后触发该状态机上报
          this.avPlayer.play(); // 再次播放接口开始播放
          break;
        case 'completed': // 播放结束后触发该状态机上报
          this.avPlayer.stop(); //调用播放结束接口
          break;
        case 'stopped': // stop接口成功调用后触发该状态机上报
          this.avPlayer.reset(); // 调用reset接口初始化avplayer状态
          break;
        case 'released':
          break;
        default:
          break;
      }
    });

    // 当前播放时长
    this.avPlayer.on('timeUpdate',(millisecond:number)=>{
      this.currentTime = millisecond;
      this.angle += 5;
      if(this.angle === 360)this.angle = 0;
      if(this.duration){
        this.progress = Math.ceil((this.currentTime / this.duration) * 100)
      }
      this.lyric?.seek(Math.floor(millisecond));// 歌词跳转到对应的时间线
    })

    // 总时长
    this.avPlayer.on('durationUpdate',(millisecond:number)=>{
      this.duration = millisecond
    })
  }

  build(){
    Column({space:size.pagePadding}){
      Text(this.musicItem.songName)
        .margin({top:size.pagePadding,bottom:size.pagePadding})
        .fontColor(colors.blockColor)
        .fontSize(size.bigFontSize)
      Row(){
        Row(){
          Image(HOST + this.musicItem.cover)
            .width('100%')
            .aspectRatio(1)
            .borderRadius(this.circleSize - size.smallPadding * 12)
        }.linearGradient({
          direction: GradientDirection.Top, // 渐变方向
          repeating: true, // 渐变颜色是否重复
          colors: [[0x000000, 0.0], [0x333333, 0.5], [0x000000, 1]] // 数组末尾元素占比小于1时满足重复着色效果
        })
        .width(this.circleSize - size.smallPadding * 2)
        .aspectRatio(1)
        .rotate({ angle: this.angle })
        .padding(size.smallPadding * 5)
        .borderRadius(this.circleSize - size.smallPadding * 2)
      }
      .border({
        width: size.smallPadding,
        color: colors.playerOuterCircleColor,
        style: BorderStyle.Solid
      })
      .borderRadius(this.circleSize)
      .width('80%')
      .aspectRatio(1)

      // 歌词
      if(this.lyric?.lines.length > 0){
        // 歌词列表
        List({space:size.miniPadding,scroller:this.scroller}) {
          ForEach(this.lyric?.lines, (item: LineInterface, index: number) => {
            ListItem(){
              Text(item.txt)
                .fontColor(colors.blockColor)
                .opacity(this.currentLineNum === index ? 1 : 0.5)
                .alignSelf(ItemAlign.Center)
            }.width('100%')
          })
        }
        .width('100%')
        .layoutWeight(1)
      }else{
        Row(){
          Text("暂无歌词").fontColor(colors.blockColor).opacity(0.5)
        }
        .width('100%')
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
        .alignItems(VerticalAlign.Center)
      }

      Text(this.musicItem.authorName).width('80%').fontColor(colors.blockColor)
      Row(){
        Image($r('app.media.icon_music_collect')).width(size.middlIconSize).aspectRatio(1)
        Image($r('app.media.icon_music_down')).width(size.middlIconSize).aspectRatio(1)
        Image($r('app.media.icon_music_comments')).width(size.middlIconSize).aspectRatio(1)
        Image($r('app.media.icon_music_white_menu')).width(size.middlIconSize).aspectRatio(1)
      }.width('80%').justifyContent(FlexAlign.SpaceBetween)
      Row({space:size.smallPadding}){
        Text(this.currentTime ? formatSecond(this.currentTime/1000) : '00:00').fontColor(colors.blockColor)
        Slider({value:this.progress})
          .selectedColor(colors.blockColor)
          .layoutWeight(1)
        Text(this.duration ? formatSecond(this.duration/1000) : '00:00').fontColor(colors.blockColor)
      }.width('80%').alignItems(VerticalAlign.Center)

      Row(){
        Image($r('app.media.icon_music_order')).width(size.middlIconSize).aspectRatio(1)
        Image($r('app.media.icon_music_prev')).width(size.middlIconSize).aspectRatio(1)
        Row(){
          Image($r('app.media.icon_music_play_white')).width(size.middlIconSize).aspectRatio(1)
        }
        .justifyContent(FlexAlign.Center)
        .alignItems(VerticalAlign.Center)
        .width(size.bigAvaterSize)
        .aspectRatio(1)
        .borderRadius(size.bigAvaterSize)
        .border({
          width: 2,
          color: colors.blockColor,
          style: BorderStyle.Solid
        })
        Image($r('app.media.icon_music_next')).width(size.middlIconSize).aspectRatio(1)
        Image($r('app.media.icon_music_play_menu')).width(size.middlIconSize).aspectRatio(1)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      .margin({bottom:size.pagePadding})
    }
    .backgroundImage(HOST + this.musicItem.cover,ImageRepeat.NoRepeat)
    .backgroundImagePosition({x:0,y:0})
    .backgroundImageSize({width:'100%',height:'100%'})
    .backdropBlur(200)
    .alignItems(HorizontalAlign.Center)
    .height('100%')
    .width('100%')
  }
}

github代码:GitHub - wuyuanwuhui99/Harmony-arkts-movie-music-app-ui: 基于harmony arkts开发的电影和音乐app,后端采用springboot+mybatis+mysql开发,有flutter、react-native、java、微信小程序、uniapp多个版本,参见个人主页springboot、flutter、react-native等项目包括底部tab导航,,首页,电影,电视剧,我的,搜索页,分类页,电影详情页,播放页,登录,注册,浏览记录,播放记录,收藏,缓存,电影排行榜等页面和模块、音乐,功能齐全完善,所有数据来自python爬虫程序,抓取爱奇艺和第三方电影网站实时电影数据和酷狗音乐数据,每周更新影片两只三次,持续更新中...

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个 MFC 中使用滚动条的简单示例: 1. 在对话框资源编辑器中添加一个滚动条控件,ID 为 IDC_SCROLLBAR1,设置它的范围为 0 到 100,初始位置为 0。 2. 在对话框类的头文件中添加以下代码: ``` class CMyDlg : public CDialogEx { //... private: int m_nPos; // 滚动条位置 CScrollBar m_scrollBar; // 滚动条控件对象 //... }; ``` 3. 在对话框类的实现文件中添加以下代码: ``` BOOL CMyDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 初始化滚动条控件 m_scrollBar.Attach(GetDlgItem(IDC_SCROLLBAR1)->GetSafeHwnd()); m_scrollBar.SetScrollRange(0, 100); m_scrollBar.SetScrollPos(0); m_nPos = 0; //... } void CMyDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { if (pScrollBar == &m_scrollBar) { switch (nSBCode) { case SB_LINELEFT: m_nPos--; break; case SB_LINERIGHT: m_nPos++; break; case SB_PAGELEFT: m_nPos -= 10; break; case SB_PAGERIGHT: m_nPos += 10; break; case SB_THUMBPOSITION: case SB_THUMBTRACK: m_nPos = nPos; break; default: break; } // 限制滚动条位置在范围内 m_nPos = max(0, min(100, m_nPos)); // 更新滚动条位置 m_scrollBar.SetScrollPos(m_nPos); // 更新显示内容 // TODO: 根据需要更新显示内容 } CDialogEx::OnHScroll(nSBCode, nPos, pScrollBar); } ``` 4. 在对话框类的消息映射中添加以下代码: ``` BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx) //... ON_WM_HSCROLL() //... END_MESSAGE_MAP() ``` 现在,你就可以在对话框中使用滚动条控件了。在 `OnHScroll` 函数中,根据滚动条的操作类型(`nSBCode`)更新滚动条位置(`m_nPos`),并更新滚动条控件的位置。在更新滚动条位置后,你需要根据需要更新显示内容。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值