鴻蒙一多開發:多設備適配
鏈接
文章配套的圖片素材如下
知識點-圖片資源.zip
“一次開發、多端部署”簡稱“一多”:一套代碼工程,一次開發上架,多端按需部署。
為了實現這個目標,主要解決 3 個核心問題:
- 頁面適配問題:界面級一多(重點掌握)
- 功能兼容問題:功能級一多(瞭解)
- 工程如何組織:工程級一多(重點掌握)
界面級一多能力(掌握)
鏈接
界面級一多能力有 2 類:
- 自適應佈局: 略微調整界面結構
- 響應式佈局:比較大的界面調整
自適應佈局
自適應佈局
自適應佈局的能力有 7 種,主要解決的是:窗口尺寸在【一定範圍內】變化時,頁面能夠正常顯示
|
自適應佈局類別 |
自適應佈局能力 |
使用場景 |
實現方式 |
|
自適應拉伸
|
拉伸能力 |
容器組件尺寸發生變化時,增加或減小的空間全部分配給容器組件內指定區域。 |
Flex佈局的flexGrow和flexShrink屬性 |
|
|
均分能力 |
容器組件尺寸發生變化時,增加或減小的空間均勻分配給容器組件內所有空白區域。 |
Row組件、Column組件或Flex組件的justifyContent屬性設置為FlexAlign.SpaceEvenly |
|
自適應縮放
|
佔比能力 |
子組件的寬或高按照預設的比例,隨容器組件發生變化。 |
基於通用屬性的兩種實現方式:
- 將子組件的寬高設置為父組件寬高的百分比
- layoutWeight屬性
|
|
|
縮放能力 |
子組件的寬高按照預設的比例,隨容器組件發生變化,且變化過程中子組件的寬高比不變。 |
佈局約束的aspectRatio屬性 |
|
自適應延伸
|
延伸能力 |
容器組件內的子組件,按照其在列表中的先後順序,隨容器組件尺寸變化顯示或隱藏。 |
基於容器組件的兩種實現方式:
- 通過List組件實現 - 通過Scroll組件配合Row組件或Column組件實現 |
|
|
隱藏能力 |
容器組件內的子組件,按照其預設的顯示優先級,隨容器組件尺寸變化顯示或隱藏。相同顯示優先級的子組件同時顯示或隱藏。 |
佈局約束的displayPriority屬性 |
|
自適應折行
|
折行能力 |
容器組件尺寸發生變化時,如果佈局方向尺寸不足以顯示完整內容,自動換行。 |
Flex組件的wrap屬性設置為FlexWrap.Wrap |
拉伸能力
Flex佈局
拉伸能力指的是容器尺寸發生變化時:將變化的空間,分配給容器內的【指定區域】。利用的是 2 個屬性:
|
屬性名 |
類型 |
必填 |
説明 |
|
flexGrow
|
number
|
是
|
設置父容器在主軸方向上的剩餘空間分配給此屬性所在組件的比例。 默認值:0
|
|
flexShrink
|
number
|
是
|
設置父容器壓縮尺寸分配給此屬性所在組件的比例。 父容器為Column、Row時,默認值:0 父容器為Flex時,默認值:1 |
測試代碼:
@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)
}
}
}
均分能力
均分能力指的是容器尺寸發生變化時:將變化的空間,【均勻分配】給容器組件內【空白區域】。利用的是一個屬性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)
}
}
}
佔比能力
佔比能力是指子組件的【寬高】按照【預設的比例】,隨父容器組件發生變化。實現的方式有 2 種:
- 寬高設置為百分比
- 設置 layoutWeight
|
屬性名 |
類型 |
必填 |
説明 |
|
width
|
Length |
是
|
要設置的組件寬度。 單位:vp
|
|
height
|
Length |
是
|
要設置的組件高度。 單位:vp
|
|
layoutWeight
|
number
|
string
|
是
|
測試代碼:
@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)
}
}
}
縮放能力
縮放能力是指子組件的【寬高】按照預設的比例,隨容器組件發生變化,變化過程中子組件的【寬高比不變】。使用的屬性是 aspectRatio
|
屬性名 |
類型 |
必填 |
説明 |
|
aspectRatio
|
number
|
是
|
指定當前組件的寬高比,aspectRatio = width/height。 API version 9及以前,默認值為:1.0。 API version 10:無默認值。 説明: 該屬性在不設置值或者設置非法值時不生效。 例如,Row只設置寬度且沒有子組件,aspectRatio不設置值或者設置成負數時,此時Row高度為0。 |
測試代碼:
@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
.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)
}
}
}
延伸能力
延伸能力是指容器組件內的子組件,按照其在列表中的先後順序,隨容器組件尺寸變化【顯示或隱藏】,隱藏時可以通過滑動切換顯示。實現的方式是通過 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)
.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)
}
}
}
隱藏能力
隱藏能力指的是:按其【顯示優先級】,隨容器組件尺寸變化顯示或隱藏。通過displayPriority屬性來實現
|
屬性名 |
類型 |
必填 |
説明 |
|
displayPriority
|
number
|
是
|
設置當前組件在佈局容器中顯示的優先級,當父容器空間不足時,低優先級的組件會被隱藏。 小數點後的數字不作優先級區分,即區間為[x, x + 1)內的數字視為相同優先級。例如:1.0與1.9為同一優先級。 説明: 僅在Row/Column/Flex(單行)容器組件中生效。 |
測試代碼:
@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)
}
}
}
折行能力
折行能力是指容器組件尺寸發生變化,當佈局方向尺寸不足以顯示完整內容時自動換行。折行能力通過使用 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')
}
}
}
小結
響應式佈局
響應式佈局
自適應佈局可以保證窗口尺寸在【一定範圍內變化】時,頁面的顯示是正常的。但是將窗口尺寸【變化較大】時(如窗口寬度從400vp變化為1000vp),僅僅依靠自適應佈局可能出現圖片異常放大或頁面內容稀疏、留白過多等問題,此時就需要藉助響應式佈局能力調整頁面結構。
響應式佈局是指頁面內的元素可以根據特定的特徵(如窗口寬度、屏幕方向等)自動變化以適應外部容器變化的佈局能力。
響應式佈局中最常使用的特徵是窗口寬度,可以將窗口寬度劃分為不同的範圍(下文中稱為斷點)。當窗口寬度從一個斷點變化到另一個斷點時,改變頁面佈局(如將頁面內容從單列排布調整為雙列排布甚至三列排布等)以獲得更好的顯示效果。
三種響應式佈局能力:
|
響應式佈局能力 |
簡介 |
|
斷點 |
將窗口寬度劃分為不同的範圍(即斷點),監聽窗口尺寸變化,當斷點改變時同步調整頁面佈局。
|
|
媒體查詢 |
媒體查詢支持監聽窗口寬度、橫豎屏、深淺色、設備類型等多種媒體特徵,當媒體特徵發生改變時同步調整頁面佈局。
|
|
柵格佈局 |
柵格組件將其所在的區域劃分為有規律的多列,通過調整不同斷點下的柵格組件的參數以及其子組件佔據的列數等,實現不同的佈局效果。
|
斷點
斷點以應用窗口寬度為切入點,將應用窗口在寬度維度上分成了幾個不同的區間即不同的斷點,在不同的區間下,開發者可根據需要實現不同的頁面佈局效果。
|
斷點名稱 |
取值範圍(vp) |
設備 |
|
xs
|
[0, 320)
|
手錶等超小屏
|
|
sm
|
[320, 600)
|
手機豎屏
|
|
md
|
[600, 840)
|
手機橫屏,摺疊屏
|
|
lg
|
[840, +∞)
|
平板,2in1 設備
|
系統提供了多種方法,判斷應用當前處於何種斷點,進而可以調整應用的佈局。常見的監聽斷點變化的方法如下所示:
- 獲取窗口對象並監聽窗口尺寸變化(掌握)
- 通過媒體查詢監聽應用窗口尺寸變化(瞭解)
- 藉助柵格組件能力監聽不同斷點的變化(掌握)
全局斷點
通過窗口對象,監聽窗口尺寸變化
- 在 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%')
}
}
:::success 試一試:
- 測試根據斷點調整頁面結構,比如顏色,比如圖片
- 考慮 2 種情況即可,比如:
- md 為紅色,其他為綠色
- sm 為圖片 A,其他為圖片 B
- ...
- 思考,如果是 3 種情況要如何實現?
:::
系統工具-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)
)
}
}
案例-電影列表
使用剛剛學習的媒體查詢工具,結合斷點來完成一個案例效果
:::success需求:
- 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)
}
}
斷點工具封裝
鏈接、window事件
基於上一步實現的效果,進行二次封裝,簡化後續調用
:::success首選上來就要執行一次
獲取window對象有兩種方式
- EntryAbility中有一個windowStage.getMainWindow()
- window.getLastWindow(getContext() || 傳入上下文)
:::
import { display, window } from "@kit.ArkUI"
export class ScreenManager {
ctx: Context
curBp: BreakPointEnum = BreakPointEnum.XS
constructor(ctx: Context) {
this.ctx = ctx
}
async init() {
const win = await window.getLastWindow(this.ctx)
const uiCtx = win.getUIContext()
// 先計算當前的屏幕寬度
this.updateBreakpoint(uiCtx.px2vp(display.getDefaultDisplaySync()
.width))
win.on("windowSizeChange", (size) => {
this.updateBreakpoint(size.width)
})
}
async off() {
const win = await window.getLastWindow(this.ctx)
win.off("windowSizeChange")
}
updateBreakpoint(windowWidth: number): void {
try {
// 獲取vp尺寸
let windowWidthVp = windowWidth / display.getDefaultDisplaySync()
.densityPixels
let newBp: BreakPointEnum = BreakPointEnum.XS
// 核心代碼2: 基於窗口寬度vp值,判斷當前設備屬於哪個斷點範圍
if (windowWidthVp < 320) {
newBp = BreakPointEnum.XS
} else if (windowWidthVp < 600) {
newBp = BreakPointEnum.SM
} else if (windowWidthVp < 840) {
newBp = BreakPointEnum.MD
} else if (windowWidthVp < 1440) {
newBp = BreakPointEnum.LG
} else {
newBp = BreakPointEnum.XL
}
if (this.curBp !== newBp) {
this.curBp = newBp
// 核心代碼3: 使用狀態變量記錄當前斷點值
AppStorage.setOrCreate('currentBreakpoint', this.curBp)
}
} catch (err) {
}
}
}
export enum BreakPointEnum {
XS = "xs",
SM = "sm",
MD = "md",
LG = "lg",
XL = "xl"
}
使用步驟
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { ScreenManager } from '../utils/ScreenManager';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
screenManager?: ScreenManager
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
this.screenManager = new ScreenManager(this.context)
this.screenManager.init()
windowStage.loadContent('pages/ScreenWidth', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
this.screenManager.off() // 窗口卸載
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
媒體查詢
媒體查詢
媒體查詢常用於下面兩種場景:
- 針對設備和應用的屬性信息(比如顯示區域、深淺色、分辨率),設計出相匹配的佈局。
- 當屏幕發生動態改變時(比如分屏、橫豎屏切換),同步更新應用的頁面佈局。
相比於上一節演示的 通過窗口對象監聽尺寸變化,媒體查詢的功能會【更為強大】
:::success只需要查詢斷點值:【獲取窗口對象並監聽窗口尺寸】
還要額外查詢其他設備信息,【媒體查詢】
:::
核心用法
咱們分 2 個角度來看看如何使用媒體查詢
- 整合步驟(重要)
- 調整媒體查詢條件(瞭解)
// 導入模塊
import mediaquery from '@ohos.mediaquery';
// 1. 創建監聽器
listenerXS = this.getUIContext()
.getMediaQuery()
.matchMediaSync('(0vp<=width<320vp)');
listenerSM = this.getUIContext()
.getMediaQuery()
.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')
}
:::success試一試:
- 參考示例代碼:完成 4 個斷點的監聽
- 參考文檔,增加黑暗模式查詢
:::
|
斷點名稱 |
取值範圍(vp) |
設備 |
|
xs
|
[0, 320)
|
手錶等超小屏
|
|
sm
|
[320, 600)
|
手機豎屏
|
|
md
|
[600, 840)
|
手機橫屏,摺疊屏
|
|
lg
|
[840, +∞)
|
平板,2in1 設備
|
使用查詢結果
目前查詢的內容只在當前頁面可以使用,如果希望應用中任意位置都可以使用,咱們可以使用AppStorage 進行共享
:::success 核心步驟:
- 事件中通過 AppStorage.set(key,value)的方式保存當前斷點值
- 需要使用的位置通過 AppStorage 來獲取即可
:::
// 添加回調函數
this.listenerXS.on('change', (res: mediaquery.MediaQueryResult) => {
console.log('changeRes:', JSON.stringify(res))
if (res.matches == true) {
// this.breakpoint = 'xs'
AppStorage.set('breakpoint', 'xs')
}
})
// 組件中引入 AppStorage
@StorageProp('breakpoint') breakpoint: string = ''
// 在需要的位置使用 AppStorage 中保存的斷點值
Text(this.breakpoint)
媒體查詢斷點工具(瞭解)
:::success獲取斷點:
- 通過window事件獲取
- 通過媒體查詢獲取
實際項目中選擇一個使用即可
:::
import mediaQuery from '@ohos.mediaquery'
interface Breakpoint {
name: string
size: number
mediaQueryListener?: mediaQuery.MediaQueryListener
}
export const BreakpointKey: string = 'currentBreakpoint'
export class BreakpointSystem {
private currentBreakpoint: string = 'md'
private breakpoints: Breakpoint[] = [
{ name: 'xs', size: 0 }, { name: 'sm', size: 320 },
{ name: 'md', size: 600 }, { name: 'lg', size: 840 },
{ name: 'xl', size: 1440 }
]
public register() {
this.breakpoints.forEach((breakpoint: Breakpoint, index) => {
let condition: string
if (index === this.breakpoints.length - 1) {
condition = '(' + breakpoint.size + 'vp<=width' + ')'
} else {
condition = '(' + breakpoint.size + 'vp<=width<' + this.breakpoints[index + 1].size + 'vp)'
}
console.log(condition)
breakpoint.mediaQueryListener = mediaQuery.matchMediaSync(condition)
breakpoint.mediaQueryListener.on('change', (mediaQueryResult) => {
if (mediaQueryResult.matches) {
this.updateCurrentBreakpoint(breakpoint.name)
}
})
})
}
public unregister() {
this.breakpoints.forEach((breakpoint: Breakpoint) => {
if (breakpoint.mediaQueryListener) {
breakpoint.mediaQueryListener.off('change')
}
})
}
private updateCurrentBreakpoint(breakpoint: string) {
if (this.currentBreakpoint !== breakpoint) {
this.currentBreakpoint = breakpoint
AppStorage.Set<string>(BreakpointKey, this.currentBreakpoint)
console.log('on current breakpoint: ' + this.currentBreakpoint)
}
}
}
案例-電影列表-媒體查詢版本(自行完成)
使用剛剛學習的媒體查詢工具,結合斷點來完成一個案例效果
:::success需求:
- 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)
}
}
柵格佈局 Grid(面試會問)
創建網格、一多中的Grid
柵格組件的本質是:將組件劃分為有規律的多列,通過調整【不同斷點】下的【柵格組件的列數】,及【子組件所佔列數】實現不同佈局
:::color3通過調整總列數,及子組件所佔列數,實現不同佈局
:::
斷點:
|
斷點名稱 |
取值範圍(vp) |
設備 |
|
xs
|
[0, 320)
|
手錶等超小屏
|
|
sm
|
[320, 600)
|
手機豎屏
|
|
md
|
[600, 840)
|
手機橫屏,摺疊屏
|
|
lg
|
[840, 1440)
|
平板,2in1 設備
|
|
xl
|
(1440, +無窮
|
PC、摺疊PC展開態
|
比如:
參考柵格列數設置:
核心用法
// 行
GridRow(屬性){
// 列
GridCol(屬性){
}
}
測試代碼:
:::success優先級從上往下:
- 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%')
}
}
案例-標題欄與搜索欄
標題欄和搜索欄,在sm和md斷點下分兩行顯示,在lg斷點下單行顯示,可以通過柵格實現。在sm和md斷點下,標題欄和搜索欄佔滿12列,此時會自動換行顯示。在lg斷點下,標題欄佔8列而搜索欄佔4列,此時標題欄和搜索欄在同一行中顯示。
|
|
sm/md |
lg |
|
效果圖
|
||
|
柵格佈局圖
|
案例-登錄界面
結合咱們剛剛學習的 柵格佈局。來實現如下效果
:::success需求:
- 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 })
}
}
功能級一多開發(瞭解)
多功能設備開發
一個前提
功能開發的適配主要體現在需要適配不同範類的應用,比如既要適配手機和平板,也需要適配智能穿戴設備,如果是同泛類產品,系統能力一致,無需考慮多設備上應用功能開發的差異,我們的美寇商城需要適配的是手機和Pad,屬於同泛類產品,無需考慮功能開發的差異。以下是常見類型分類:
- 默認設備(一般為手機)、平板
- 車機、智慧屏
- 智能穿戴
什麼是系統能力
系統能力(即SystemCapability,縮寫為SysCap)指操作系統中每一個相對獨立的特性,如藍牙,WIFI,NFC,攝像頭等,都是系統能力之一。每個系統能力對應多個API,隨着目標設備是否支持該系統能力共同存在或消失。
:::color3比如: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);
}
:::warning注意:
- 目前的開發主要是 手機及平板開發,屬於統一範類,功能差別不大
- 目前 Harmony Next 的系統首發登錄的肯定是手機,其他設備會逐步接入
:::
工程級一多(掌握)
工程管理,分層架構設計
概念
一多模式下,官方推薦在開發過程中採用"三層工程架構",其實就是把項目拆分成不同類型的模塊,再通過模塊之間的引用組合,最終實現應用功能,拆分規範如下:
- commons(公共能力層):用於存放公共基礎能力合集,比如工具庫,公共配置等
- features(基礎特性層):用於存放應用中相對獨立的各個功能的UI以及業務邏輯實現
- products(產品定製層):用於針對不同設備形態進行功能和特性集成,作為應用入口
:::success參考官方示例:美蔻商城將進行如下拆分
:::
選擇合適的包類型
選擇合適的包類型
HAP、HAR、HSP三者的功能和使用場景總結對比如下:
|
Module類型 |
包類型 |
説明 |
|
Ability
|
HAP |
應用的功能模塊,可以獨立安裝和運行,必須包含一個entry類型的HAP,可選包含一個或多個feature類型的HAP。
|
|
Static Library
|
HAR |
靜態共享包,編譯態複用。
- 支持應用內共享,也可以發佈後供其他應用使用。
- 作為二方庫,發佈到OHPM私倉,供公司內部其他應用使用。 - 作為三方庫,發佈到OHPM中心倉,供其他應用使用。 - 多包(HAP/HSP)引用相同的HAR時,會造成多包間代碼和資源的重複拷貝,從而導致應用包膨大。
- 注意:編譯HAR時,建議開啓混淆能力,保護代碼資產。 |
|
Shared Library
|
HSP |
動態共享包,運行時複用。
- 當前僅支持應用內共享。
- 當多包(HAP/HSP)同時引用同一個共享包時,採用HSP替代HAR,可以避免HAR造成的多包間代碼和資源的重複拷貝,從而減小應用包大小。
|
:::success
- hap 可以作為入口,放到 Product
- hsp 多個地方應用不會增加容量消耗
- har 可以單獨發佈,如果要考慮單獨發佈的模塊,可以用 har
- 三層架構:
- hap+ n hsp