核心:
- 一多开发
- 项目搭建
图片素材:
1. 应用开发准备
1.1. 注册成为开发者
在华为开发者联盟网站上,注册成为开发者,并完成实名认证,从而享受联盟开放的各类能力和服务,通过实名认证之后的管理中心如下图所示:
1.2. 在AGC平台创建项目和创建应用
1- 创建项目:名字根据情况自行调整
分析服务 【不开启】
2- 在项目中创建应用
企业中一般是通过企业域名确保应用的唯一性,比如:
com.itheima.hm_meikou
com.itheima.mk_shop
记住这个应用的名称,DevEco Studio 创建项目要用到。
2. 一多开发
“一次开发、多端部署”简称“一多”:一套代码工程,一次开发上架,多端按需部署。
为了实现这个目标,主要解决 3 个核心问题:
- 页面适配问题:界面级一多(重点掌握)
- 功能兼容问题:功能级一多(了解)
- 工程如何组织:工程级一多(重点掌握)
3. 界面级一多能力(掌握)
界面级一多能力有 2 类:
- 自适应布局: 略微调整界面结构
- 响应式布局:比较大的界面调整
3.1. 自适应布局
自适应布局的能力有 7 种,主要解决的是:窗口尺寸在【一定范围内】变化时,页面能够正常显示
自适应布局类别 | 自适应布局能力 | 使用场景 | 实现方式 |
自适应拉伸 | 容器组件尺寸发生变化时,增加或减小的空间全部分配给容器组件内指定区域。 | 的flexGrow和flexShrink属性 | |
容器组件尺寸发生变化时,增加或减小的空间均匀分配给容器组件内所有空白区域。 | 的justifyContent属性设置为FlexAlign.SpaceEvenly | ||
自适应缩放 | 子组件的宽或高按照预设的比例,随容器组件发生变化。 | 基于通用属性的两种实现方式: - 将子组件的宽高设置为父组件宽高的百分比 - layoutWeight属性 | |
子组件的宽高按照预设的比例,随容器组件发生变化,且变化过程中子组件的宽高比不变。 | 的aspectRatio属性 | ||
自适应延伸 | 容器组件内的子组件,按照其在列表中的先后顺序,随容器组件尺寸变化显示或隐藏。 | 基于容器组件的两种实现方式: - 通过List组件实现 | |
容器组件内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏。相同显示优先级的子组件同时显示或隐藏。 | 的displayPriority属性 | ||
自适应折行 | 容器组件尺寸发生变化时,如果布局方向尺寸不足以显示完整内容,自动换行。 | 的wrap属性设置为FlexWrap.Wrap |
3.1.1. 拉伸能力
拉伸能力指的是容器尺寸发生变化时:将变化的空间,分配给容器内的【指定区域】。利用的是 2 个属性:
属性名 | 类型 | 必填 | 说明 |
flexGrow | number | 是 | 设置父容器在主轴方向上的剩余空间分配给此属性所在组件的比例。 |
flexShrink | number | 是 | 设置父容器压缩尺寸分配给此属性所在组件的比例。 |
测试代码:
@Entry
@Component
struct Demo01 {
// 绑定的宽度-默认 600
@State containerWidth: number = 600
// 底部滑块,可以通过拖拽滑块改变容器尺寸。
@Builder
sliderBuilder() {
Slider({
value: this.containerWidth, // 绑定的值
min: 400, // 最小值
max: 1000, // 最大值
style: SliderStyle.OutSet // 滑块在滑轨上
})
.onChange((value: number) => {
this.containerWidth = value
})
.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)
// 右
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)
}
}
}
3.1.2. 均分能力
均分能力指的是容器尺寸发生变化时:将变化的空间,【均匀分配】给容器组件内【空白区域】。利用的是一个属性justifyContent,只能用在容器:Flex、Column、Row 上,将他设置为 SpaceEvenly即可
枚举名称 | 描述 |
Start | 元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。 |
Center | 元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。 |
End | 元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。 |
SpaceBetween | Flex主轴方向均匀分配弹性元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。 |
SpaceAround | Flex主轴方向均匀分配弹性元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。 |
SpaceEvenly | Flex主轴方向均匀分配弹性元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。 |
测试代码:
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
})
.onChange((value: number) => {
this.rate = value
})
.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({ space: 8 }) {
Image(item.icon)
.width(48)
.height(48)
Text(item.title)
.fontSize(12)
}
.justifyContent(FlexAlign.Center)
.width(80)
.height(102)
.backgroundColor('#8FBF9F')
.borderRadius(10)
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly) // 均分
}
.width(this.rate) // 绑定滑块改变的尺寸
.padding({ top: 10, bottom: 10 })
.backgroundColor(Color.White)
.borderRadius(16)
this.sliderBuilder()
}
.width('100%')
.height('100%')
.backgroundColor(Color.Pink)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
}
3.1.3. 占比能力
占比能力是指子组件的【宽高】按照【预设的比例】,随父容器组件发生变化。实现的方式有 2 种:
- 宽高设置为百分比
- 设置 layoutWeight
属性名 | 类型 | 必填 | 说明 |
width | 是 | 要设置的组件宽度。 | |
height | 是 | 要设置的组件高度。 | |
layoutWeight | number | string | 是 | 父容器尺寸确定时,设置了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)
.onChange((value: number) => {
this.rate = value / 100
})
.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)
}
}
}
3.1.4. 缩放能力
缩放能力是指子组件的【宽高】按照预设的比例,随容器组件发生变化,变化过程中子组件的【宽高比不变】。使用的属性是 aspectRatio
属性名 | 类型 | 必填 | 说明 |
aspectRatio | number | 是 | 指定当前组件的宽高比,aspectRatio = width/height。 |
测试代码:
@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)
.onChange((value: number) => {
this.rate = value / 100
})
.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
.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("#F1F3F5")
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
}
3.1.5. 延伸能力
延伸能力是指容器组件内的子组件,按照其在列表中的先后顺序,随容器组件尺寸变化【显示或隐藏】,隐藏时可以通过滑动切换显示。实现的方式是通过 List 组件或 Scroll 组件
测试代码:
export interface NavItem {
id: number
icon: ResourceStr
title: string
}
@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)
.onChange((value: number) => {
this.rate = value / 100
})
.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
// 核心:调整父容器的尺寸,让页面中显示的组件数量发生改变
// 通过List组件实现隐藏能力
// List({ space: 10 }) {
// ForEach(this.appList, (item: NavItem) => {
// ListItem() {
// 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)
// }
// .width(80)
// .height(102)
// })
// }
// .padding({ top: 16, left: 10 })
// .listDirection(Axis.Horizontal)
// .width('100%')
// .height(118)
// .borderRadius(16)
// .backgroundColor(Color.White)
// 通过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)
}
}
}
3.1.6. 隐藏能力
隐藏能力指的是:按其【显示优先级】,随容器组件尺寸变化显示或隐藏。通过displayPriority属性来实现
属性名 | 类型 | 必填 | 说明 |
displayPriority | number | 是 | 设置当前组件在布局容器中显示的优先级,当父容器空间不足时,低优先级的组件会被隐藏。 |
测试代码:
@Entry
@Component
struct Demo06 {
@State rate: number = 48
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder
slider() {
Slider({
value: this.rate,
min: 0,
max: 400,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.width('60%')
.height(50)
.onChange((value: number) => {
this.rate = value / 100
})
.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:隐藏能力
// 容器组件内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏
// 实现方式:
// 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('#F1F3F5')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
}
3.1.7. 折行能力
折行能力是指容器组件尺寸发生变化,当布局方向尺寸不足以显示完整内容时自动换行。折行能力通过使用 Flex 折行布局 (将wrap属性设置为FlexWrap.Wrap)实现。
名称 | 描述 |
NoWrap | Flex容器的元素单行/列布局,子项不允许超出容器。 |
Wrap | Flex容器的元素多行/列排布,子项允许超出容器。 |
WrapReverse | Flex容器的元素反向多行/列排布,子项允许超出容器。 |
测试代码:
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('#F1F3F5')
}
}
}
3.1.8. 小结
3.2. 响应式布局
自适应布局可以保证窗口尺寸在【一定范围内变化】时,页面的显示是正常的。但是将窗口尺寸【变化较大】时(如窗口宽度从400vp变化为1000vp),仅仅依靠自适应布局可能出现图片异常放大或页面内容稀疏、留白过多等问题,此时就需要借助响应式布局能力调整页面结构。
响应式布局是指页面内的元素可以根据特定的特征(如窗口宽度、屏幕方向等)自动变化以适应外部容器变化的布局能力。
响应式布局中最常使用的特征是窗口宽度,可以将窗口宽度划分为不同的范围(下文中称为断点)。当窗口宽度从一个断点变化到另一个断点时,改变页面布局(如将页面内容从单列排布调整为双列排布甚至三列排布等)以获得更好的显示效果。
三种响应式布局能力:
响应式布局能力 | 简介 |
将窗口宽度划分为不同的范围(即断点),监听窗口尺寸变化,当断点改变时同步调整页面布局。 | |
媒体查询支持监听窗口宽度、横竖屏、深浅色、设备类型等多种媒体特征,当媒体特征发生改变时同步调整页面布局。 | |
栅格组件将其所在的区域划分为有规律的多列,通过调整不同断点下的栅格组件的参数以及其子组件占据的列数等,实现不同的布局效果。 |
3.2.1. 断点
断点以应用窗口宽度为切入点,将应用窗口在宽度维度上分成了几个不同的区间即不同的断点,在不同的区间下,开发者可根据需要实现不同的页面布局效果。
断点名称 | 取值范围(vp) | 设备 |
xs | [0, 320) | 手表等超小屏 |
sm | [320, 600) | 手机竖屏 |
md | [600, 840) | 手机横屏,折叠屏 |
lg | [840, +∞) | 平板,2in1 设备 |
系统提供了多种方法,判断应用当前处于何种断点,进而可以调整应用的布局。常见的监听断点变化的方法如下所示:
- 获取窗口对象并监听窗口尺寸变化(掌握)
- 通过媒体查询监听应用窗口尺寸变化(了解)
- 借助栅格组件能力监听不同断点的变化(掌握)
3.2.1.1. 全局断点
通过窗口对象,监听窗口尺寸变化
- 在 EntryAbility 中添加监听
// MainAbility.ts
import { window, display } from '@kit.ArkUI'
import { UIAbility } from '@kit.AbilityKit'
export default class MainAbility extends UIAbility {
private curBp: string = ''
//...
// 根据当前窗口尺寸更新断点
private updateBreakpoint(windowWidth: number) :void{
try {
// 核心代码1: 将长度的单位由px换算为vp,(px除以像素密度得到vp)
let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels
let newBp: string = ''
// 核心代码2: 基于窗口宽度vp值,判断当前设备属于哪个断点范围
if (windowWidthVp < 320) {
newBp = 'xs'
} else if (windowWidthVp < 600) {
newBp = 'sm'
} else if (windowWidthVp < 840) {
newBp = 'md'
} else {
newBp = 'lg'
}
if (this.curBp !== newBp) {
this.curBp = newBp
// 核心代码3: 使用状态变量记录当前断点值
AppStorage.setOrCreate('currentBreakpoint', this.curBp)
}
} catch(err) {
console.log("getDisplayByIdSync failed err" + err.code)
}
}
onWindowStageCreate(windowStage: window.WindowStage) :void{
windowStage.getMainWindow().then((windowObj) => {
// 获取应用启动时的窗口尺寸
this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width)
// 注册回调函数,监听窗口尺寸变化
windowObj.on('windowSizeChange', (windowSize)=>{
this.updateBreakpoint(windowSize.width)
})
});
// ...
}
//...
}
- 页面中使用断点信息
@Entry
@Component
struct Index {
@StorageProp('currentBreakpoint') curBp: string = 'sm'
build() {
Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}) {
Text(this.curBp).fontSize(50).fontWeight(FontWeight.Medium)
}
.width('100%')
.height('100%')
}
}
试一试:
- 测试根据断点调整页面结构,比如颜色,比如图片
- 考虑 2 种情况即可,比如:
-
- md 为红色,其他为绿色
- sm 为图片 A,其他为图片 B
- ...
- 思考,如果是 3 种情况要如何实现?
3.2.1.2. 系统工具-BreakPointType
上一节演示的工具可以方便的监听屏幕处于哪个断点范围,咱们可以根据断点调整页面:
- 如果是 两种的情况:用 三元表达式 即可
- 如果是 多种的情况:用 三元表达式 就不太方便啦
咱们来看一个系统提供的工具BreakPointType(参考官方示例)
// common/breakpointSystem.ets
interface BreakPointTypeOption<T> {
xs?: T
sm?: T
md?: T
lg?: T
}
export class BreakPointType<T> {
options: BreakPointTypeOption<T>
constructor(option: BreakPointTypeOption<T>) {
this.options = option
}
getValue(currentBreakPoint: string) {
if (currentBreakPoint === 'xs') {
return this.options.xs
} else if (currentBreakPoint === 'sm') {
return this.options.sm
} else if (currentBreakPoint === 'md') {
return this.options.md
} else if (currentBreakPoint === 'lg') {
return this.options.lg
} else {
return undefined
}
}
}
核心用法:
// 1. 导入BreakPointType
import { BreakPointType } from '../../common/breakpointSystem'
@entry
@Component
struct ComB {
// 2. 通过 AppStorage 获取断点值
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'xs'
build() {
Column() {
Text(this.currentBreakpoint)
}
.width(200)
.height(200)
.backgroundColor(
// 3. 实例化 设置不同断点的取值,并通过 getValue 根据当前断点值对应的值
new BreakPointType({
xs: Color.Red,
sm: Color.Yellow,
md: Color.Blue,
lg: Color.Green
})
.getValue(this.currentBreakpoint)
)
}
}
3.2.1.3. 案例-电影列表
使用刚刚学习的媒体查询工具,结合断点来完成一个案例效果
需求:
- xs 及 sm 2 列
- md:3 列
- lg:4 列
自行拓展:
- 设置不同的宽高
- 设置不同的圆角尺寸
- 设置不同的间隙
- 。。。。
基础模版:
interface MovieItem {
title: string
img: ResourceStr
}
@Entry
@Component
struct Demo09_demo {
items: MovieItem[] = [
{ title: '电影标题1', img: $r('app.media.ic_video_grid_1') },
{ title: '电影标题2', img: $r('app.media.ic_video_grid_2') },
{ title: '电影标题3', img: $r('app.media.ic_video_grid_3') },
{ title: '电影标题4', img: $r('app.media.ic_video_grid_4') },
{ title: '电影标题5', img: $r('app.media.ic_video_grid_5') },
{ title: '电影标题6', img: $r('app.media.ic_video_grid_6') },
{ title: '电影标题7', img: $r('app.media.ic_video_grid_7') },
{ title: '电影标题8', img: $r('app.media.ic_video_grid_8') },
{ title: '电影标题9', img: $r('app.media.ic_video_grid_9') },
{ title: '电影标题10', img: $r('app.media.ic_video_grid_10') },
]
build() {
Grid() {
ForEach(this.items, (item: MovieItem) => {
GridItem() {
Column({ space: 10 }) {
Image(item.img)
.borderRadius(10)
Text(item.title)
.width('100%')
.fontSize(20)
.fontWeight(600)
}
}
})
}
.columnsTemplate('1fr 1fr')
.rowsGap(10)
.columnsGap(10)
.padding(10)
}
}
参考代码:
// 1. 导入BreakPointType
import { BreakPointType } from '../../common/breakpointSystem'
interface MovieItem {
title: string
img: ResourceStr
}
@Entry
@Component
struct Demo09_demo {
items: MovieItem[] = [
{ title: '电影标题1', img: $r('app.media.ic_video_grid_1') },
{ title: '电影标题2', img: $r('app.media.ic_video_grid_2') },
{ title: '电影标题3', img: $r('app.media.ic_video_grid_3') },
{ title: '电影标题4', img: $r('app.media.ic_video_grid_4') },
{ title: '电影标题5', img: $r('app.media.ic_video_grid_5') },
{ title: '电影标题6', img: $r('app.media.ic_video_grid_6') },
{ title: '电影标题7', img: $r('app.media.ic_video_grid_7') },
{ title: '电影标题8', img: $r('app.media.ic_video_grid_8') },
{ title: '电影标题9', img: $r('app.media.ic_video_grid_9') },
{ title: '电影标题10', img: $r('app.media.ic_video_grid_10') },
]
// 获取 AppStorage 保存的全局断点(EntryAbility.ets)
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm'
build() {
Grid() {
ForEach(this.items, (item: MovieItem) => {
GridItem() {
Column({ space: 10 }) {
Image(item.img)
.borderRadius(10)
Text(item.title)
.width('100%')
.fontSize(20)
.fontWeight(600)
}
}
})
}
.columnsTemplate(
new BreakPointType({
xs: '1fr 1fr',
sm: '1fr 1fr ',
md: '1fr 1fr 1fr ',
lg: '1fr 1fr 1fr 1fr '
})
.getValue(this.currentBreakpoint)
)
.rowsGap(10)
.columnsGap(10)
.padding(10)
}
}
3.2.2. 媒体查询
媒体查询常用于下面两种场景:
- 针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。
- 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。
相比于上一节演示的 通过窗口对象监听尺寸变化,媒体查询的功能会【更为强大】
只需要查询断点值:【获取窗口对象并监听窗口尺寸】
还要额外查询其他设备信息,【媒体查询】
3.2.2.1. 核心用法
咱们分 2 个角度来看看如何使用媒体查询
- 整合步骤(重要)
- 调整媒体查询条件(了解)
// 导入模块
import mediaquery from '@ohos.mediaquery';
// 1. 创建监听器
const listenerXS: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(0vp<=width<320vp)');
const listenerSM: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(320vp<=width<600vp)');
// 2. 注册监听器
aboutToAppear(): void {
// 添加回调函数
listenerXS.on('change', (res: mediaquery.MediaQueryResult) => {
console.log('changeRes:', JSON.stringify(res))
// 执行逻辑
})
listenerSM.on('change', (res: mediaquery.MediaQueryResult) => {
console.log('changeRes:', JSON.stringify(res))
// 执行逻辑
})
}
// 4. 移除监听器
aboutToDisappear(): void {
// 移除监听 避免性能浪费
listenerXS.off('change')
listenerSM.off('change')
}
试一试:
- 参考示例代码:完成 4 个断点的监听
- 参考文档,增加黑暗模式查询
断点名称 | 取值范围(vp) | 设备 |
xs | [0, 320) | 手表等超小屏 |
sm | [320, 600) | 手机竖屏 |
md | [600, 840) | 手机横屏,折叠屏 |
lg | [840, +∞) | 平板,2in1 设备 |
3.2.2.2. 使用查询结果
目前查询的内容只在当前页面可以使用,如果希望应用中任意位置都可以使用,咱们可以使用AppStorage 进行共享
核心步骤:
- 事件中通过 AppStorage.set(key,value)的方式保存当前断点值
- 需要使用的位置通过 AppStorage 来获取即可
// 添加回调函数
listenerXS.on('change', (res: mediaquery.MediaQueryResult) => {
console.log('changeRes:', JSON.stringify(res))
if (res.matches == true) {
// this.currentBreakpoint = 'xs'
AppStorage.set('currentBreakpoint', 'xs')
}
})
// 组件中引入 AppStorage
@StorageProp('currentBreakpoint') currentBreakpoint: string = ''
// 在需要的位置使用 AppStorage 中保存的断点值
Text(this.currentBreakpoint)
3.2.2.3. 案例-电影列表
使用刚刚学习的媒体查询工具,结合断点来完成一个案例效果
需求:
- xs 及 sm 2 列
- md:3 列
- lg:4 列
自行拓展:
- 设置不同的宽高
- 设置不同的圆角尺寸
- 设置不同的间隙
- 。。。。
基础模版:
interface MovieItem {
title: string
img: ResourceStr
}
@Entry
@Component
struct Demo09_demo {
items: MovieItem[] = [
{ title: '电影标题1', img: $r('app.media.ic_video_grid_1') },
{ title: '电影标题2', img: $r('app.media.ic_video_grid_2') },
{ title: '电影标题3', img: $r('app.media.ic_video_grid_3') },
{ title: '电影标题4', img: $r('app.media.ic_video_grid_4') },
{ title: '电影标题5', img: $r('app.media.ic_video_grid_5') },
{ title: '电影标题6', img: $r('app.media.ic_video_grid_6') },
{ title: '电影标题7', img: $r('app.media.ic_video_grid_7') },
{ title: '电影标题8', img: $r('app.media.ic_video_grid_8') },
{ title: '电影标题9', img: $r('app.media.ic_video_grid_9') },
{ title: '电影标题10', img: $r('app.media.ic_video_grid_10') },
]
build() {
Grid() {
ForEach(this.items, (item: MovieItem) => {
GridItem() {
Column({ space: 10 }) {
Image(item.img)
.borderRadius(10)
Text(item.title)
.width('100%')
.fontSize(20)
.fontWeight(600)
}
}
})
}
.columnsTemplate('1fr 1fr')
.rowsGap(10)
.columnsGap(10)
.padding(10)
}
}
参考代码:
import { BreakPointType, BreakpointSystem, BreakpointKey } from '../../common/breakpointsystem'
interface MovieItem {
title: string
img: ResourceStr
}
@Entry
@Component
struct Demo09_demo {
items: MovieItem[] = [
{ title: '电影标题1', img: $r('app.media.ic_video_grid_1') },
{ title: '电影标题2', img: $r('app.media.ic_video_grid_2') },
{ title: '电影标题3', img: $r('app.media.ic_video_grid_3') },
{ title: '电影标题4', img: $r('app.media.ic_video_grid_4') },
{ title: '电影标题5', img: $r('app.media.ic_video_grid_5') },
{ title: '电影标题6', img: $r('app.media.ic_video_grid_6') },
{ title: '电影标题7', img: $r('app.media.ic_video_grid_7') },
{ title: '电影标题8', img: $r('app.media.ic_video_grid_8') },
{ title: '电影标题9', img: $r('app.media.ic_video_grid_9') },
{ title: '电影标题10', img: $r('app.media.ic_video_grid_10') },
]
breakpointSystem: BreakpointSystem = new BreakpointSystem()
@StorageProp(BreakpointKey)
currentBreakpoint: string = 'sm'
aboutToAppear(): void {
this.breakpointSystem.register()
}
aboutToDisappear(): void {
this.breakpointSystem.unregister()
}
build() {
Grid() {
ForEach(this.items, (item: MovieItem) => {
GridItem() {
Column({ space: 10 }) {
Image(item.img)
.borderRadius(10)
Text(item.title)
.width('100%')
.fontSize(20)
.fontWeight(600)
}
}
})
}
.columnsTemplate(new BreakPointType({
xs: '1fr 1fr',
sm: '1fr 1fr ',
md: '1fr 1fr 1fr ',
lg: '1fr 1fr 1fr 1fr '
}).getValue(this.currentBreakpoint))
.rowsGap(10)
.columnsGap(10)
.padding(10)
}
}
3.2.3. 栅格布局 Grid
、创建网格
栅格组件的本质是:将组件划分为有规律的多列,通过调整【不同断点】下的【栅格组件的列数】,及【子组件所占列数】实现不同布局
通过调整总列数,及子组件所占列数,实现不同布局
断点:
断点名称 | 取值范围(vp) | 设备 |
xs | [0, 320) | 手表等超小屏 |
sm | [320, 600) | 手机竖屏 |
md | [600, 840) | 手机横屏,折叠屏 |
lg | [840, +∞) | 平板,2in1 设备 |
比如:
参考栅格列数设置:
3.2.3.1. 核心用法
// 行
GridRow(属性){
// 列
GridCol(属性){
}
}
测试代码:
优先级从上往下:
- GridRow的 columns 属性、GridCol 的 span 属性(掌握)
- GridRow 的 gutter 属性、GridCol 的 offset 属性(掌握)
- GridRow breakpoints 属性 和 的 onBreakpointChange 事件(了解)
@Entry
@Component
struct Demo10 {
@State breakPoint: string = 'sm'
// 颜色数组
build() {
Column() {
// GridRow 默认支持 4 个断点
// xs:(0vp<=width<320vp) 智能穿戴,比如手表
// sm:(320vp<=width<600vp) 手机
// md:(600vp<=width<840vp) 折叠屏
// lg:(840vp<=width) 平板
GridRow({
// 4个断点 和默认的一样
breakpoints: { value: ['320vp', '600vp', '840vp'] },
gutter: 10, // 子组件间隙
// columns: 12 // 统一设计列数 默认 12
columns: {
// 不同的断点分别设置不同的列数
xs: 2, // 超小
sm: 4, // 手机竖屏
md: 8, // 折叠,手机横屏
lg: 12 // 大屏
}
}) {
ForEach(Array.from({ length: 2 }), (item: string, index: number) => {
GridCol({
// 每一行 2 个子元素,span 怎么设置(占的行数)
// span: 2, // 占用列数 这样设置所有断点都是 2 列
// 支持不同断点分别设置不同的占用列数
span: {
xs: 2,
sm: 2,
md: 2,
lg: 4
},
// offset 偏移列数 默认为 0
// offset: 1, // 偏移一列
// 支持不同断点分别设置偏移不同的列数
offset: {
// xs: 2,
// sm: 1
}
}) {
Text(index.toString())
.height(50)
}
.border({ width: 1 })
})
}
.border({ width: 1, color: Color.Orange })
.width('90%')
.height('90%')
// 断点发生变化时触发回调
// breakpoint当前的断点 字符串
.onBreakpointChange(breakpoint => {
console.log('breakpoint:', breakpoint)
this.breakPoint = breakpoint
})
Text(this.breakPoint)
.width('100%')
.textAlign(TextAlign.Center)
.fontSize(30)
.fontWeight(900)
}
.width('100%')
.height('100%')
}
}
3.2.3.2. 案例-标题栏与搜索栏
标题栏和搜索栏,在sm和md断点下分两行显示,在lg断点下单行显示,可以通过栅格实现。在sm和md断点下,标题栏和搜索栏占满12列,此时会自动换行显示。在lg断点下,标题栏占8列而搜索栏占4列,此时标题栏和搜索栏在同一行中显示。
sm/md | lg | |
效果图 | | |
栅格布局图 | | |
3.2.3.3. 案例-登录界面
结合咱们刚刚学习的 栅格布局。来实现如下效果
需求:
- sm:4 列,占 4 列
- md:8 列,占 6 列,偏移 1
- lg: 12 列,占 8 列,偏移 2
基础模版:
@Entry
@Component
struct Demo11_login {
build() {
Stack() {
// 辅助用的栅格(顶层粉色区域)
GridRow({ gutter: 10, columns: { sm: 4, md: 8, lg: 12 } }) {
ForEach(Array.from({ length: 12 }), () => {
GridCol()
.width('100%')
.height('100%')
.backgroundColor('#baffa2b4')
})
}
.zIndex(2)
.height('100%')
// 内容区域
GridRow({
// TODO 分别设置不同断点的 列数
}) {
// 列
GridCol({
// TODO 分别设置不同断点的 所占列数
// TODO 分别设置不同断点的 偏移
}) {
Column() {
// logo+文字
LogoCom()
// 输入框 + 底部提示文本
InputCom()
// 登录+注册账号按钮
ButtonCom()
}
}
}
.width('100%')
.height('100%')
.backgroundColor('#ebf0f2')
}
}
}
@Component
struct LogoCom {
build() {
Column({ space: 5 }) {
Image($r('app.media.ic_logo'))
.width(80)
Text('登录界面')
.fontSize(23)
.fontWeight(900)
Text('登录账号以使用更多服务')
.fontColor(Color.Gray)
}
.margin({ top: 100 })
}
}
@Component
struct InputCom {
build() {
Column() {
Column() {
TextInput({ placeholder: '账号' })
.backgroundColor(Color.Transparent)
Divider()
.color(Color.Gray)
TextInput({ placeholder: '密码' })
.type(InputType.Password)
.backgroundColor(Color.Transparent)
}
.backgroundColor(Color.White)
.borderRadius(20)
.padding({ top: 10, bottom: 10 })
Row() {
Text('短信验证码登录')
.fontColor('#006af7')
.fontSize(14)
Text('忘记密码')
.fontColor('#006af7')
.fontSize(14)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 10 })
}
.padding(5)
.margin({ top: 80 })
}
}
@Component
struct ButtonCom {
build() {
Column({ space: 10 }) {
Button('登录')
.width('90%')
Text('注册账号')
.fontColor('#006af7')
.fontSize(16)
}
.margin({ top: 60 })
}
}
参考代码
@Entry
@Component
struct Demo11_login {
build() {
Stack() {
// 辅助用的栅格(顶层粉色区域)
GridRow({ gutter: 10, columns: { sm: 4, md: 8, lg: 12 } }) {
ForEach(Array.from({ length: 12 }), () => {
GridCol()
.width('100%')
.height('100%')
.backgroundColor('#baffa2b4')
})
}
.zIndex(2)
.height('100%')
// 内容区域
GridRow({
// TODO 分别设置不同断点的 列数
columns: {
sm: 4,
md: 8,
lg: 12
}
}) {
// 列
GridCol({
// TODO 分别设置不同断点的 所占列数
span: {
sm: 4,
md: 6,
lg: 8
},
// TODO 分别设置不同断点的 偏移
offset: {
md: 1,
lg: 2
}
}) {
Column() {
// logo+文字
LogoCom()
// 输入框 + 底部提示文本
InputCom()
// 登录+注册账号按钮
ButtonCom()
}
}
}
.width('100%')
.height('100%')
.backgroundColor('#ebf0f2')
}
}
}
@Component
struct LogoCom {
build() {
Column({ space: 5 }) {
Image($r('app.media.ic_logo'))
.width(80)
Text('登录界面')
.fontSize(23)
.fontWeight(900)
Text('登录账号以使用更多服务')
.fontColor(Color.Gray)
}
.margin({ top: 100 })
}
}
@Component
struct InputCom {
build() {
Column() {
Column() {
TextInput({ placeholder: '账号' })
.backgroundColor(Color.Transparent)
Divider()
.color(Color.Gray)
TextInput({ placeholder: '密码' })
.type(InputType.Password)
.backgroundColor(Color.Transparent)
}
.backgroundColor(Color.White)
.borderRadius(20)
.padding({ top: 10, bottom: 10 })
Row() {
Text('短信验证码登录')
.fontColor('#006af7')
.fontSize(14)
Text('忘记密码')
.fontColor('#006af7')
.fontSize(14)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 10 })
}
.padding(5)
.margin({ top: 80 })
}
}
@Component
struct ButtonCom {
build() {
Column({ space: 10 }) {
Button('登录')
.width('90%')
Text('注册账号')
.fontColor('#006af7')
.fontSize(16)
}
.margin({ top: 60 })
}
}
3.2.4. 横竖屏开发实践
3.2.4.1. 配置 module.json5 的 orientation 字段
此字段配置的是应用启动时的窗口显示状态,对于开屏时就需要以默认的横屏或者竖屏方式显示,需要在此字段进行相应的配置:
// module.json5
{
"module": {
...
,
"abilities": [
{
"name": "EntryAbility",
...,
"orientation":"auto_rotation_unspecified" // 受开关控制的自动旋转模式
}
]
}
}
4. 功能级一多开发(了解)
一个前提
功能开发的适配主要体现在需要适配不同范类的应用,比如既要适配手机和平板,也需要适配智能穿戴设备,如果是同泛类产品,系统能力一致,无需考虑多设备上应用功能开发的差异,我们的美寇商城需要适配的是手机和Pad,属于同泛类产品,无需考虑功能开发的差异。以下是常见类型分类:
- 默认设备(一般为手机)、平板
- 车机、智慧屏
- 智能穿戴
什么是系统能力
系统能力(即SystemCapability,缩写为SysCap)指操作系统中每一个相对独立的特性,如蓝牙,WIFI,NFC,摄像头等,都是系统能力之一。每个系统能力对应多个API,随着目标设备是否支持该系统能力共同存在或消失。
比如:display.isFoldable() 这个 api 并不是每个设备都可以使用,在调用之前就可以先判断一下
如何适配系统能力
- 不同设备的系统能力有差异,如智能穿戴设备是否具备定位能力、智慧屏是否具备摄像头等,功能如何兼容。
方法1:使用canUse接口判断设备是否支持某系统能力
if (canIUse("能力集的名字")) {
// 正常调用
} else {
// 提示用户
console.log("该设备不支持SystemCapability.Communication.NFC.Core")
}
方法2:通过import动态导入,配合try/catch
import controller from '@kit.ConnectivityKit';
try {
controller.enableNfc();
console.log("controller enableNfc success");
} catch (busiError) {
console.log("controller enableNfc busiError: " + busiError);
}
注意:
- 目前的开发主要是 手机及平板开发,属于统一范类,功能差别不大
- 目前 Harmony Next 的系统首发登录的肯定是手机,其他设备会逐步接入
5. 工程级一多(掌握)
5.1. 概念
一多模式下,官方推荐在开发过程中采用"三层工程架构",其实就是把项目拆分成不同类型的模块,再通过模块之间的引用组合,最终实现应用功能,拆分规范如下:
- commons(公共能力层):用于存放公共基础能力合集,比如工具库,公共配置等
- features(基础特性层):用于存放应用中相对独立的各个功能的UI以及业务逻辑实现
- products(产品定制层):用于针对不同设备形态进行功能和特性集成,作为应用入口
参考官方示例:美蔻商城将进行如下拆分
5.2. 选择合适的包类型
HAP、HAR、HSP三者的功能和使用场景总结对比如下:
Module类型 | 包类型 | 说明 |
Ability | 应用的功能模块,可以独立安装和运行,必须包含一个entry类型的HAP,可选包含一个或多个feature类型的HAP。 | |
Static Library | 静态共享包,编译态复用。 - 支持应用内共享,也可以发布后供其他应用使用。 - 作为二方库,发布到OHPM私仓,供公司内部其他应用使用。 - 作为三方库,发布到OHPM中心仓,供其他应用使用。 - 多包(HAP/HSP)引用相同的HAR时,会造成多包间代码和资源的重复拷贝,从而导致应用包膨大。 - 注意:编译HAR时,建议开启混淆能力,保护代码资产。 | |
Shared Library | 动态共享包,运行时复用。 - 当前仅支持应用内共享。 - 当多包(HAP/HSP)同时引用同一个共享包时,采用HSP替代HAR,可以避免HAR造成的多包间代码和资源的重复拷贝,从而减小应用包大小。 |
5.3. 项目搭建
- common设计为hsp包,新建module时选择
Shared Library
, 内部存放全局通用的工具函数,公共配置等 - feature设计为hsp包,新建module时选择
Shared Library
, 内部存放相对独立的业务单元,比如购物车
、我的
、分类
、Home
,也就是首页底部Tab切换时的四个核心业务模块 - product为产品层,里面放置phone模块,也就是入口模块,在phone中我们放置入口ability和所有页面级别的组件(hap)
选用 Shared Library的原因:
动态共享包,运行时复用,减小应用包大小。
接下来咱们完成项目搭建,参考下图
核心步骤:
- 创建项目:项目名用 AGC 平台创建的项目名
- 创建目录 commons、features、products
- commons目录下
-
- 创建 Shared Library,起名 basic,存放全局通用的工具函数,公共配置等
- features 目录下:
-
- 创建 Shared Library:内部存放相对独立的业务单元,比如
购物车(cart)
、我的(my)
、分类(category)
、首页(home)
,也就是首页底部Tab切换时的四个核心业务模块
- 创建 Shared Library:内部存放相对独立的业务单元,比如
- products 目录下:
-
- 将原本的 entry 移入该目录
- 把 entry 模块并改名为 phone
- 选择 phone 模块,运行到模拟器
git 记录:
- 本地初始化 git,并记录
- 工程搭建
- 创建 gitee 仓库,本地推送到远程
6. 准备工作
接下来完成模块编码前期的准备工作
- 图标+名字
- 底部 tabs
- 日志工具
6.1. 图标+名字
要调整的地方有 2 个
- 管理界面的名字+图标(老版本)
- 桌面的名字+图标(新版本)
- 管理界面图标+名字
下载图片,替换下图中,红线对应的图片
- 桌面的名字+图标
- 拷贝资源到 phone 的resources/base/media目录下
- 修改 src/main/resources/base/element/string.json 配置文件,国际化
- 卸载应用,重新编译
{
"module": {
"name": "phone",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image", // 应用图标
"label": "$string:EntryAbility_label", // 应用名称
"startWindowIcon": "$media:startIcon", // 启动图标
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
]
}
}
git记录:
准备工作-图标+名字设置
6.2. 底部 tabs 搭建
基础模版:
- 在 phone 入口模块准备 tabs 静态结构
注意:拷贝过去之后因为还没有颜色会报错
interface TabItem {
text: string
normal: ResourceStr
active: ResourceStr
}
@Entry
@Component
struct Index {
@State activeIndex: number = 0
list: TabItem[] = [
{ text: '首页', normal: $r('app.media.ic_public_home_normal'), active: $r('app.media.ic_public_home_active') },
{ text: '分类', normal: $r('app.media.ic_public_pro_normal'), active: $r('app.media.ic_public_pro_active') },
{ text: '购物袋', normal: $r('app.media.ic_public_cart_normal'), active: $r('app.media.ic_public_cart_active') },
{ text: '我的', normal: $r('app.media.ic_public_my_normal'), active: $r('app.media.ic_public_my_active') },
]
build() {
Tabs({ barPosition: BarPosition.End }) {
ForEach(this.list, (item: TabItem, index: number) => {
TabContent() {
}
.tabBar(this.TabItemBuilder(item, index))
})
}
.scrollable(false)
.onTabBarClick(index => {
this.activeIndex = index
})
}
@Builder
TabItemBuilder(item: TabItem, index: number) {
Column() {
Image(this.activeIndex === index ? item.active : item.normal)
.width(24)
.aspectRatio(1)
Text(item.text)
// 跨 HSP 访问资源,需要在 oh-package.json5中导入
.fontColor($r('[basic].color.black'))
.fontSize(12)
}
.justifyContent(FlexAlign.SpaceEvenly)
.height(50)
}
}
- 在 basic 模块下新建颜色配置:
{
"color": [
{
"name": "black",
"value": "#191919"
},
{
"name": "white",
"value": "#FFFFFF"
},
{
"name": "text",
"value": "#434343"
},
{
"name": "gray",
"value": "#A6A3AD"
},
{
"name": "red",
"value": "#FF2737"
},
{
"name": "linear_begin",
"value": "#FD3F8F"
},
{
"name": "linear_end",
"value": "#FF773C"
},
{
"name": "yellow",
"value": "#F2E7DC"
},
{
"name": "under",
"value": "#F5F4F9"
},
{
"name": "border",
"value": "#e4e4e4"
}
]
}
- 在 phone 模块的 oh-package.json5 添加 basic 模块引用
注意:修改了配置后,右上角会出现的 Sync Now ,点击重新构建项目。
{
"name": "phone",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {
// 模块名称: 模块路径
"basic": "file:../../commons/basic",
}
}
核心步骤:
- phone
-
- 整合图片资源到 phone中(上一节已经完成)
- 整合基础模版
- common/basic:
-
- 整合颜色到 resources/base/element/color.json 中
- 这个文件,不存在需要先创建
- phone:
-
- oh-package.json5中导入 配置 commons/basic
- 点击 右上角出现的 SyncNow 即可完成安装
-
在 basic 模块中,鼠标右键,创建资源文件,可直接新建 color.json 文件到 resource 目录中。
注:跨模块访问资源,需要【先导入模块】才可以访问
git 记录:
准备工作-底部 tabs 搭建
6.3. 引入 features 模块
接下来将 4 个 tabs 对应的组件创建好并整合到 index 页面,这几个都是 tabs 管理的组件
核心步骤:
- 创建基本页面:
- feature 中导出,4个:
- products/phone中导入并使用
- 创建tab管理的 4 个基本页面:
-
- 分别在 features 下的 cart、category、home、my 的/ets 下创建 views 目录,页面命名参考:
-
-
- CartView:购物车
- CategoryView:分类
- HomeView:主页
- MyView:我的
-
-
- 页面内容参考截图,设置基础结构即可
- 导出:
-
- 4个 tabs 对应的模块都需要导出
- 参考代码如下
- 依次完成,features下面的:cart、category、home、my的导出
// export { MainPage } from './src/main/ets/components/mainpage/MainPage'
export { CartView } from './src/main/ets/views/CartView'
- 导入并使用:
{
"name": "phone",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {
"basic": "file:../../commons/basic"
"home": "file:../../features/home",
"cart": "file:../../features/cart",
"category": "file:../../features/category",
"my": "file:../../features/my",
}
}
import { HomeView } from 'home';
import { CategoryView } from 'category';
import { CartView } from 'cart';
import { MyView } from 'my';
interface TabItem {
text: string
normal: ResourceStr
active: ResourceStr
}
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
@State activeIndex: number = 0
list: TabItem[] = [
{ text: '首页', normal: $r('app.media.ic_public_home_normal'), active: $r('app.media.ic_public_home_active') },
{ text: '分类', normal: $r('app.media.ic_public_pro_normal'), active: $r('app.media.ic_public_pro_active') },
{ text: '购物袋', normal: $r('app.media.ic_public_cart_normal'), active: $r('app.media.ic_public_cart_active') },
{ text: '我的', normal: $r('app.media.ic_public_my_normal'), active: $r('app.media.ic_public_my_active') },
]
build() {
Tabs({ barPosition: BarPosition.End }) {
ForEach(this.list, (item: TabItem, index: number) => {
TabContent() {
if (index == 0) {
HomeView()
} else if (index == 1) {
CategoryView()
} else if (index == 2) {
CartView()
} else {
MyView()
}
}
.tabBar(this.TabItemBuilder(item, index))
})
}
.scrollable(false)
.onTabBarClick(index => {
this.activeIndex = index
})
}
@Builder
TabItemBuilder(item: TabItem, index: number) {
Column() {
Image(this.activeIndex === index ? item.active : item.normal)
.width(24)
.aspectRatio(1)
Text(item.text)
.fontColor($r('app.color.black'))
.fontSize(12)
}
.justifyContent(FlexAlign.SpaceEvenly)
.height(50)
}
}
git记录:
准备工作-引入 features 模块
6.4. 日志工具
HarmonyOsLog是一个日志打印工具,支持hilog和console两种工具打印,支持各种类型包含JSON格式化打印,非常的方便,咱们把他整合到项目中
需求:
- 在 commons/basic
-
- 下载 logger 日志工具
- 完成初始化并导出,起别名 Logger
- products/phone 中测试使用
打开终端:
- 在 commons/basic下打开终端进行安装 (mac 和 window 界面可能略有差异)
ohpm install @abner/log
- 初始化,起别名,导出
import { Log } from '@abner/log'
// 初始化
Log.init({
tag: "MK_LOGGER",
domain: 0x0101,
close: false,
isHilog: true,
showLogLocation: true,
logSize: 1024
})
export { Log as Logger }
export { add } from './src/main/ets/utils/Calc'
export * from './src/main/ets/utils/Logger'
- 测试使用:
// 导入
import { Logger } from 'basic'
// 测试使用
aboutToAppear(): void {
Logger.info({
name: 'jack',
age: 18
})
}
git 记录:
准备工作-日志工具