中工-音乐编辑

1.介绍


本篇Codelab基于自适应布局和响应式布开发的一款音乐专辑软件,功能主要是一些音乐播放器的普通功能:播放音乐、上一首、下一首、暂停播放、歌单评论等,手机效果如图所示:

2 环境搭建
1.软件要求
•    DevEco Studio版本:DevEco Studio NEXT Developer Beta1及以上。
•    HarmonyOS SDK版本:HarmonyOS NEXT Developer Beta1 SDK及以上。
2.硬件要求
•    设备类型:华为手机。
•    HarmonyOS系统:HarmonyOS NEXT Developer Beta1及以上。


项目创建如下:
 
3 代码结构解读
├──common                                     // 公共能力层
│  ├──constantsCommon/src/main/ets/constants  // 公共常量层
│  │  ├──BreakpointConstants.ets              // 断点常量类
│  │  ├──GridConstants.ets                    // 栅格常量类
│  │  ├──RouterUrlConstants.ets               // 地址常量类
│  │  ├──SongConstants.ets                    // 音乐常量类
│  │  └──StyleConstants.ets                   // 样式常量类
│  └──mediaCommon/src/main/ets                // 公共方法层 
│     ├──utils
│     │  ├──BackgroundUtil.ets                // 后台任务工具类
│     │  ├──BreakpointSystem.ets              // 断点工具类
│     │  ├──ColorConversion.ets               // 颜色转换工具类
│     │  ├──Logger.ets                        // 日志工具类
│     │  ├──MediaService.ets                  // 音乐操作工具类
│     │  ├──MediaTools.ets                    // 音乐时间工具类
│     │  ├──PreferencesUtil.ets               // 数据存储工具类 
│     │  └──SongItemBuilder.ets               // 歌曲构造工具类
│     └──viewmodel
│        ├──CardData.ets                      // 卡片数据实体类
│        ├──MenuData.ets                      // 菜单数据实体类
│        ├──MusicData.ets                     // 状态和类型数据类
│        └──SongData.ets                      // 歌曲数据实体类
├──features                                   // 基础特性层
│  ├──live/src/main/ets                       // 直播页面
│  │  ├──constants
│  │  │  └──LiveConstants.ets                 // 直播常量类
│  │  ├──view
│  │  │  ├──Header.ets                        // 顶部标题栏
│  │  │  ├──LiveList.ets                      // 直播列表
│  │  │  └──LivePage.ets                      // 直播主页面
│  │  └──viewmodel
│  │     ├──LiveStream.ets                    // 直播数据实体类
│  │     └──LiveStreamViewModel.ets           // 直播列表数据类              
│  ├──live/src/main/resources                 // 资源文件目录
│  ├──musicComment/src/main/ets               // 评论页面
│  │  ├──constants
│  │  │  └──CommonConstants.ets               // 评论常量类
│  │  ├──view
│  │  │  ├──HeadComponent.ets                 // 顶部标题栏
│  │  │  ├──ListItemComponent.ets             // 评论列表
│  │  │  ├──MusicCommentPage.ets              // 音乐评论主页面
│  │  │  └──MusicInfoComponent.ets            // 歌曲信息简介
│  │  └──viewmodel
│  │     ├──Comment.ets                       // 评论数据实体类
│  │     └──CommentViewModel.ets              // 评论列表数据类              
│  ├──musicComment/src/main/resources         // 资源文件目录
│  ├──musicList/src/main/ets                  // 音乐列表页面
│  │  ├──components
│  │  │  ├──AlbumComponent.ets                // 封面内容组件 
│  │  │  ├──AlbumCover.ets                    // 专辑封面组件
│  │  │  ├──ControlAreaComponent.ets          // 播放控制自定义组件 
│  │  │  ├──Header.ets                        // 顶部标题栏组件
│  │  │  ├──ListContent.ets                   // 列表内容组件
│  │  │  ├──LyricsComponent.ets               // 歌词区域组件
│  │  │  ├──MusicControlComponent.ets         // 歌曲区域组件
│  │  │  ├──MusicInfoComponent.ets            // 音乐信息组件
│  │  │  ├──Player.ets                        // 播放控制区组件 
│  │  │  ├──PlayList.ets                      // 播放列表组件
│  │  │  └──TopAreaComponent.ets              // 歌词顶部区域组件
│  │  ├──constants
│  │  │  ├──ContentConstants.ets              // 音乐列表常量类
│  │  │  ├──HeaderConstants.ets               // 列表头部常量类
│  │  │  └──PlayerConstants.ets               // 列表播控区常量类
│  │  ├──lyric
│  │  │  ├──LrcEntry..ets                     // 歌词信息实体类
│  │  │  ├──LrcUtils.ets                      // 歌词解析工具类
│  │  │  ├──LrcView.ets                       // 歌词详情自定义组件
│  │  │  └──LyricConst.ets                    // 歌词常量类
│  │  ├──view
│  │  │  └──MusicListPage.ets                 // 音乐列表主页面
│  │  ├──viewmodel
│  │  │  ├──SongDataSource.ets                // 歌曲数据操作类  
│  │  │  └──SongListData.ets                  // 歌曲列表数据类              
│  ├──musicList/src/main/resources            // 资源文件目录
└──products                                   // 产品定制层
   ├──phone/src/main/ets                      // 支持手机、平板
   │  ├──common
   │  │  └──constants
   │  │     └──HomeConstants.ets              // 音乐常量类
   │  ├──entryability
   │  │  └──EntryAbility.ets                  // 程序入口类
   │  ├──pages
   │  │  └──Index.ets                         // 主界面
   │  ├──phonebackupextability
   │  │  └──PhoneBackupExtAbility.ets         // 备份恢复类
   │  └──viewmodel
   │     ├──IndexItem.ets                     // 音乐信息实体类
   │     └──IndexViewModel.ets                // 音乐主页数据类 
   └──phone/src/main/resources                // 资源文件目录
4 界面解读
4.1 音乐列表页
在音乐列表页中,音乐列表页由标题栏、专辑封面、歌曲列表和播放控制区四部分组成。
 
第①部分 标题栏在不同设备的标题栏始终只显示“返回按钮”、“歌单”以及“更多按钮”,中间空白区域通过Blank组件填充,可实现自适应拉伸能力。
 
1.    @Component
2.    export struct Header {
3.      @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointConstants.BREAKPOINT_SM;
4.      @StorageLink('pageIndexInfos') pageIndexInfos: NavPathStack = new NavPathStack();
5.    
6.    
7.      build() {
8.        Row() {
9.          Image($r('app.media.ic_back'))
10.            .width($r('app.float.icon_width'))
11.            .height($r('app.float.icon_height'))
12.            .margin({ left: $r('app.float.icon_margin') })
13.            .onClick(() => {
14.              this.pageIndexInfos.pop();
15.            })
16.          Text($r('app.string.play_list'))
17.            .fontSize(new BreakpointType({
18.              sm: $r('app.float.header_font_sm'),
19.              md: $r('app.float.header_font_md'),
20.              lg: $r('app.float.header_font_lg')
21.            }).getValue(this.currentBreakpoint))
22.            .fontWeight(HeaderConstants.TITLE_FONT_WEIGHT)
23.            .fontColor($r('app.color.title_color'))
24.            .opacity($r('app.float.title_opacity'))
25.            .letterSpacing(HeaderConstants.LETTER_SPACING)
26.            .padding({ left: $r('app.float.title_padding_left') })
27.    
28.    
29.          Blank()
30.    
31.    
32.          Image($r('app.media.ic_more'))
33.            .width($r('app.float.icon_width'))
34.            .height($r('app.float.icon_height'))
35.            .margin({ right: $r('app.float.icon_margin') })
36.            .bindMenu(this.getMenu())
37.        }
38.        .width(StyleConstants.FULL_WIDTH)
39.        .height($r('app.float.title_bar_height'))
40.        .zIndex(HeaderConstants.Z_INDEX)
41.      }
42.      ...
43.    }
第②部分 专辑封面由图片、歌单介绍及常用操作三部分组成,这三部分的布局可以用栅格实现。
•    在sm断点下,图片占4个栅格,歌单介绍占8个栅格,常用操作占12个栅格。
•    在md断点下,图片、歌单介绍和常用操作分别占12个栅格。
•    在lg断点下,图片、歌单介绍和常用操作分别占12个栅格。
 
// features/musicList/src/main/ets/components/AlbumComponent.ets
@Component
export struct AlbumComponent {
  ...

  build() {
    Column() {
      GridRow() {
        GridCol({
          span: { sm: GridConstants.SPAN_FOUR, md: GridConstants.SPAN_TWELVE, lg: GridConstants.SPAN_TWELVE }
        }) {
          this.CoverImage()
        }

        GridCol({
          span: { sm: GridConstants.SPAN_EIGHT, md: GridConstants.SPAN_TWELVE, lg: GridConstants.SPAN_TWELVE }
        }) {
          this.CoverIntroduction()
        }

        GridCol({
          span: { sm: GridConstants.SPAN_TWELVE, md: GridConstants.SPAN_TWELVE, lg: GridConstants.SPAN_TWELVE }
        }) {
          this.CoverOptions()
        }
        .padding({
          top: this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM ? $r('app.float.option_margin') : 0,
          bottom: this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM ? $r('app.float.option_margin') : 0
        })
      }
      .padding({
        top: this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM ?
        $r('app.float.cover_padding_top_sm') : $r('app.float.cover_padding_top_other'),
        left: new BreakpointType({
          sm: $r('app.float.album_padding_sm'),
          md: $r('app.float.album_padding_md'),
          lg: $r('app.float.album_padding_lg')
        }).getValue(this.currentBreakpoint),
        right: new BreakpointType({
          sm: $r('app.float.album_padding_sm'),
          md: $r('app.float.album_padding_md'),
          lg: $r('app.float.album_padding_lg')
        }).getValue(this.currentBreakpoint)
      })
    }
    .margin({
      left: new BreakpointType({
        sm: $r('app.float.cover_margin_sm'),
        md: $r('app.float.cover_margin_md'),
        lg: $r('app.float.cover_margin_lg')
      }).getValue(this.currentBreakpoint),
      right: new BreakpointType({
        sm: $r('app.float.cover_margin_sm'),
        md: $r('app.float.cover_margin_md'),
        lg: $r('app.float.cover_margin_lg')
      }).getValue(this.currentBreakpoint)
    })
  }
第③部分 对于不同屏幕尺寸的设备,歌曲列表的样式基本一致,但sm和md断点下的歌曲列表是单列显示,lg断点下的歌曲列表是双列显示。可以通过List组件的lanes属性实现这一效果。
 
// features/musicList/src/main/ets/components/PlayList.ets
@Component
export struct PlayList {
  ...

  build() {
    Column() {
      this.PlayAll()
      List() {
        LazyForEach(new SongDataSource(this.songList), (item: SongItem, index: number) => {
          ListItem() {
            Column() {
              this.SongItem(item, index)
            }
            .padding({
              left: $r('app.float.list_item_padding'),
              right: $r('app.float.list_item_padding')
            })
          }
        }, (item: SongItem, index?: number) => JSON.stringify(item) + index)
      }
      .width(StyleConstants.FULL_WIDTH)
      .backgroundColor(Color.White)
      .margin({ top: $r('app.float.list_area_margin_top') })
      .lanes(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ?
        ContentConstants.COL_TWO : ContentConstants.COL_ONE)
      .layoutWeight(1)
      .divider({
        color: $r('app.color.list_divider'),
        strokeWidth: $r('app.float.stroke_width'),
        startMargin: $r('app.float.list_item_padding'),
        endMargin: $r('app.float.list_item_padding')
      })
    }
    .padding({
      top: this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM ? 0 : $r('app.float.list_area_padding_top'),
      bottom: $r('app.float.list_area_padding_bottom')
    })
  }
}
第④部分对于不同屏幕尺寸的设备,播放控制区显示的内容有差异,sm断点仅显示播放按钮,md断点显示上一首、播放和下一首按钮,lg断点显示收藏、上一首、播放、下一首、列表按钮,可以分别设置按钮的displayPriority属性,通过自适应隐藏能力实现。同时,歌曲信息与播放控制按钮之间通过Blank组件自动填充,实现自适应拉伸。 


// features/musicList/src/main/ets/components/Player.ets
@Component
export struct Player {
  ...
  build() {
    Row() {
      ...
      Row() {
        Image($r('app.media.ic_previous'))
          ...
          .displayPriority(PlayerConstants.DISPLAY_PRIORITY_TWO)
        Image(this.isPlay ? $r('app.media.ic_play') : $r('app.media.ic_pause'))
          ...
          .displayPriority(PlayerConstants.DISPLAY_PRIORITY_THREE)
        Image($r('app.media.ic_next'))
          ...
          .displayPriority(PlayerConstants.DISPLAY_PRIORITY_TWO)
        Image($r('app.media.ic_music_list'))
          ...
          .displayPriority(PlayerConstants.DISPLAY_PRIORITY_ONE)
      }
      .width(new BreakpointType({
        sm: $r('app.float.play_width_sm'),
        md: $r('app.float.play_width_sm'),
        lg: $r('app.float.play_width_lg')
      }).getValue(this.currentBreakpoint))
      .justifyContent(FlexAlign.End)
    }
    ...
    .position({
      x: 0,
      y: StyleConstants.FULL_HEIGHT
    })
    .translate({
      x: 0,
      y: StyleConstants.TRANSLATE_PLAYER_Y
    })
  }
}


4.2 音乐播放页
在音乐播放页中,音乐播放页面由歌曲页面和歌词页面两部分组成。
 
在sm断点下通过Swiper组件切换播控区域以及歌词区域,在md以及lg断点下播控区域以及歌词区域左右两列展示,在md断点下GridRow设置总栅格数为8,播控区域以及歌词区域分别占4个栅格,在lg断点下GridRow设置总栅格数为12,播控区域占4个栅格,GridCol设置offset为1,歌词区域占6个栅格,设置offset为1。
// features/musicList/src/main/ets/components/MusicControlComponent.ets
@Component
struct MusicControlComponent {
  ...

  build() {
    ...
      Row() {
        if (this.isFoldFull) {
          Column() {
            ...
            GridRow({
              columns: { md: BreakpointConstants.COLUMN_MD },
              gutter: BreakpointConstants.GUTTER_MUSIC_X
            }) {
              GridCol({
                span: { md: BreakpointConstants.SPAN_SM }
              }) {
                MusicInfoComponent()
              }
              .margin({
                left: $r('app.float.margin_small'),
                right: $r('app.float.margin_small')
              })
              GridCol({
                span: { md: BreakpointConstants.SPAN_SM }
              }) {
                LyricsComponent({ isShowControl: this.isShowControlLg, isTablet: this.isTabletFalse })
              }
              .padding({
                left: $r('app.float.twenty_four')
              })
            }
            ...
          }
          ...
        } else if (this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG) {
          Column() {
            ...
            GridRow({
              columns: { md: BreakpointConstants.COLUMN_MD, lg: BreakpointConstants.COLUMN_LG },
              gutter: BreakpointConstants.GUTTER_MUSIC_X
            }) {
              GridCol({
                span: { md: BreakpointConstants.SPAN_SM, lg: BreakpointConstants.SPAN_SM },
                offset: { lg: BreakpointConstants.OFFSET_MD }
              }) {
                Column() {
                  Image(this.songList[this.selectIndex].label)
                    .width(StyleConstants.FULL_WIDTH)
                    .aspectRatio(1)
                    .borderRadius($r('app.float.cover_radius'))
                  ControlAreaComponent()
                }
                .height(StyleConstants.FULL_HEIGHT)
                .justifyContent(FlexAlign.SpaceBetween)
                .margin({
                  bottom: $r('app.float.common_margin')
                })
              }
              GridCol({
                span: { md: BreakpointConstants.SPAN_SM, lg: BreakpointConstants.SPAN_MD },
                offset: { lg: BreakpointConstants.OFFSET_MD }
              }) {
                LyricsComponent({ isShowControl: this.isShowControlLg, isTablet: this.isTablet })
              }
            }
            ...
          }
        } else {
          Stack({ alignContent: Alignment.TopStart }) {
            Swiper() {
              MusicInfoComponent()
                .margin({
                  top: $r('app.float.music_component_top'),
                  bottom: $r('app.float.music_component_bottom')
                })
                .padding({
                  left: $r('app.float.common_padding'),
                  right: $r('app.float.common_padding')
                })
              LyricsComponent({ isShowControl: this.isShowControl, isTablet: this.isTabletFalse })
                .margin({
                  top: $r('app.float.margin_lyric')
                })
                .padding({
                  left: $r('app.float.common_padding'),
                  right: $r('app.float.common_padding')
                })
            }
            ...
          }
          .height(StyleConstants.FULL_HEIGHT)
        }
      }
      .padding({
        bottom: this.bottomArea,
        top: this.topArea
      })
    ...
  }
  ...
}
4.3 音乐评论页
在音乐评论页中,音乐评论页由标题、歌曲信息、精彩评论和最新评论四部分组成。
 
标题栏在不同的设备上面自适应显示“返回按钮”和“评论”。歌曲信息在不同设备的显示内容一样,中间空白区域通过Blank组件填充,可实现自适应拉伸能力。精彩评论列表和最新评论列表对于不同屏幕尺寸的设备,评论列表的样式基本一致,但sm和md断点下的精彩评论列表和最新评论列表是单列显示,lg断点下的精彩评论列表和最新评论列表是双列显示,可以通过List组件的lanes属性实现这一效果。
// features/musicComment/src/main/ets/view/MusicCommentPage.ets
@Entry
@Component
struct MusicCommentPage {
  ...

  @Builder ShowTitle(title: ResourceStr) {
    Row() {
      Text(title)
        ...
    }
    ...
  }

  build() {
    GridRow({
      breakpoints: {
        value: BreakpointConstants.BREAKPOINT_VALUE,
        reference: BreakpointsReference.WindowSize
      },
      columns: {
        sm: BreakpointConstants.COLUMN_SM,
        md: BreakpointConstants.COLUMN_MD,
        lg: BreakpointConstants.COLUMN_LG
      },
      gutter: { x: BreakpointConstants.GUTTER_X }
    }) {
      GridCol({
        span: {
          sm: BreakpointConstants.COLUMN_SM,
          md: BreakpointConstants.COLUMN_MD,
          lg: BreakpointConstants.COLUMN_LG
        }
      }) {
        Column() {
          HeadComponent()
            .margin({
              left: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
                $r('app.float.margin_left_sm') : $r('app.float.margin_left'),
              right: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
                $r('app.float.margin_right_sm') : $r('app.float.margin_right')
            })

          MusicInfoComponent()
            .margin({
              left: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
                $r('app.float.margin_left_sm') : $r('app.float.margin_left'),
              right: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
                $r('app.float.margin_right_sm') : $r('app.float.margin_right')
            })

          this.ShowTitle($r('app.string.wonderful_comment'))

          List() {
            ...
          }
          .lanes(this.currentBp === BreakpointConstants.BREAKPOINT_LG ? 2 : 1)
          .scrollBar(BarState.Off)
          .divider({
            color: $r('app.color.list_divider'),
            strokeWidth: $r('app.float.stroke_width'),
            startMargin: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
              $r('app.float.start_margin') : $r('app.float.start_margin_lg'),
            endMargin: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
              0 : $r('app.float.divider_margin_left')
          })
          .margin({
            left: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
              $r('app.float.margin_left_sm') : $r('app.float.margin_left_list'),
            right: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
              $r('app.float.margin_right_sm') : $r('app.float.margin_right_list')
          })

          this.ShowTitle($r('app.string.new_comment'))

          List() {
            ...
          }
          .layoutWeight(1)
          .lanes(this.currentBp === BreakpointConstants.BREAKPOINT_LG ? 2 : 1)
          .scrollBar(BarState.Off)
          .margin({
            left: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
              $r('app.float.margin_left_sm') : $r('app.float.margin_left_list'),
            right: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
              $r('app.float.margin_right_sm') : $r('app.float.margin_right_list')
          })
          .divider({
            color: $r('app.color.list_divider'),
            strokeWidth: $r('app.float.stroke_width'),
            startMargin: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
              $r('app.float.start_margin') : $r('app.float.start_margin_lg'),
            endMargin: this.currentBp === BreakpointConstants.BREAKPOINT_SM ?
              0 : $r('app.float.divider_margin_left')
          })
        }
        .height(StyleConstants.FULL_HEIGHT)
      }
    }
    ...
  }
}

5 歌单音乐
歌单中的音乐数据来自SongItem数组,id是歌曲的排序序号,title是歌曲名称,singer是演唱歌手,label是歌曲专辑背景图,src既引入的歌曲文件,在歌单中添加或删除歌曲在SongItem数组添加一项或减去一项,添加歌曲时要配置好歌曲的各个属性,既前面描述的
信息


features\musicList\src\main\ets\viewmodel\SongListData.etsconst songList: SongItem[] = [
  { id: 1, title: 'Dream It Possible', singer: 'Delacey', mark: $r('app.media.ic_vip'),
    label: $r('app.media.ic_dream'), src: 'Delacey - Dream It Possible.flac', index:0,
    lyric: 'lrcfiles/DreamItPossible.lrc' },
  { id: 2, title: '不知道', singer: '张三-你好我好都好', mark: $r('app.media.ic_sq'),
    label: $r('app.media.ic_avatar2'), src: 'world.wav', index:1, lyric: '' },
  { id: 3, title: '还是歌名', singer: '不知道你是谁', mark: $r('app.media.ic_vip'),
    label: $r('app.media.ic_avatar16'), src: 'power.wav', index:2, lyric: '' },

   { id: 35, title: '晚风', singer: 'kt', mark: $r('app.media.ic_sq'),
    label: $r('app.media.ic_avatar11'), src: 'Copy.mp3', index:34, lyric: '' },
  { id: 36, title: '心做', singer: 'kt', mark: $r('app.media.ic_sq'),
    label: $r('app.media.ic_avatar11'), src: 'xinzuo.mp3', index:35, lyric: '' },
  { id: 37, title: 'why would I ever', singer: 'kt', mark: $r('app.media.ic_sq'),
    label: $r('app.media.ic_avatar11'), src: 'why would I ever.mp3', index:36, lyric: '' }
]

const optionList : OptionItem[] = [
  { image: $r('app.media.ic_collect'), text: $r('app.string.collect') },
  { image: $r('app.media.ic_download'), text: $r('app.string.download') },
  { image: $r('app.media.ic_comments'), text: $r('app.string.comment'), action: (pageIndexInfos: NavPathStack) => {
    pageIndexInfos.pushPathByName(RouterUrlConstants.MUSIC_COMMENT, null);
  }},
  { image: $r('app.media.ic_share'), text: $r('app.string.share') }
]

class OptionItem {
  image: Resource = $r('app.media.ic_collect');
  text?: Resource;
  action?: (pageIndexInfos: NavPathStack) => void;
}

export { optionList, OptionItem, songList }
 借鉴文章链接:https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_NEXT-MusicHome


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值