HarmonyOS5 音乐播放器app(一):歌曲展示与收藏功能(附代码)

鸿蒙音乐应用开发:从收藏功能实现看状态管理与交互设计

在移动应用开发中,收藏功能是用户体验的重要组成部分。本文将以鸿蒙OS音乐应用为例,详细解析如何实现具有动画效果的收藏功能,涉及状态管理、组件通信和交互动画等核心技术点。

一、收藏功能的核心数据模型设计

首先定义Song数据模型,通过@Observed装饰器实现数据响应式:

@Observed
class Song {id: string = ''title: string = ''singer: string = ''mark: string = "1" // 音乐品质标识,1:sq, 2:viplabel: Resource = $r('app.media.ic_music_icon') // 歌曲封面src: string = '' // 播放路径lyric: string = '' // 歌词路径isCollected: boolean = false // 收藏状态constructor(id: string, title: string, singer: string, mark: string, label: Resource, src: string, lyric: string, isCollected: boolean) {this.id = idthis.title = titlethis.singer = singerthis.mark = markthis.label = labelthis.src = srcthis.lyric = lyricthis.isCollected = isCollected}
}

isCollected字段作为收藏状态的核心标识,配合@Observed实现数据变更时的UI自动更新。

二、首页与收藏页的整体架构

应用采用标签页架构,通过Tabs组件实现"首页"与"我的"页面切换:

@Entry
@Component
struct Index {@State currentIndex: number = 0@State songList: Array<Song> = [// 歌曲列表初始化new Song('1', '海阔天空', 'Beyond', '1', $r('app.media.ic_music_icon'), 'common/music/1.mp3', 'common/music/1.lrc', false),// 省略其他歌曲...]@BuildertabStyle(index: number, title: string, selectedImg: Resource, unselectedImg: Resource) {Column() {Image(this.currentIndex == index ? selectedImg : unselectedImg).width(20).height(20)Text(title).fontSize(16).fontColor(this.currentIndex == index ? "#ff1456" : '#ff3e4040')}}build() {Tabs() {TabContent() {RecommendedMusic({ songList: this.songList })}.tabBar(this.tabStyle(0, '首页', $r('app.media.home_selected'), $r("app.media.home")))TabContent() {CollectedMusic({ songList: this.songList })}.tabBar(this.tabStyle(1, '我的', $r('app.media.userfilling_selected'), $r("app.media.userfilling")))}.barPosition(BarPosition.End).width("100%").height("100%")}
}

通过@State管理标签页索引currentIndex,当用户切换标签时,自动更新UI显示推荐歌曲或收藏歌曲。

三、收藏功能的核心实现:状态切换与动画效果

在歌曲列表项组件中,实现收藏状态切换逻辑与动画效果:

@Component
export struct SongListItem {@Prop song: Song@Link songList: Array<Song>@Prop index: number// 动画状态标识@State isAnimating: boolean = false/*** 收藏状态切换方法*/collectStatusChange(): void {// 1. 切换当前歌曲收藏状态this.song.isCollected = !this.song.isCollected// 2. 更新列表中对应歌曲的状态this.songList = this.songList.map(item => {if (item.id === this.song.id) {item.isCollected = this.song.isCollected}return item})// 3. 触发收藏动画this.isAnimating = truesetTimeout(() => {this.isAnimating = false}, 300)// 4. 发送收藏事件(可用于全局状态同步)getContext(this).eventHub.emit('collected', this.song.id, this.song.isCollected ? '1' : '0')}build() {Column() {Row() {Column() {Text(this.song.title).fontWeight(500).fontSize(16).margin({ bottom: 4 })Row({ space: 4 }) {Image(this.song.mark === '1' ? $r('app.media.ic_vip') : $r('app.media.ic_sq')).width(16).height(16)Text(this.song.singer).fontSize(12)}}.alignItems(HorizontalAlign.Start)// 收藏图标带动画效果Image(this.song.isCollected ? $r('app.media.ic_item_collected') : $r('app.media.ic_item_uncollected')).width(50).height(50).padding(13).scale({ x: this.isAnimating ? 1.8 : 1, y: this.isAnimating ? 1.8 : 1 }).animation({ duration: 300, curve: Curve.EaseOut }).onClick(() => this.collectStatusChange())}.width("100%").padding({ left: 16, right: 16, top: 12, bottom: 12 })}.width("100%")}
}

核心动画效果通过scale变换和animation配置实现:

  • 点击时图标放大至1.8倍
  • 300ms的缓出动画(Curve.EaseOut
  • isAnimating状态控制动画的触发与结束

四、收藏列表的筛选与展示

"我的"页面通过filter方法筛选已收藏歌曲:

@Component
export struct CollectedMusic {@Link songList: Array<Song>build() {Column(){Text('收藏').fontSize(26).fontWeight(700).height(56).width('100%').padding({ left: 16, right: 16 })List(){ForEach(this.songList.filter(song => song.isCollected), (song: Song, index: number) => {ListItem() {SongListItem({ song: song, songList: this.songList })}}, (song: Song) => song.id)}.cachedCount(3).divider({ strokeWidth: 1, color: "#E5E5E5" }).scrollBar(BarState.Off)}.width("100%").height("100%")}
}

通过filter(song => song.isCollected)实现已收藏歌曲的精准筛选,确保"我的"页面只显示用户收藏的内容。

五、附:代码
import { promptAction } from "@kit.ArkUI"@Observed
class Song {id: string = ''title: string = ''singer: string = ''// 音乐品质标识,1:sq,2:vipmark: string = "1"// 歌曲封面图片label: Resource = $r('app.media.ic_music_icon')// 歌曲播放路径src: string = ''// 歌词文件路径lyric: string = ''// 收藏状态, true:已收藏 false:未收藏isCollected: boolean = falseconstructor(id: string, title: string, singer: string, mark: string, label: Resource, src: string, lyric: string, isCollected: boolean) {this.id = idthis.title = titlethis.singer = singerthis.mark = markthis.label = labelthis.src = srcthis.lyric = lyricthis.isCollected = isCollected}
}@Entry
@Component
struct Index {@State currentIndex: number = 0// 存储收藏歌曲列表@State songList: Array<Song> = [new Song('1', '海阔天空', 'Beyond', '1', $r('app.media.ic_music_icon'), 'common/music/1.mp3', 'common/music/1.lrc', false),new Song('2', '夜空中最亮的星', '逃跑计划', '1', $r('app.media.ic_music_icon'), 'common/music/2.mp3', 'common/music/2.lrc', false),new Song('3', '光年之外', 'GAI周延', '2', $r('app.media.ic_music_icon'), 'common/music/3.mp3', 'common/music/3.lrc', false),new Song('4', '起风了', '买辣椒也用券', '1', $r('app.media.ic_music_icon'), 'common/music/4.mp3', 'common/music/4.lrc', false),new Song('5', '孤勇者', '陈奕迅', '2', $r('app.media.ic_music_icon'), 'common/music/5.mp3', 'common/music/5.lrc', false)]@BuildertabStyle(index: number, title: string, selectedImg: Resource, unselectedImg: Resource) {Column() {Image(this.currentIndex == index ? selectedImg : unselectedImg).width(20).height(20)Text(title).fontSize(16).fontColor(this.currentIndex == index ? "#ff1456" : '#ff3e4040')}}build() {Tabs() {TabContent() {// 首页标签内容RecommendedMusic({ songList: this.songList})}.tabBar(this.tabStyle(0, '首页', $r('app.media.home_selected'), $r("app.media.home")))TabContent() {// 我的标签内容占位CollectedMusic({ songList: this.songList})}.tabBar(this.tabStyle(1, '我的', $r('app.media.userfilling_selected'), $r("app.media.userfilling")))}.barPosition(BarPosition.End).width("100%").height("100%").onChange((index: number) => {this.currentIndex = index})}
}// 推荐歌单
@Component
export struct RecommendedMusic {// 创建路由栈管理导航@Provide('navPath') pathStack: NavPathStack = new NavPathStack()// 热门歌单标题列表@State playListsTiles: string[] = ['每日推荐', '热门排行榜', '经典老歌', '流行金曲', '轻音乐精选']@Link songList: Array<Song>@BuildershopPage(name: string, params:string[]) {if (name === 'HotPlaylist') {HotPlayList({songList: this.songList});}}build() {Navigation(this.pathStack) {Scroll() {Column() {// 推荐标题栏Text('推荐').fontSize(26).fontWeight(700).height(56).width('100%').padding({ left: 16, right: 16 })// 水平滚动歌单列表List({ space: 10 }) {ForEach(this.playListsTiles, (item: string, index: number) => {ListItem() {HotListPlayItem({ title: item }).margin({left: index === 0 ? 16 : 0,right: index === this.playListsTiles.length - 1 ? 16 : 0}).onClick(() => {this.pathStack.pushPathByName('HotPlaylist', [item])})}}, (item: string) => item)}.height(200).width("100%").listDirection(Axis.Horizontal).edgeEffect(EdgeEffect.None).scrollBar(BarState.Off)// 热门歌曲标题栏Text('热门歌曲').fontSize(22).fontWeight(700).height(56).width('100%').padding({ left: 16, right: 16 })// 歌曲列表List() {ForEach(this.songList, (song: Song, index: number) => {ListItem() {SongListItem({ song: song, index: index,songList: this.songList})}}, (song: Song) => song.id)}.cachedCount(3).divider({strokeWidth: 1,color: "#E5E5E5",startMargin: 16,endMargin: 16}).scrollBar(BarState.Off).nestedScroll({scrollForward: NestedScrollMode.PARENT_FIRST,scrollBackward: NestedScrollMode.SELF_FIRST})}.width("100%")}.scrollBar(BarState.Off)}.hideTitleBar(true).mode(NavigationMode.Stack).navDestination(this.shopPage)}
}
// 热门歌单
@Component
export struct HotListPlayItem {@Prop title: string // 接收外部传入的标题build() {Stack() {// 背景图片Image($r('app.media.cover5')).width("100%").height('100%').objectFit(ImageFit.Cover).borderRadius(16)// 底部信息栏Row() {Column() {// 歌单标题Text(this.title).fontSize(20).fontWeight(700).fontColor("#ffffff")// 辅助文本Text("这个歌单很好听").fontSize(16).fontColor("#efefef").height(12).margin({ top: 5 })}.justifyContent(FlexAlign.SpaceBetween).alignItems(HorizontalAlign.Start).layoutWeight(1)// 播放按钮SymbolGlyph($r('sys.symbol.play_round_triangle_fill')).fontSize(36).fontColor(['#99ffffff'])}.backgroundColor("#26000000").padding({ left: 12, right: 12 }).height(72).width("100%")}.clip(true).width(220).height(200).align(Alignment.Bottom).borderRadius({ bottomLeft: 16, bottomRight: 16 })}
}// 歌曲列表项
@Component
export struct SongListItem {//定义当前歌曲,此歌曲是由前面通过遍历出来的单个数据@Prop song: Song@Link songList: Array<Song>//当前点击歌曲的index值@Prop index: number/*** 点击红心收藏效果*/collectStatusChange(): void {const songs = this.song// 切换收藏状态this.song.isCollected = !this.song.isCollected// 更新收藏列表this.songList = this.songList.map((item) => {if (item.id === songs.id) {item.isCollected = songs.isCollected}return item})promptAction.showToast({message: this.song.isCollected ? '收藏成功' : '已取消收藏',duration: 1500});// 触发全局收藏事件getContext(this).eventHub.emit('collected', this.song.id, this.song.isCollected ? '1' : '0')}aboutToAppear(): void {// 初始化歌曲数据(如果需要)}build() {Column() {Row() {Column() {Text(this.song.title).fontWeight(500).fontColor('#ff070707').fontSize(16).margin({ bottom: 4 })Row({ space: 4 }) {Image(this.song.mark === '1' ? $r('app.media.ic_vip') : $r('app.media.ic_sq')).width(16).height(16)Text(this.song.singer).fontSize(12).fontWeight(400).fontColor('#ff070707')}}.alignItems(HorizontalAlign.Start)Image(this.song.isCollected ? $r('app.media.ic_item_collected') : $r('app.media.ic_item_uncollected')).width(50).height(50).padding(13).onClick(() => {this.collectStatusChange()})}.width("100%").justifyContent(FlexAlign.SpaceBetween).padding({left: 16,right: 16,top: 12,bottom: 12}).onClick(() => {// 设置当前播放歌曲this.song = this.song// todo: 添加播放逻辑})}.width("100%")}
}@Component
export struct HotPlayList {@Prop title:string@Link songList: Array<Song>build() {NavDestination(){List(){ForEach(this.songList,(song:Song)=>{ListItem(){SongListItem({song:song,songList:this.songList})}},(song:Song)=>song.id)}.cachedCount(3).divider({strokeWidth:1,color:"#E5E5E5",startMargin:16,endMargin:16}).scrollBar(BarState.Off)}.width("100%").height("100%").title(this.title)}
}@Component
export struct CollectedMusic {@Link songList: Array<Song>build() {Column(){Text('收藏').fontSize(26).fontWeight(700).height(56).width('100%').padding({ left: 16, right: 16 })List(){ForEach(this.songList.filter((song) => song.isCollected),(song:Song,index:number)=>{ListItem(){SongListItem({song:song,songList:this.songList})}},(song:Song)=>song.id)}.cachedCount(3).layoutWeight(1).divider({strokeWidth:1,color:"#E5E5E5",startMargin:16,endMargin:16}).scrollBar(BarState.Off)}.width("100%").height("100%")}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/84373.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/84373.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PHP函数大全参考代码

字符串相关操作函数 去除空格或其他字符 trim删除字符串两端空格或其他预定义字符rtrim删除字符串右边空格或其他预定义字符choprtrim() 的别名 chop() 与 Perl 的 chop() 函数有所不同&#xff0c;它会删除字符串的最后一个字符。ltrim删除字符串左边空格或其他预定义字符 字…

Flowise工作流引擎的本地部署与远程访问实践

文章目录 前言1. Docker安装Flowise2. Ubuntu安装Cpolar3. 配置Flowise公网地址4. 远程访问Flowise5. 固定Cpolar公网地址6. 固定地址访问 前言 当多数团队仍深陷传统数据处理框架的桎梏时&#xff0c;创新者已率先引入Flowise智能流程引擎&#xff0c;成功将面向大型语言模型…

端侧AI+OS垂直创新研究报告

端侧AIOS垂直创新研究报告 摘要 端侧AIOS研究背景、核心创新点及产业价值 研究背景 随着AI技术的快速发展&#xff0c;端侧AI已成为2025年的重要技术趋势[4]。端侧AI是指将AI计算能力从云端迁移到终端设备上&#xff0c;实现本地化的智能处理。这一技术变革主要受到隐私安全…

【JVM 07-运行时常量池重要组成部分-StringTable】

StringTable 笔记记录 1. 常量池、运行时常量池与字符串常量池(StringTable)的关系2. String str"a"放入字符串常量池的过程3. 常见面试题4. StringTable特性5.StringTable的位置变更5.1 为什么位置变换&#xff1f;5.2 位置变更演示 6. StringTable垃圾回收7. Strin…

算法-每日一题(DAY10)打家劫舍

1.题目链接&#xff1a; 198. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09; 2.题目描述&#xff1a; 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xf…

android UI 布局

一&#xff1a;约束布局 参考&#xff1a; 【约束布局】ConstraintLayout 约束布局 ( 简介 | 引入依赖 | 基本操作 | 垂直定位约束 | 角度定位约束 | 基线约束 )_韩曙亮-2048 AI社区 以下是一个基于 ConstraintLayout 的简单 Android 示例&#xff0c;包含三个控件&#xff0…

【K8S】详解Labels​​ 和 ​​Annotations

在 Kubernetes&#xff08;K8s&#xff09;中&#xff0c;​​Labels&#xff08;标签&#xff09;​​ 和 ​​Annotations&#xff08;注解&#xff09;​​ 都是用于为资源对象&#xff08;如 Pod、Service、Deployment&#xff09;附加元数据的机制&#xff0c;但它们在设计…

系统模块编程与实现

设备类&#xff08;Device Class&#xff09;​​ 和 ​​设备节点&#xff08;Device Node&#xff09;​​是深入 Linux 设备管理和驱动模型的核心基础。它们就像“骨骼”与“门户”&#xff0c;共同构建了 Linux 与硬件交互的核心桥梁。 一、设备类与设备节点 1. ​​设备…

视频压缩、码率与流媒体传输知识总结

&#x1f3a5; 视频压缩、码率与流媒体传输知识总结 本笔记整理了 I/P/B 帧结构、码率计算、文件大小估算、压缩格式对比、推流带宽建议等视频工程常见技术要点。 一、单帧与未压缩视频数据量估算 分辨率&#xff1a;19201080&#xff08;1080p&#xff09; 色深&#xff1a;…

嵌入式C++学习路线

&#x1f680; 嵌入式C学习路线图 从C语言基础到嵌入式C高手的完整路径 &#x1f4cb; 学习进度追踪 总体目标&#xff1a; 20-26周完成全部学习内容 前置条件&#xff1a; C语言基础 STM32开发经验 学习方式&#xff1a; 理论学习 实践项目 阶段1: C基础过渡 (2-3周) 目标…

VSCode1.101.1Win多语言语言编辑器便携版安装教程

软件下载 【名称】&#xff1a; VSCode1.101.1 【大小】&#xff1a; 120M 【语言】&#xff1a; 简体中文 【安装环境】&#xff1a; Win10/Win11 【迅雷网盘下载链接】&#xff08;务必手机注册&#xff09;&#xff1a; 迅雷 【网站下载链接】: 其他网盘 软件介绍 VSCod…

ssh 服务和 rsync 数据同步

目录 一、ssh服务 1、概述 2、命令解析 远程登录命令 远程拷贝命令 3、登录方式配置 1、用户名密码登录 2、公钥验证登录 二、rsync 数据同步 1、rsync概述 2、rsync运行原理 3、rsync部署 一、ssh服务 1、概述 ssh服务&#xff0c;一种远程管理连接工具&#xf…

使用随机森林实现目标检测

核心实现思路 滑动窗口策略&#xff1a;在图像上滑动固定大小的窗口&#xff0c;对每个窗口进行分类多维特征提取&#xff1a;结合统计特征、纹理特征、边缘特征、形状特征等随机森林分类&#xff1a;训练二分类器判断窗口是否包含目标后处理优化&#xff1a;使用非极大值抑制…

3.6 move_base导航初体验

1.环境搭建 在工作空间src下git wpr_simulation&#xff0c;安装install_for_noetic.sh&#xff0c;然后再回退工作空间进行编译 下载参数文件 git clone https://github.com/6-robot/wpb_home.git下载需要魔法&#xff0c;在这里可以使用手机热点进行平替 进入脚本文件夹 …

Mysql高级——MVCC(多版本并发控制)

MySQL MVCC&#xff08;多版本并发控制&#xff09;详解 MVCC&#xff08;Multi-Version Concurrency Control&#xff09;是 MySQL InnoDB 存储引擎实现的一种并发控制机制&#xff0c;用于在保证事务隔离性的同时&#xff0c;提高数据库的并发性能。下面从原理、实现、事务隔…

Oracle union连接的怎么排序

在Oracle数据库中&#xff0c;使用UNION或UNION ALL操作符来合并两个或多个查询结果时&#xff0c;如果想对这些合并后的结果进行排序&#xff0c;通常有两种方法可以实现&#xff1a; 方法1&#xff1a;在最后的查询结果上使用ORDER BY 你可以在所有使用UNION或UNION ALL合并…

uni-app总结2-所需知识储备和学习途径

使用uni-app进行跨平台开发&#xff0c;开发者不用去掌握各个平台的开发语言&#xff0c;只需一套代码即可完成多端的产品输出。那么使用uni-app需要掌握什么呢&#xff0c;这里给大家分享一下。 Vue.js uni-app里是通过Vue来开发的&#xff0c;所以首先肯定是要掌握Vue语言。…

如何高效实现公司文件管理

要实现公司文件管理的高效&#xff0c;企业应聚焦统一文件规范、部署文档管理系统、强化权限控制、推动协同编辑、实施定期清理、推进文化建设、引入可视化分析。其中&#xff0c;统一文件规范是文件高效管理的基础。若缺乏清晰的命名规则与分类体系&#xff0c;即便配备了先进…

多模态大语言模型arxiv论文略读(124)

MediConfusion: Can you trust your AI radiologist? Probing the reliability of multimodal medical foundation models ➡️ 论文标题&#xff1a;MediConfusion: Can you trust your AI radiologist? Probing the reliability of multimodal medical foundation models …

nacos的总结

服务发现与健康监测&#xff1a;Nacos 支持多种服务注册方式&#xff0c;包括 API、SDK 和 Annotation 等&#xff0c;服务消费者可以通过 DNS 或 RPC 方式方便地发现服务。其健康检查机制通过主动和被动的方式实时监测服务实例的健康状态&#xff0c;确保流量不会被发送到不健…