鸿蒙HarmonyOS开发:一次开发,多端部署(界面级)音乐专辑案例

一、概述

随着终端设备形态日益多样化,分布式技术逐渐打破单一硬件边界,一个应用或服务,可以在不同的硬件设备之间随意调用、互助共享,让用户享受无缝的全场景体验。而作为应用开发者,广泛的设备类型也能为应用带来广大的潜在用户群体。但是如果一个应用需要在多个设备上提供同样的内容,则需要适配不同的屏幕尺寸和硬件,开发成本较高。HarmonyOS 系统面向多终端提供了“一次开发,多端部署”(后文中简称为“一多”)的能力,让开发者可以基于一种设计,高效构建多端可运行的应用。

在这里插入图片描述

1、定义和目标

定义:一套代码工程,一次开发上架,多端按需部署。

目标:支撑开发者快速高效的开发支持多种终端设备形态的应用,实现对不同设备兼容的同时,提供跨设备的流转、迁移和协同的分布式体验。

2、关键问题

为了实现“一多”的目标,需要解决如下三个基础问题:

  • 问题1:页面如何适配

不同设备间的屏幕尺寸、色彩风格等存在差异,页面如何适配。

  • 问题2:功能如何兼容

不同设备的系统能力有差异,如智能穿戴设备是否具备定位能力、智慧屏是否具备摄像头等,功能如何兼容。

  • 问题3:工程如何组织

如何实现一套代码同时能部署到多种不同设备上,代码工程如何组织。

3、关键问题的解决思路

针对“一多”提出的三个基础问题,可以从界面级、功能级、工程级三个维度给出相关问题的解决思路:

在这里插入图片描述

二、界面级一多

页面级一多需要考虑不同设备间的屏幕尺寸、色彩风格等存在差异,页面如何适配。可以从布局能力、资源使用、交互归一几个方面去考虑。

1、布局能力

布局决定了页面中的元素按照何种方式排布及显示,是页面设计及开发过程中首先需要考虑的问题。一般情况下,可以通过页面(或自定义组件)内的组件结构(组件个数、组件的父子/兄弟关系、组件类型、组件的相对位置)来判断使用何种布局能力。

对于随尺寸变化组件结构相同的场景,可以在开发过程中灵活使用自适应布局能力来达到目标效果。
对于随尺寸变化组件结构不同的场景,更适合使用响应式布局能力来实现不同尺寸下的不同显示的效果。
布局可以分为自适应布局和响应式布局,二者的介绍如下表所示:

表1

名称 简介
自适应布局 当外部容器大小发生变化时,元素可以根据相对关系自动变化以适应外部容器变化的布局能力。相对关系如占比、固定宽高比、显示优先级等。当前自适应布局能力有7种:拉伸能力、均分能力、占比能力、缩放能力、延伸能力、隐藏能力、折行能力。自适应布局能力可以实现界面显示随外部容器大小连续变化。
响应式布局 当外部容器大小发生变化时,元素可以根据断点、栅格或特定的特征(如屏幕方向、窗口宽高等)自动变化以适应外部容器变化的布局能力。当前响应式布局能力有3种:断点、媒体查询、栅格布局。响应式布局可以实现界面随外部容器大小有级不连续变化,通常不同特征下的界面显示会有较大的差异。
1.1、自适应布局

针对常见的开发场景,方舟开发框架提炼了七种自适应布局能力,这些布局可以独立使用,也可多种布局叠加使用。

表2

自适应布局能力 使用场景 实现方式
拉伸能力 容器组件尺寸发生变化时,增加或减小的空间全部分配给容器组件内指定区域。 Flex布局的flexGrow和flexShrink属性
均分能力 容器组件尺寸发生变化时,增加或减小的空间均匀分配给容器组件内所有空白区域。 Row组件、Column组件或Flex组件的justifyContent属性设置为FlexAlign.SpaceEvenly
占比能力 子组件的宽或高按照预设的比例,随容器组件发生变化。 基于通用属性的两种实现方式:
将子组件的宽高设置为父组件宽高的百分比
layoutWeight属性
缩放能力 子组件的宽高按照预设的比例,随容器组件发生变化,且变化过程中子组件的宽高比不变。 布局约束的aspectRatio属性
延伸能力 容器组件内的子组件,按照其在列表中的先后顺序,随容器组件尺寸变化显示或隐藏。 基于容器组件的两种实现方式:
通过List组件实现
通过Scroll组件配合Row组件或Column组件实现
隐藏能力 容器组件内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏。相同显示优先级的子组件同时显示或隐藏。 布局约束的displayPriority属性
折行能力 容器组件尺寸发生变化时,如果布局方向尺寸不足以显示完整内容,自动换行。 Flex组件的wrap属性设置为FlexWrap.Wrap
1.2、响应式布局

响应式布局是指页面内的元素可以根据特定的特征(如窗口宽度、屏幕方向等)自动变化以适应外部容器变化的布局能力。响应式布局中最常使用的特征是窗口宽度,可以将窗口宽度划分为不同的范围(下文中称为断点)。当窗口宽度从一个断点变化到另一个断点时,改变页面布局(如将页面内容从单列排布调整为双列排布甚至三列排布等)以获得更好的显示效果。

表3

响应式布局能力 简介
断点 将窗口宽度划分为不同的范围(即断点),监听窗口尺寸变化,当断点改变时同步调整页面布局。
媒体查询 媒体查询支持监听窗口宽度、横竖屏、深浅色、设备类型等多种媒体特征,当媒体特征发生改变时同步调整页面布局。
栅格布局 栅格组件将其所在的区域划分为有规律的多列,通过调整不同断点下的栅格组件的参数以及其子组件占据的列数等,实现不同的布局效果。

三、音乐专辑案例

1、UI设计

在这里插入图片描述

2、思路分析

在这里插入图片描述

3、详细实现代码
import {
    
     BreakpointSystem } from '../utils/BreakpointSystem'

@Entry
@Component
struct GridRowSample1 {
    
    
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm'
  private breakpointSystem: BreakpointSystem = new BreakpointSystem();

  aboutToAppear() {
    
    
    // 注册监听
    this.breakpointSystem.register()
  }

  aboutToDisappear() {
    
    
    // 移除监听
    this.breakpointSystem.unregister()
  }

  build() {
    
    

    Column() {
    
    

      // 标题栏(返回键+菜单)
      HeaderComponent()

      // 中部
      Column() {
    
    

        GridRow() {
    
    

          GridCol({
     
      span: {
     
      sm: 12, md: 6, lg: 4 } }) {
    
    

            Column() {
    
    
              GridRow({
     
      columns: {
     
      sm: 12, md: 12, lg: 12 } }) {
    
    

                GridCol({
     
     
                  span: {
     
      sm: 4, md: 10, lg: 10 },
                  offset: {
     
      sm: 0, md: 1, lg: 1 }
                }) {
    
    
                  // 专辑图片
                  AlbumImageComponent()
                }


                GridCol({
     
     
                  span: {
     
      sm: 8, md: 10, lg: 10 },
                  offset: {
     
      sm: 0, md: 2, lg: 2 }
                }) {
    
    
                  // 专辑名称+简介 组件
                  AlbumComponent()
                }


                GridCol({
     
     
                  span: {
     
      sm: 10, md: 10, lg: 10 },
                  offset: {
     
      sm: 1, md: 2, lg: 2 }
                }) {
    
    
                  // 收藏/下载/评论/分享 组件
                  HandleMenuComponent()
                }
              }
            }
            .width('100%')
            .padding(10)
          }

          GridCol({
     
      span: {
     
      sm: 12, md: 6, lg: 8 } }) {
    
    

            Column({
     
      space: 4 }) {
    
    
              // 播放全部组件
              PlayAllComponent()

              // 歌曲列表组件
              MusicListComponent()

            }.backgroundColor(Color.White)
            .padding(2)
            .borderRadius({
    
     topLeft: 12, topRight: 12 })

          }.backgroundColor("#F4F9FC")
        }

      }.layoutWeight(1)
      .width('100%')
      .backgroundColor("#E4ECF7")

      // 底部播放栏组件
      BottomComponent()

    }.width('100%')
    .height('100%')

  }
}

// 标题栏组件
@Component
struct HeaderComponent {
    
    
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm'
  build() {
    
    
    Row() {
    
    
      GridRow() {
    
    
        GridCol({
     
     
          span: {
     
      sm: 12, md: 6, lg: 4 }
        }) {
    
    
          Row({
     
      space: 8 }) {
    
    
            Image($r('app.media.back'))
              .width(20)
              .height(20)
              .aspectRatio(1)
              .fillColor("#6E7F91")
            Text('歌单')
              .fontSize(18)
              .fontColor("#6E7F91")
              .fontWeight(FontWeight.Bold)
            Blank()
            if (this.currentBreakpoint == 'sm') {
    
    
              Image($r('app.media.fullScreen'))
                .width(20)
                .height(20)
                .aspectRatio(1)
                .fillColor("#6E7F91")
            }
          }
          .width('100%')
          .height(44)
          .padding(10)
          .backgroundColor("#E4ECF7")
        }

        GridCol({
     
     
          span: {
     
      sm: 0, md: 6, lg: 8 }
        }) {
    
    
          Row() {
    
    
            Image($r('app.media.fullScreen'))
              .width(20)
              .height(20)
              .aspectRatio(1)
              .fillColor("#6E7F91")
              .margin({
    
     right: 10 })
          }.justifyContent(FlexAlign.End)
          .width('100%')
          .height(44)
          .backgroundColor("#F4F9FC")
        }
      }
    }
  }
}

// 播放栏组件
@Component
struct BottomComponent {
    
    
  build() {
    
    
    Row() {
    
    
      // 当前播放
      Row({
     
      space: 6 }) {
    
    
        Image($r('app.media.1'))
          .width(28)
          .height(28)
          .borderRadius(4)
          .margin({
    
     left: 6 })
        Column() {
    
    
          Text('不知道')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor("#444444")
          Row({
     
      space: 4 }) {
    
    
            Text('vip')
              .fontSize(10)
              .fontColor('#ED6784')
              .border({
    
     width: 1, color: '#ED6784', radius: 4, style: BorderStyle.Solid })
              .padding({
    
     left: 2, right: 2 })
            Text('小碗你好')
              .fontSize(12)
              .fontColor("#BBBBBB")
          }.width('100%')
          .justifyContent(FlexAlign.Start)
        }.layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
      }.padding(4)
      .layoutWeight(3)

      // 收藏/播放/上、下一首
      Row({
     
      space: 4 }) {
    
    
        Image($r('app.media.collection'))
          .width('100%')
          .height('100%')
          .displayPriority(1)
          .width(34)
          .height(34)
          .margin(4)
        Image($r('app.media.previous'))
          .width('100%')
          .height('100%')
          .displayPriority(2)
          .width(32)
          .height(32)
          .margin(4)
        Image($r('app.media.play'))
          .width('100%')
          .height('100%')
          .displayPriority(3)
          .width(32)
          .height(32)
          .margin(4)
        Image($r('app.media.next'))
          .width('100%')
          .height('100%')
          .displayPriority(2)
          .width(28)
          .height(28)
          .margin(4)
        Image($r('app.media.musiclist'))
          .width('100%')
          .height('100%')
          .displayPriority(1)
          .width(32)
          .height(32)
          .margin(4)
      }.justifyContent(FlexAlign.SpaceEvenly)
      .layoutWeight(1)
    }.width('100%')
    .height(44)
    .backgroundColor("#F4F9FC")
  }
}

// 歌曲列表组件
@Component
struct MusicListComponent {
    
    
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm'
  @State listItem: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]

  build() {
    
    
    List() {
    
    
      ForEach(this.listItem, (item) => {
    
    
        ListItem() {
    
    
          Row() {
    
    
            Column({
     
      space: 6 }) {
    
    
              Text(`歌曲${
      
      item}`)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor("#444444")
              Row({
     
      space: 4 }) {
    
    
                Text('vip')
                  .fontSize(10)
                  .fontColor('#ED6784')
                  .border({
    
     width: 1, color: '#ED6784', radius: 4, style: BorderStyle.Solid })
                  .padding({
    
     left: 2, right: 2 })
                Text('小碗你好')
                  .fontSize(12)
                  .fontColor('#BBBBBB')
              }.width('100%')
              .justifyContent(FlexAlign.Start)
            }.layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Image($r('app.media.more')).width(20)
          }.padding(8)
        }
      })
    }.lanes(this.currentBreakpoint == 'lg' ? 2 : 1)
    .divider({
    
     strokeWidth: 1, color: "#efefef" })
  }
}

// 播放全部
@Component
struct PlayAllComponent {
    
    
  build() {
    
    
    Row({
     
      space: 6 }) {
    
    
      Image($r('app.media.play2'))
        .width(20)
        .height(20)
      Text('播放全部(114)')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
      Blank()
      Image($r('app.media.order'))
        .width(22)
        .height(18)
      Image($r('app.media.list'))
        .width(20)
        .height(22)
    }.width('100%')
    .padding(6)
  }
}

// 专辑信息
@Component
struct AlbumComponent {
    
    
  build() {
    
    
    Column({
     
      space: 8 }) {
    
    
      Text('独立民谣')
        .fontSize(16)
        .fontColor("#6E7F91")
        .fontWeight(FontWeight.Bold)
      Text('歌单选取了一些比较受关注的民谣歌曲。')
        .fontSize(14)
        .fontColor("#6E7F91")
    }.alignItems(HorizontalAlign.Start)
    .padding(10)
    .width('100%')
    .margin({
    
     top: 20 })
  }
}

// 专辑图片
@Component
struct AlbumImageComponent {
    
    
  build() {
    
    
    Image($r('app.media.1'))
      .width('100%')
      .objectFit(ImageFit.Cover)
      .borderRadius(8)
      .aspectRatio(1)
  }
}

// 收藏/下载/评论/分享
@Component
struct HandleMenuComponent {
    
    
  build() {
    
    
    Row() {
    
    
      Column({
     
      space: 4 }) {
    
    
        Image($r('app.media.collect2'))
          .width(26)
          .height(26)
          .fillColor("#6E7F91")
        Text('999+')
          .fontSize(13)
          .fontColor("#6E7F91")
      }

      Column({
     
      space: 4 }) {
    
    
        Image($r('app.media.down'))
          .width(26)
          .height(26)
          .fillColor("#6E7F91")
        Text('下载')
          .fontSize(13)
          .fontColor("#6E7F91")
      }

      Column({
     
      space: 4 }) {
    
    
        Image($r('app.media.comment'))
          .width(26)
          .height(26)
          .fillColor("#6E7F91")
        Text('评论')
          .fontSize(13)
          .fontColor("#6E7F91")
      }

      Column({
     
      space: 4 }) {
    
    
        Image($r('app.media.share2'))
          .width(26)
          .height(26)
          .fillColor("#6E7F91")
        Text('分享')
          .fontSize(13)
          .fontColor("#6E7F91")
      }
    }.width('100%')
    .padding(10)
    .justifyContent(FlexAlign.SpaceAround)
    .margin({
    
     top: 10 })
  }
}



4、运行效果

在这里插入图片描述

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/shanghai597/article/details/142101795