文章目录
一、概述
随着终端设备形态日益多样化,分布式技术逐渐打破单一硬件边界,一个应用或服务,可以在不同的硬件设备之间随意调用、互助共享,让用户享受无缝的全场景体验。而作为应用开发者,广泛的设备类型也能为应用带来广大的潜在用户群体。但是如果一个应用需要在多个设备上提供同样的内容,则需要适配不同的屏幕尺寸和硬件,开发成本较高。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 })
}
}