当前位置: 首页 > news >正文

HarmonyOS7 ArkUI 歌词滚动播放器 - 歌词同步高亮、进度控制与翻译切换实战:从状态到页面反馈

文章目录

      • 前言
      • 真实页面会关心什么
      • 列表、状态和反馈
      • 关键片段
        • 片段 1:`get currentLineIndex(): number {`
        • 片段 2:`get progress(): number {`
      • 使用方式
      • 完整代码
      • 可扩展点

前言

如果刚开始写 HarmonyOS7 页面,我建议多拆这种案例。代码量不夸张,但能把声明式 UI 的味道摸清楚。
它的主线是歌词滚动播放器,细节落在歌词同步高亮、进度控制与翻译切换。这些细节刚好能把页面状态、用户操作和组件刷新串起来。

真实页面会关心什么

LyricPlayerPage不是一个只展示静态内容的页面。它会根据用户点击、输入、切换或计时来改变界面,所以阅读代码时可以先抓三条线:

观察点在这个案例里的表现
页面主题歌词滚动播放器
主要文案晴天,周杰伦,叶惠美,故事的小黄花,从出生那年就飘着,童年的荡秋千
常用组件Column,ForEach,Row,Scroll,Slider,Stack,Text
适合练习状态驱动 UI、条件样式、列表渲染、事件回调

列表、状态和反馈

ArkUI 的页面刷新依赖状态变化。下面这些@State字段就是页面的“开关”和“数据源”,不用手动找 DOM,也不用自己通知某个控件刷新。

状态字段类型我会怎么理解它
currentTimenumber当前选中项,决定高亮和内容切换
isPlayingboolean布尔开关,控制显示隐藏或模式切换

|showTranslation|boolean| 布尔开关,控制显示隐藏或模式切换 |
|fontSize|number| 记录页面当前选择、输入或展示状态 |
|timerHandle|number| 记录页面当前选择、输入或展示状态 |

一个经验:先把@State看完,再看build(),页面逻辑会清楚很多。否则很容易被一长串布局代码带偏。

关键片段

片段 1:get currentLineIndex(): number {

这段代码承担的是页面里的“判断”或“动作”,把它从布局里抽出来后,build()会清爽很多。后面要加新条件,也不会到处翻 UI 代码。

getcurrentLineIndex():number{letidx=0for(leti=0;i<this.lyrics.length;i++){if(this.currentTime>=this.lyrics[i].time){idx=i}}returnidx}
片段 2:get progress(): number {

这段代码承担的是页面里的“判断”或“动作”,把它从布局里抽出来后,build()会清爽很多。后面要加新条件,也不会到处翻 UI 代码。

getprogress():number{returnthis.currentTime/this.song.duration}

使用方式

把下面代码放进 ArkTS 页面文件中即可运行。用于正式项目时,我会继续把数据模型、卡片 Builder 和页面事件拆出去,页面文件只负责组合。

建议练习顺序:先改状态字段 -> 再改交互事件 -> 最后调整 UI 样式 检查重点:点击是否刷新、列表 key 是否稳定、条件样式是否集中管理
这份代码可以继续扩展的方向
  • 把模拟数据替换成接口数据
  • 抽出复用卡片组件
  • 增加空状态和异常状态
  • 补充深色模式或主题色适配
  • 对输入类场景增加校验提示

完整代码

// LyricPlayerPage - 歌词滚动播放器 - 歌词同步高亮、进度控制与翻译切换interfaceLyricLine{time:numbertext:stringtranslation:string}interfaceSongInfo_lnjm{title:stringartist:stringalbum:stringduration:number}@Entry@Componentstruct LyricPlayerPage{@StatecurrentTime:number=45@StateisPlaying:boolean=false@StateshowTranslation:boolean=true@StatefontSize:number=16@StatetimerHandle:number=-1privatesong:SongInfo_lnjm={title:'晴天',artist:'周杰伦',album:'叶惠美',duration:269,}privatelyrics:LyricLine[]=[{time:0,text:'故事的小黄花',translation:'The little dandelion of the story'},{time:5,text:'从出生那年就飘着',translation:'Has been floating since the year of birth'},{time:10,text:'童年的荡秋千',translation:'Swinging on the childhood swing'},{time:15,text:'随记忆一直晃到现在',translation:'Swaying along with memories until now'},{time:20,text:'Rui Bing说他不明白',translation:'Rui Bing says he doesn\'t understand'},{time:25,text:'为什么我吉他总在半夜弹',translation:'Why I always play guitar at midnight'},{time:30,text:'她说她喜欢男生弹吉他的样子',translation:'She said she likes how boys look playing guitar'},{time:35,text:'我说这简单',translation:'I said it\'s simple'},{time:40,text:'等你学会',translation:'Wait till you learn'},{time:45,text:'我再好好说清楚',translation:'I\'ll explain it properly'},{time:50,text:'那等等',translation:'Then wait a moment'},{time:55,text:'我先把歌唱完',translation:'Let me finish the song first'},{time:60,text:'你今天哭了吗',translation:'Did you cry today'},{time:65,text:'不管有没有眼泪',translation:'Whether or not there are tears'},{time:70,text:'在你脸上来回',translation:'Running down your face'},{time:75,text:'你笑了吗',translation:'Did you smile'},{time:80,text:'不管有没有阳光',translation:'Whether or not there is sunlight'},{time:85,text:'那个秋天的童年',translation:'That childhood autumn'},{time:90,text:'印象太深刻了',translation:'The impression is too deep'},{time:95,text:'不知不觉开始忘了',translation:'Gradually started to forget'},{time:100,text:'晴天',translation:'Sunny day'},{time:108,text:'盼望着晴天',translation:'Hoping for a sunny day'},]getcurrentLineIndex():number{letidx=0for(leti=0;i<this.lyrics.length;i++){if(this.currentTime>=this.lyrics[i].time){idx=i}}returnidx}getprogress():number{returnthis.currentTime/this.song.duration}formatTime(seconds:number):string{constm=Math.floor(seconds/60)consts=Math.floor(seconds%60)return`${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`}togglePlay(){this.isPlaying=!this.isPlayingif(this.isPlaying){this.timerHandle=setInterval(()=>{if(this.currentTime<this.song.duration){this.currentTime++}else{this.currentTime=0clearInterval(this.timerHandle)this.isPlaying=false}},1000)}else{clearInterval(this.timerHandle)}}aboutToDisappear(){if(this.timerHandle>=0)clearInterval(this.timerHandle)}build(){Column({space:0}){// 顶部区域Column({space:16}){// 专辑封面Stack(){Column().width(160).height(160).borderRadius(80).linearGradient({angle:135,colors:[['#667eea',0],['#764ba2',0.5],['#f64f59',1.0]]}).shadow({radius:20,color:'#667eea50',offsetX:0,offsetY:8}).rotate({x:0,y:0,z:1,angle:this.isPlaying?360:0}).animation({duration:8000,curve:Curve.Linear,iterations:-1,playMode:PlayMode.Normal})Circle().width(40).height(40).fill('#ffffff')Circle().width(16).height(16).fill('#333333')}Column({space:4}){Text(this.song.title).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')Text(`${this.song.artist}·${this.song.album}`).fontSize(14).fontColor('#888888')}.alignItems(HorizontalAlign.Center)}.width('100%').padding({top:24,bottom:20}).backgroundColor('#ffffff').alignItems(HorizontalAlign.Center)// 歌词显示区域Column({space:0}){// 工具栏Row(){Text('A-').fontSize(14).fontColor('#666666').padding({left:12,right:12,top:6,bottom:6}).backgroundColor('#f0f0f0').borderRadius(16).onClick(()=>{if(this.fontSize>12)this.fontSize-=2})Text(`${this.fontSize}px`).fontSize(13).fontColor('#999999').margin({left:8,right:8})Text('A+').fontSize(14).fontColor('#666666').padding({left:12,right:12,top:6,bottom:6}).backgroundColor('#f0f0f0').borderRadius(16).onClick(()=>{if(this.fontSize<22)this.fontSize+=2})Blank()Row({space:6}){Text('译').fontSize(13).fontColor(this.showTranslation?'#ffffff':'#666666').backgroundColor(this.showTranslation?'#6c5ce7':'#f0f0f0').padding({left:12,right:12,top:6,bottom:6}).borderRadius(16).onClick(()=>{this.showTranslation=!this.showTranslation})}}.padding({left:16,right:16,top:10,bottom:10}).backgroundColor('#ffffff')// 歌词列表Scroll(){Column({space:0}){ForEach(this.lyrics,(line:LyricLine,idx:number)=>{Column({space:4}){Text(line.text).fontSize(idx===this.currentLineIndex?this.fontSize+2:this.fontSize).fontWeight(idx===this.currentLineIndex?FontWeight.Bold:FontWeight.Normal).fontColor(idx===this.currentLineIndex?'#6c5ce7':idx<this.currentLineIndex?'#bbbbbb':'#444444').textAlign(TextAlign.Center).width('100%').animation({duration:300,curve:Curve.EaseOut})if(this.showTranslation){Text(line.translation).fontSize(this.fontSize-3).fontColor(idx===this.currentLineIndex?'#a29bfe':'#cccccc').textAlign(TextAlign.Center).width('100%')}}.padding({top:10,bottom:10,left:24,right:24}).backgroundColor(idx===this.currentLineIndex?'#f5f2ff':'transparent').borderRadius(8).margin({left:8,right:8})})Column().height(40)}.padding({top:16})}.layoutWeight(1).backgroundColor('#fafafa')}.layoutWeight(1)// 播放控制区Column({space:16}){// 进度条Column({space:6}){Slider({value:this.currentTime,min:0,max:this.song.duration,step:1,style:SliderStyle.OutSet,}).width('100%').trackColor('#e0e0e0').selectedColor('#6c5ce7').onChange((v:number)=>{this.currentTime=Math.round(v)})Row(){Text(this.formatTime(this.currentTime)).fontSize(12).fontColor('#999999')Blank()Text(this.formatTime(this.song.duration)).fontSize(12).fontColor('#999999')}.width('100%')}// 播放按钮Row({space:32}){Text('⏮').fontSize(28).fontColor('#444444')Stack(){Circle().width(60).height(60).fill('#6c5ce7')Text(this.isPlaying?'⏸':'▶').fontSize(22).fontColor('#ffffff')}.onClick(()=>this.togglePlay())Text('⏭').fontSize(28).fontColor('#444444')}.width('100%').justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center)}.padding({left:24,right:24,top:16,bottom:32}).backgroundColor('#ffffff')}.width('100%').height('100%').backgroundColor('#ffffff')}}

可扩展点

这个案例可以当成一个小模板:先把页面会变的东西放进状态,再用函数或 Builder 处理重复逻辑,最后让布局只关心展示。写 HarmonyOS7 页面时,这个顺序通常比一上来堆 UI 更稳。

http://www.jsqmd.com/news/1175729/

相关文章:

  • PUBG地图黑客安全部署指南:避免封号的10个终极技巧
  • 2026年装修避坑指南:西安本地新房装修哪家好,整装交付不踩坑 - 小随科技
  • raf polyfill 实现原理揭秘:深入理解浏览器兼容性解决方案
  • 西安欧米茄回收价格查询及各大回收平台实测排行(2026年7月最新) - 天价名表回收平台
  • 备战2026系规考试,最怕“听不懂、学不会、写不出”?深度拆解软考老金团队如何用“三位一体”教学法,帮你扫清备考路上所有障碍
  • ★★★隐身截屏Pro 新版V1.3 功能全面介绍
  • Seekr主题定制:如何打造个性化的OSINT工作环境
  • ComfyUI-LTXVideo完整指南:轻松实现AI视频生成与编辑
  • morss实用技巧:10个提升RSS阅读效率的方法
  • 亲身到店探访杭州亨得利官方名表服务中心|全新维修地址及客服热线(2026年7月更新) - 亨得利官方
  • 宏观认识大模型
  • 想了解西安豪美装修工艺/施工工艺?先看施工链条,再看落地表现 - 子柔传媒
  • 2026年装修避坑指南:西安本地新房装修哪家好,整装交付不踩坑 - 科技快讯
  • WebWalker社区贡献指南:如何参与LLM网页遍历基准测试的开发
  • 亨得利官方名表服务中心|网点地址与官方电话权威信息通告(2026年7月更新) - 亨得利官方博客
  • 文本分析合集数据
  • 北京宝珀回收价格查询和靠谱回收平台实测排行(2026年7月最新) - 收的高名表回收平台
  • Linux(二)Linux账号与权限管理
  • Open Source Candies代码审查工具大比拼:Codacy vs CodeClimate vs SonarCloud
  • 海口闲置奢侈品鞋服怎么卖?2026LV/古驰/大鹅鞋服保值回收攻略 - 每日生活报
  • ChangeDetection核心功能大揭秘:从网页比较到后台同步,5大特性让你告别手动检查
  • 出包不踩雷干货:判断上海包包回收靠谱门店的 5 个标准,实现上海高价回收包包 - 讯息早知道
  • Vite 和 Turbopack 的构建性能对比:冷启动、HMR 和生产构建全维度测试
  • Qwen3 VL多模态解析
  • 2026年装修避坑指南:西安本地新房装修哪家好,整装交付不踩坑 - 子柔传媒
  • 零基础学 Python 第 4 章 | for 循环与 range():让程序帮你重复干活
  • MVP变换
  • 1. 生产级 Agent 完整实现流程2. Agent 状态机与工作流引擎3.长对话上下文管理与压缩4. Agent 流式响应与中断控制
  • 2026年7月最新乌鲁木齐格拉苏蒂官方售后客户服务热线与维修网点地址汇总 - 亨得利钟表维修中心
  • 2026年奎屯正规优选GEO优化公司优势及选择推荐