前言
一、一多开发是什么意思?
一多开发是鸿蒙系统未来的趋势,
①页面适配问题 不同设备,屏幕尺寸、色彩风格,都存在差异
②功能兼容问题 不同设备,系统能力差异 eg.手机摄像头,手表测心率…
③工程如何组织 一个应用,实现一套代码工程,将来同时部署到多个设备
二、为了解决什么问题?
页面适配问题:界面级一多(重点掌握)
功能兼容问题:功能级一多(了解)
工程如何组织:工程级一多(重点掌握)
三、我们要掌握哪些知识、能力?
上面的3个问题,我们需要重点掌握页面适配问题和工程如何组织的问题,下面讲一页面适配问题的解决路径:界面级一多能力。
界面级一多能力有 2 类:
1.自适应布局: 略微调整界面结构
2.响应式布局:比较大的界面调整
用白话说,就是在格局较小变化的设备中,使用自适应布局;而在多终端切换的情况下使用响应式布局。
其中,自适应布局能力有以下几种:
布局能力有:拉伸能力、均分能力、占比能力、缩放能力、延伸能力、隐藏能力、折行能力。
而响应式布局会根据设备显示器的大小,预先设置不同的取值范围对应不同的断点,适用于不同类型的设备。设计思路举例如下:
四、自适应布局的七种布局能力
(一)拉伸能力
正常未拉伸状态:
极限压缩状态:
极限拉伸状态:
同时使用拉伸和放大能力:
上述案例中使用的就是拉伸能力的自适应的布局方式: 实际上,拉伸能力包含压缩和拉伸两种能力
在拉动下方的Slider滑动条时,我们会发现,左、中、右的大小都会随之变化。 那么变化的规律是什么?是怎么使其变化的呢?
首先,我们要知道这个案例的关键参数。 ① containerWidth =
600,这个是绑定的默认宽度,即左、中、右没有压缩也没有拉伸时的正常宽度。
②使用Builder建立的滑动条,设置了最小值和最大值的参数,分别是400和1000,这在后面会用来条件拉伸的比率。
其次,我们要知道这个案例使用了什么调试方法。
调试时,使用一个大的盒子,设定宽度width(this.containerWidth),其后的参数为@State标签修饰的变量,这个变量即Slilder滑动条组件的value值,使用双向绑定实时同步数值,预设值为600。在这个盒子中装入需要压缩、拉伸的小盒子。
在拉伸能力中用到的是flexShrink(压缩)和flexGrow(拉伸)两个属性,其后带的参数表示了压缩和拉伸的比例。(实践表明,调节其后的参数对实际的拉伸比例影响不大)
第三,极限压缩状态:中间的绿色方块,设定为宽300,高400,在横屏显示时可见其最小的状态是在压缩至极限时,此时的状态即为其所在的Row容器的设定大小(宽300,高400)。而两边的土灰色盒子则被压缩至大约原大小的三分之一左右。
第四,极限拉伸状态:两边的土灰色盒子的大小如其预先设定所在的Row容器大小一致(宽150,高400),而中间的绿色方块则被拉伸至最大,达到宽度约1000的大小。
小结:在上述操作中: (1)压缩和放大的比例不好准确推断,最好需要手动调试确认。
(2)但有一些可以提前确认的参数标准,即最大和最小的形状大小范围。 (3)拉伸能力所带的参数作用方式有待研究,但有时改变参数似乎影响不大。
(4)拉伸方法造成的图形大小会超出边框范围。
(5)收缩时,拉伸方法所在图形的大小不会小于原设定大小,放大时,收缩方法所在图形的大小不会大于原设定大小。(同一组件只使用压缩、放大一种能力)
(6)同一组件可以同时使用压缩、放大功能。
案例源码:
@Entry
@Component
struct Demo01 {
// 绑定的宽度-默认 600
@State containerWidth: number = 600
// 底部滑块,可以通过拖拽滑块改变容器尺寸。
@Builder
sliderBuilder() {
Slider({
value: $$this.containerWidth, // 绑定的值
min: 400, // 最小值
max: 1000, // 最大值
style: SliderStyle.OutSet // 滑块在滑轨上
})
.blockColor(Color.White)
.width('60%')
.position({ x: '20%', y: '80%' })
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
// 标记现在的宽度
Text('宽度:' + this.containerWidth)
.zIndex(2)
.translate({ x: 20, y: 20 })
.fontColor(Color.Orange)
// 核心区域
Column() {
Column() {
Row() {
// 布局能l力 1:拉伸能力:
// 容器组件尺寸发生改变时,将变化的部分分配给容器内的【指定区域】
//
// 涉及属性:
// flexShrink:压缩比例,默认值:Column,Row 时(0),Flex 时(1)
// flexGrow:拉伸比例,默认值 0
// 需求:
// 1. 空间不足时:分配给左右,1:1
// 2. 空间富余时:分配给中间
// 左
Row() {
Text('左')
.fontSize(20)
.fontColor(Color.White)
}
.justifyContent(FlexAlign.Center)
.width(150)
.height(400)
.backgroundColor('#c2baa6')
.flexShrink(1)
// 中
Row() {
Text('中')
.fontSize(30)
.fontColor(Color.White)
}
.width(300)
.height(400)
.backgroundColor('#68a67d')
.justifyContent(FlexAlign.Center)
.flexGrow(1)
//.flexShrink(1) //可同时使用压缩、拉伸能力
// 右
Row() {
Text('右')
.fontSize(20)
.fontColor(Color.White)
}
.justifyContent(FlexAlign.Center)
.width(150)
.height(400)
.backgroundColor('#c2baa6')
.flexShrink(1)
}
.width(this.containerWidth)
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.border({ width: 2, color: Color.Orange })
.backgroundColor(Color.Black)
}
// 底部滑块
this.sliderBuilder()
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
}
应用场景联想: 拉伸能力的应用场景可放在:
(1)在线绘图工具:初始设定一定的图形大小,之后可在原先图形的基础上拉动压缩、拉伸其大小,可以对单一图形进行压缩拉伸,也可以对组合的图形统一进行压缩、拉伸
(2)可以用在图片处理工具中:用于图片大小的比例调节、图片的缩放等
(二)均分能力
指容器组件尺寸发生变化时,增加或减小的空间均匀分配给容器组件内所有【空白区域】。 常用于内容数量固定、均分显示的场景,比如工具栏、底部菜单栏、导航栏等。
如上图所示,这是一个由四个宽度分别为80的小组件组成的四个菜单按钮,总宽度320。
我们使用Row、Column、Flex等组件的 justifyContent (内容调整)属性进行自适应均分对齐
只需要将 justifyContent 后带的参数设置为FlexAlign.SpaceEvenly即可。
知识补充:
justifyContent()属于线性布局中典型的主轴对齐方式,其与FlexAlign枚举类型参的配合,有以下典型用法,均分布局是其中一种:
案例源码:
export interface NavItem {
id: number
icon: ResourceStr
title: string
}
@Entry
@Component
struct Demo02 {
readonly list: NavItem [] = [
{ id: 1, icon: $r('app.media.ic_nav_01'), title: '淘金币' },
{ id: 2, icon: $r('app.media.ic_nav_02'), title: '摇现金' },
{ id: 3, icon: $r('app.media.ic_nav_03'), title: '闲鱼' },
{ id: 4, icon: $r('app.media.ic_nav_04'), title: '中通快递' },
]
@State rate: number = 600
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder
sliderBuilder() {
Slider({
value: $$this.rate,
min: 200,
max: 600,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.width('60%')
.position({ x: '20%', y: '80%' })
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
// 标记现在的宽度
Text('宽度:' + this.rate.toFixed(0))
.zIndex(2)
.translate({ x: 20, y: 20 })
.fontColor(Color.Orange)
Column() {
Column() {
// 布局能力 2:均分能力
// 指容器组件尺寸发生变化时,增加或减小的空间均匀分配给容器组件内所有【空白区域】。
// 常用于内容数量固定、均分显示的场景,比如工具栏、底部菜单栏、导航栏等
// 涉及属性:
// Row、Column、Flex 组件的 justifyContent 属性
// justifyContent设置为 FlexAlign.SpaceEvenly即可
Row() {
ForEach(this.list, (item: NavItem) => {
Column() {
Image(item.icon)
.width(48)
.height(48)
.margin({ top: 8 })
Text(item.title)
.width(64)
.height(30)
.lineHeight(15)
.fontSize(12)
.textAlign(TextAlign.Center)
.margin({ top: 8 })
.padding({ bottom: 15 })
}
.width(80)
.height(102)
.backgroundColor('#8FBF9F')
.borderRadius(10)
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly) // 均分
}
.width(this.rate) // 绑定滑块改变的尺寸
.padding({ top: 16 })
.backgroundColor('#FFFFFF')
.borderRadius(16)
this.sliderBuilder()
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
}
小结: 1、均分能力自适应的使用要件:
(1)线性布局的均分布局方式
(2)有大小会变化的外部容器
2、使用场景:常用于内容数量固定、均分显示的场景,比如工具栏、底部菜单栏、导航栏等
(三)占比能力
占比能力的使用很简单,主要是使用layoutWeight来调整需要自适应的组件的相对于上一级容器大小的占比参数,本例中,左、中、右三个播放按钮的占比比例均设为5,实质就是1:1:1,三个大小相同。这种情况下,仅需要设置layoutWeight比例,不需要设置实际的宽度值。
案例源码:
@Entry
@Component
struct Demo03 {
@State rate: number = 200
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder
slider() {
Slider({
value: $$this.rate,
min: 200,
max: 500,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.width('60%')
.height(50)
.position({ x: '20%', y: '80%' })
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
// 显示目前容器的宽度
Text('宽度:' + this.rate.toFixed(0))
.zIndex(2)
.translate({ x: 20, y: 20 })
.fontColor(Color.Orange)
Column() {
// 布局能力 3:占比能力
// 子组件的宽高按照预设的比例,随父容器组件发生变化
// 实现方式:
// 1. 子组件的【宽高】设置为父组件宽高的【百分比】
// 2. 通过 layoutWeight 属性设置主轴方向【布局权重】(比例)
// 容器 主轴横向
Row() {
// 上一首
Column() {
Image($r("app.media.ic_public_play_last"))
.width(50)
.height(50)
.border({ width: 2 })
.borderRadius(30)
.padding(10)
}
.height(96)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.layoutWeight(5) // 设置子组件在父容器主轴方向的布局权重
// 播放&暂停
Column() {
Image($r("app.media.ic_public_pause"))
.width(50)
.height(50)
.border({ width: 2 })
.borderRadius(30)
.padding(10)
}
.height(96)
.backgroundColor('#66F1CCB8')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.layoutWeight(5) // 设置子组件在父容器主轴方向的布局权重
// 下一首
Column() {
Image($r("app.media.ic_public_play_next"))
.width(50)
.height(50)
.border({ width: 2 })
.borderRadius(30)
.padding(10)
}
.height(96)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.layoutWeight(5) // 设置子组件在父容器主轴方向的布局权重
}
.width(this.rate) // 绑定宽度给 容器
.height(96)
.borderRadius(16)
.backgroundColor('#FFFFFF')
// 调整宽度的滑块
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
}
小结: 占比能力与均分能力有相近性,有时可以替代使用;
但均分能力更强强调间隙的大小相等。
(四)缩放能力
在缩放能力案例中
小猴子图片的大小是400400大小的PNG图片。
而模拟器提供的模拟手机的大小为10802340。
经过四中情况的分析可知,400*400像素的PNG图片,其400的大小并不是vp(virtualpixel虚拟像素)单位,也不是px(物理像素)单位,在图中,要使小猴子图片刚好占满屏幕,需要使用其外一层的容器,控制容器宽度大小为360vp时,正好占满屏幕没有图片边缘耗费。此时模拟器的宽度为1080,是360的三倍,正好满足vp和px的单位互换比例:3倍。
案例源码:
@Entry
@Component
struct Demo04 {
@State sliderWidth: number = 400
@State sliderHeight: number = 400
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder
slider() {
Slider({
value: $$this.sliderHeight,
min: 100,
max: 400,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.width('60%')
.height(50)
.position({ x: '20%', y: '80%' })
Slider({
value: $$this.sliderWidth,
min: 100,
max: 400,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.width('60%')
.height(50)
.position({ x: '20%', y: '87%' })
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
Text('宽度:' + this.sliderWidth.toFixed(0) + ' 高度:' + this.sliderHeight.toFixed(0))
.zIndex(2)
.translate({ x: 20, y: 20 })
.fontColor(Color.Orange)
Column() {
// 动态修改该容器的宽高
Column() {
Column() {
Image($r("app.media.avatar"))
.width('100%')
.height('100%')
}
// 布局能力 4:缩放能力
// 子组件的宽高按照预设的比例,随容器组件发生变化,且变化过程中子组件的【宽高比】不变。
// 实现方式:
// 给子组件设置 aspectRatio即可 设置的值是 宽度/高度
// .aspectRatio(1 / 4) // 固定 宽 高比 1等同于 1:1
// .aspectRatio(1 / 2) // 固定 宽 高比 1等同于 1:1
.aspectRatio(1) // 宽高比为1:1
.border({ width: 2, color: "#66F1CCB8" }) // 边框,仅用于展示效果
}
.backgroundColor("#FFFFFF")
.height(this.sliderHeight)
.width(this.sliderWidth)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor(Color.Blue)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
}
(五)延伸能力
在延伸能力中,我们可以使用两种组件进行操作,一是List组件,二是Scroll组件,这两中组件都可以实现长度大于外部容器的元素的折叠表示,其中被折叠部分的元素,通过左右(或上下)拉动,均可实现滚动显示。
第二张图展示了滚动显示的状态。
第三张图展示了外容器大小大于内部元素,此时延伸到最大,无需再进行延伸、无法再进行滑动。
案例源码:
import { NavItem } from './Demo02'
@Entry
@Component
struct Demo05 {
@State rate: number = 100
// 数组
readonly appList: NavItem [] = [
{ id: 1, icon: $r('app.media.ic_nav_01'), title: '淘金币' },
{ id: 2, icon: $r('app.media.ic_nav_02'), title: '摇现金' },
{ id: 3, icon: $r('app.media.ic_nav_03'), title: '闲鱼' },
{ id: 4, icon: $r('app.media.ic_nav_04'), title: '中通快递' },
{ id: 5, icon: $r('app.media.ic_nav_05'), title: '芭芭农场' },
{ id: 6, icon: $r('app.media.ic_nav_06'), title: '淘宝珍库' },
{ id: 7, icon: $r('app.media.ic_nav_07'), title: '阿里拍卖' },
{ id: 8, icon: $r('app.media.ic_nav_08'), title: '阿里药房' },
]
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder
slider() {
Slider({
value: $$this.rate,
min: 100,
max: 730,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.width('60%')
.height(50)
.position({ x: '20%', y: '80%' })
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
// 展示宽度
Text('宽度:' + this.rate.toFixed(0))
.zIndex(2)
.translate({ x: 20, y: 20 })
.fontColor(Color.Orange)
Column() {
Row({ space: 10 }) {
// 布局能力 5:延伸能力
// 容器组件内的子组件,按照其在列表中的先后顺序,随容器组件尺寸变化【显示或隐藏】
// 实现方式:
// 1.List 组件
// 2.Scroll 配合 Row 或者 Column
// 核心:调整父容器的尺寸,让页面中显示的组件数量发生改变
// 通过Scroll 组件实现隐藏能力
Scroll() {
Row({ space: 10 }) {
ForEach(this.appList, (item: NavItem, index: number) => {
Column() {
Image(item.icon)
.width(48)
.height(48)
.margin({ top: 8 })
Text(item.title)
.width(64)
.height(30)
.lineHeight(15)
.fontSize(12)
.textAlign(TextAlign.Center)
.margin({ top: 8 })
.padding({ bottom: 15 })
}
.width(80)
.height(102)
})
}
}
.scrollable(ScrollDirection.Horizontal) // 设置横向滚动
.padding({ top: 16, left: 10 })
.height(118)
.borderRadius(16)
.backgroundColor(Color.White)
}
.width(this.rate)
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
}
(六)隐藏能力
容器组件内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏 效果近似if else 条件渲染 适合管理多个组件 主要使用displayPriority属性,设置若干个优先级不同的单个组件
@Entry
@Component
struct Demo06 {
@State rate: number = 48
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder
slider() {
Slider({
value: $$this.rate,
min: 80,
max: 400,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.width('60%')
.height(50)
.position({ x: '20%', y: '80%' })
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
Text('宽度:' + this.rate.toFixed(0))
.zIndex(2)
.translate({ x: 20, y: 20 })
.fontColor(Color.Orange)
Column() {
// 布局能力 6:隐藏能力
// 容器组件内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏
// if else 条件渲染 适合管理多个组件{多个组件}
// displayPriority属性 适合设置若干个优先级不同的单个组件
// 实现方式:
// displayPriority属性:设置布局优先级来控制显隐
// 当主轴方向剩余尺寸不足以满足全部元素时,按照布局优先级,从[小到大]依次隐藏
Row({ space: 10 }) {
Image($r("app.media.ic_public_favor"))
.width(48)
.height(48)
.displayPriority(1) // 布局优先级
Image($r("app.media.ic_public_play_last"))
.width(48)
.height(48)
.displayPriority(2) // 布局优先级
Image($r("app.media.ic_public_pause"))
.width(48)
.height(48)
.displayPriority(3) // 布局优先级
Image($r("app.media.ic_public_play_next"))
.width(48)
.height(48)
.objectFit(ImageFit.Contain)
.displayPriority(2) // 布局优先级
Image($r("app.media.ic_public_view_list"))
.width(48)
.height(48)
.objectFit(ImageFit.Contain)
.displayPriority(1) // 布局优先级
}
.width(this.rate)
.height(96)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center)
.padding(10)
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor(Color.Red)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
}
(七)折行能力
容器组件尺寸发生变化,当布局方向尺寸不足以显示完整内容时自动换行。
使用Flex组件将 wrap参数 设置为FlexWrap.Wrap即可。
import { NavItem } from './Demo02'
@Entry
@Component
struct Demo07 {
@State rate: number = 0.7
readonly imageList: NavItem [] = [
{ id: 1, icon: $r('app.media.ic_nav_01'), title: '淘金币' },
{ id: 2, icon: $r('app.media.ic_nav_02'), title: '摇现金' },
{ id: 3, icon: $r('app.media.ic_nav_03'), title: '闲鱼' },
{ id: 4, icon: $r('app.media.ic_nav_04'), title: '中通快递' },
{ id: 5, icon: $r('app.media.ic_nav_05'), title: '芭芭农场' },
{ id: 6, icon: $r('app.media.ic_nav_06'), title: '淘宝珍库' },
]
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder
slider() {
Slider({
value: this.rate * 100,
min: 10,
max: 100,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.width('60%')
.position({ x: '20%', y: '87%' })
.onChange((value: number) => {
this.rate = value / 100
})
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
Text('宽度:' + (this.rate * 100).toFixed(0) + '%')
.zIndex(2)
.translate({ x: 20, y: 20 })
.fontColor(Color.Orange)
Flex({ justifyContent: FlexAlign.Center, direction: FlexDirection.Column }) {
Column() {
// 布局能力 7:折行能力
// 容器组件尺寸发生变化,当布局方向尺寸不足以显示完整内容时自动换行
// 实现方式:
// Flex组件将 wrp 设置为FlexWrap.Wrap即可
// 通过Flex组件warp参数实现自适应折行
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center,
wrap: FlexWrap.Wrap // 是否换行: FlexWrap.Wrap 开启换行
}) {
ForEach(this.imageList, (item: NavItem) => {
Column() {
Image(item.icon)
.width(80)
.height(80)
Text(item.title)
}
.margin(10)
})
}
.backgroundColor('#FFFFFF')
.padding(20)
.width(this.rate * 100 + '%')
.borderRadius(16)
}
.width('100%')
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor(Color.Red)
}
}
}
五、总结
以上即一多开发中常用的七种自适应能力的总结。七种自适应能力分别是:拉伸能力、均分能力
、占比能力、缩放能力、延伸能力、隐藏能力、折行能力。
写在最后
有很多小伙伴不知道该从哪里开始学习鸿蒙开发技术?也不知道鸿蒙开发的知识点重点掌握的又有哪些?自学时频繁踩坑,导致浪费大量时间。结果还是一知半解。所以有一份实用的鸿蒙(HarmonyOS NEXT)全栈开发资料用来跟着学习是非常有必要的。