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

2025-06-28 20:44:37
110次阅读
0个评论

screenshots.gif

鸿蒙音乐应用收藏功能开发指南

一、核心数据模型设计

响应式歌曲数据模型

@Observed
class Song {
  id: string = '';
  title: string = '';
  singer: string = '';
  mark: string = '1'; // 1:标准音质 2:VIP音质
  label: 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) {
    // 初始化逻辑
  }
}

二、应用整体架构

标签页导航实现

@Entry
@Component
struct Index {
  @State currentIndex: number = 0;
  @State songList: Song[] = [
    new Song('1', '海阔天空', 'Beyond', '1', 
             $r('app.media.ic_music_icon'), '1.mp3', '1.lrc', false),
    // 其他歌曲...
  ];

  build() {
    Tabs({ barPosition: BarPosition.End }) {
      TabContent() {
        RecommendedMusic({ songList: $songList })
      }.tabBar(this.tabContent('首页', 0))
      
      TabContent() {
        CollectedMusic({ songList: $songList })
      }.tabBar(this.tabContent('我的', 1))
    }
  }

  @Builder
  tabContent(title: string, index: number) {
    // 标签页样式构建
  }
}

三、收藏功能核心实现

歌曲列表项组件

@Component
export struct SongListItem {
  @Prop song: Song;
  @Link songList: Song[];
  @State isAnimating: boolean = false;

  // 收藏状态切换
  private toggleCollect() {
    this.song.isCollected = !this.song.isCollected;
    this.updateSongList();
    this.triggerAnimation();
    this.emitCollectEvent();
  }

  // 更新歌曲列表状态
  private updateSongList() {
    this.songList = this.songList.map(item => 
      item.id === this.song.id ? { ...item, isCollected: this.song.isCollected } : item
    );
  }

  // 触发收藏动画
  private triggerAnimation() {
    this.isAnimating = true;
    setTimeout(() => this.isAnimating = false, 300);
  }

  // 发送全局事件
  private emitCollectEvent() {
    getContext(this).eventHub.emit('collected', 
      this.song.id, this.song.isCollected ? '1' : '0');
  }

  build() {
    Row() {
      // 歌曲信息区域...
      
      // 收藏按钮带动画
      Image(this.song.isCollected 
            ? $r('app.media.ic_item_collected') 
            : $r('app.media.ic_item_uncollected'))
        .scale({ x: this.isAnimating ? 1.8 : 1, y: this.isAnimating ? 1.8 : 1 })
        .animation({ duration: 300, curve: Curve.EaseOut })
        .onClick(() => this.toggleCollect())
    }
  }
}

四、收藏页面实现

已收藏歌曲筛选展示

@Component
export struct CollectedMusic {
  @Link songList: Song[];

  build() {
    Column() {
      Text('收藏').fontSize(26).fontWeight(FontWeight.Bold);
      
      List() {
        ForEach(
          this.songList.filter(song => song.isCollected),
          (song: Song) => {
            ListItem() {
              SongListItem({ song: song, songList: $songList })
            }
          }
        )
      }
    }
  }
}

五、关键交互设计

收藏动画实现原理

.scale({ x: this.isAnimating ? 1.8 : 1, y: this.isAnimating ? 1.8 : 1 })
.animation({ 
  duration: 300, 
  curve: Curve.EaseOut 
})

状态管理流程

用户点击 → 更新isCollected状态 → 触发动画 → 更新全局列表 → 发送事件通知

六、完整代码结构

点击查看完整实现
import { promptAction } from "@kit.ArkUI"

@Observed
class Song {
  id: string = ''
  title: string = ''
  singer: string = ''
  // 音乐品质标识,1:sq,2:vip
  mark: string = "1"
  // 歌曲封面图片
  label: Resource = $r('app.media.ic_music_icon')
  // 歌曲播放路径
  src: string = ''
  // 歌词文件路径
  lyric: string = ''
  // 收藏状态, true:已收藏 false:未收藏
  isCollected: boolean = false

  constructor(id: string, title: string, singer: string, mark: string, label: Resource, src: string, lyric: string, isCollected: boolean) {
    this.id = id
    this.title = title
    this.singer = singer
    this.mark = mark
    this.label = label
    this.src = src
    this.lyric = lyric
    this.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)
  ]
  @Builder
  tabStyle(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>
  @Builder
  shopPage(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%")
  }
}

七、设计要点总结

  1. 响应式数据:使用@Observed实现数据变化自动更新UI
  2. 状态管理:通过@State和@Link实现组件间状态共享
  3. 动画效果:scale变换配合animation实现流畅交互
  4. 数据筛选:filter方法实现收藏歌曲快速筛选
  5. 事件通信:eventHub实现跨组件事件通知
收藏00

登录 后评论。没有帐号? 注册 一个。