鸿蒙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、运行效果

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邹荣乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值