一次開發多端部署
1. 簡介
HarmonyOS系統面向多終端提供了“一次開發,多端部署”(後文中簡稱為“一多”)的能力,讓開發者可以基於一種設計,高效構建多端(手機、電腦、平板、手錶、車機等)可運行的應用。
一多能力可以從三個維度進行描述
● 頁面級一多:解決不同尺寸屏幕下UI適配問題。
● 功能級一多:解決不同設備上功能適配問題。
● 工程級一多:採用合理的工程結構使不同類型設備間最大程度的複用代碼。
本文主要針對“頁面級一多能力”進行講解。後續單獨講解功能級一多、工程級一多。
2. 佈局能力
佈局決定了頁面中的元素按照何種方式排布及顯示,是頁面設計及開發過程中首先需要考慮的問題。符合有“一多”能力的佈局可分為以下兩種。
● 自適應佈局:當外部容器大小發生變化時,元素可以根據相對關係自動變化以適應外部容器變化的佈局能力。相對關係如佔比、固定寬高比、顯示優先級等。
● 響應式佈局:當外部容器大小發生變化時,元素可以根據斷點、柵格或特定的特徵(如屏幕方向、窗口寬高等)自動變化以適應外部容器變化的佈局能力。
💁 説明:自適應佈局多用於解決頁面各區域內的佈局差異,響應式佈局多用於解決頁面各區域間的佈局差異。
3. 自適應佈局
針對常見的開發場景,方舟開發框架提煉了七種自適應佈局能力,這些佈局可以獨立使用,也可多種佈局疊加使用。
3.1 拉伸能力
//Blank()填充空白區域,達到拉伸效果
Row() {
Text('飛行模式')
.fontSize(16)
.width(135)
.height(22)
.fontWeight(FontWeight.Medium)
.lineHeight(22)
Blank() // 通過Blank組件實現拉伸能力
Toggle({ type: ToggleType.Switch })
.width(36)
.height(20)
}
.width("100%")
.height(55)
.borderRadius(12)
.padding({ left: 13, right: 13 })
.backgroundColor('#FFFFFF')
3.2 均分能力
均分能力是指容器組件尺寸發生變化時,增加或減小的空間均勻分配給容器組件內所有空白區域
● .flexShrink(1) 配置了此屬性的子組件,按照比例收縮,分配父容器的不足空間。
● .flexGrow(1)配置了此屬性的子組件,按照比例拉伸,分配父容器的多餘空間。
//均勻分配父容器主軸方向的剩餘空間
Row(){
ForEach([1,2,3,4],(item:number,index:number)=>{
Row().width(100).height(100).backgroundColor(index%2===0?Color.Orange:Color.Pink)
.flexShrink(1) //配置了此屬性的子組件,按照比例收縮,分配父容器的不足空間。
.flexGrow(1) //配置了此屬性的子組件,按照比例拉伸,分配父容器的多餘空間。
})
}.width("100%").justifyContent(FlexAlign.SpaceEvenly)
3.3 佔比能力
佔比能力是指子組件的寬高按照預設的比例,隨父容器組件發生變化。佔比能力通常有兩種實現方式:
● 將子組件的寬高設置為父組件寬高的百分比
● 通過layoutWeight屬性配置互為兄弟關係的組件在父容器主軸方向的佈局權重
//子組件的寬度,按照比例佔滿父組件剩餘空間;
Row({ space: 10 }) {
Row().width(100).height(100).backgroundColor(Color.Brown)
.layoutWeight(1)
Row().width(100).height(100).backgroundColor(Color.Pink)
.layoutWeight(2)
Row().width(100).height(100).backgroundColor(Color.Orange)
.layoutWeight(1)
}.width("100%").justifyContent(FlexAlign.SpaceEvenly)
3.4 縮放能力
縮放能力是指子組件的寬高按照預設的比例,隨容器組件發生變化,且變化過程中子組件的寬高比不變。
● 使用百分比佈局配合固定寬高比(aspectRatio屬性)實現當容器尺寸發生變化時,內容自適應調整。
Row() {
Column() {
Column() {
Row().width("100%").height("100%").backgroundColor(Color.Orange)
}.aspectRatio(1) //固定寬高比為1,即:width/height = 1
.border({ width: 2, color: Color.Black })
}
.width("100%")
.height(100)
.backgroundColor(Color.White)
.justifyContent(FlexAlign.Center)
}
.width("100%")
.justifyContent(FlexAlign.SpaceEvenly)
3.5 延伸能力
延伸能力是指容器組件內的子組件,按照其在列表中的先後順序,隨容器組件尺寸變化顯示或隱藏。它可以根據顯示區域的尺寸,顯示不同數量的元素。
延伸能力通常有兩種實現方式:
● 通過List組件實現。
● 通過Scroll組件配合Row組件或Column組件實現。
List、Row或Column組件中子節點的在頁面顯示時就已經全部完成了佈局計算及渲染,只不過受限於父容器尺寸,用户只能看到一部分。隨着父容器尺寸增大,用户可以看到的子節點數目也相應的增加。用户還可以通過手指滑動觸發列表滑動,查看被隱藏的子節點。
List({space:10}){
ForEach(Array.of(1,2,3,4,5,6,7,8,9,10),(item:number,index:number)=>{
ListItem(){
Text(`item# ${index}`).width(100).height(100).backgroundColor(Color.Orange)
}
})
}.listDirection(Axis.Horizontal)
.width("100%")
.height("100%")
3.6 隱藏能力
隱藏能力是指容器組件內的子組件,按照其預設的顯示優先級,隨容器組件尺寸變化顯示或隱藏,其中相同顯示優先級的子組件同時顯示或隱藏。
● 隱藏能力通過設置佈局優先級(displayPriority屬性)來控制顯隱,當佈局主軸方向剩餘尺寸不足以滿足全部元素時,按照佈局優先級大小,從小到大依次隱藏,直到容器能夠完整顯示剩餘元素。具有相同佈局優先級的元素將同時顯示或者隱藏。
Row({space:10}){
Text("1").width(150).height(100).backgroundColor(Color.Orange).textAlign(TextAlign.Center)
.displayPriority(1)
Text("2").width(150).height(100).backgroundColor(Color.Pink).textAlign(TextAlign.Center)
.displayPriority(2)
Text("3").width(150).height(100).backgroundColor(Color.Gray).textAlign(TextAlign.Center)
.displayPriority(3)
Text("2").width(150).height(100).backgroundColor(Color.Yellow).textAlign(TextAlign.Center)
.displayPriority(2)
Text("1").width(150).height(100).backgroundColor(Color.Brown).textAlign(TextAlign.Center)
.displayPriority(1)
}
.width("100%")
.height(100)
.backgroundColor(Color.White)
.justifyContent(FlexAlign.Center)
3.7 折行能力
折行能力是指容器組件尺寸發生變化,當佈局方向尺寸不足以顯示完整內容時自動換行。它常用於橫豎屏適配或默認設備向平板切換的場景。
● 折行能力通過使用 Flex折行佈局 (將wrap屬性設置為FlexWrap.Wrap)實現,當橫向佈局尺寸不足以完整顯示內容元素時,通過折行的方式,將元素顯示在下方。
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center,
wrap: FlexWrap.Wrap,
space:{
cross:LengthMetrics.vp(10),
main:LengthMetrics.vp(10)
}
}){
Row().width(100).height(100).backgroundColor(Color.Orange)
Row().width(100).height(100).backgroundColor(Color.Pink)
Row().width(100).height(100).backgroundColor(Color.Yellow)
Row().width(100).height(100).backgroundColor(Color.Pink)
Row().width(100).height(100).backgroundColor(Color.Yellow)
Row().width(100).height(100).backgroundColor(Color.Orange)
}
.width("100%")
.backgroundColor(Color.White)
.padding(10)
4. 響應式佈局
‼️ 自適應佈局的侷限性
自適應佈局可以保證窗口尺寸在一定範圍內變化時,頁面的顯示是正常的。但是將窗口尺寸變化較大時(如窗口寬度從400vp變化為1000vp),僅僅依靠自適應佈局可能出現圖片異常放大或頁面內容稀疏、留白過多等問題。
- 小屏幕的拉伸(留白適中)
- 大屏幕的拉伸(留白合理)
🔔 響應式佈局簡介
由於自適應佈局能力有限,無法適應較大的頁面尺寸調整,此時就需要藉助響應式佈局能力調整頁面結構。響應式佈局中最常使用的特徵是,可以將窗口寬度劃分為不同的斷點,當窗口寬度從一個斷點變化到另一個斷點時,改變頁面佈局以獲得更好的顯示效果。
4.1 斷點
斷點是將應用窗口在寬度維度上分成了幾個不同的區間(即不同的斷點),在不同的區間下,開發者可根據需要實現不同的頁面佈局效果。具體的斷點如下所示。
説明
● 開發者可以根據實際使用場景決定適配哪些斷點。如xs斷點對應的一般是智能穿戴類設備,如果確定某頁面不會在智能穿戴設備上顯示,則可以不適配xs斷點。
● 可以根據實際需要在lg斷點後面新增xl、xxl等斷點,但注意新增斷點會同時增加UX設計師及應用開發者的工作量,除非必要否則不建議盲目新增斷點。
4.2 監聽斷點變化方式
理解了斷點含義之後,還有一件事情非常重要就是要監聽斷點的變化,判斷應用當前處於何種斷點,進而可以調整應用的佈局。
常見的監聽斷點變化的方法如下所示:
● 獲取窗口對象並監聽窗口尺寸變化
● 通過媒體查詢監聽應用窗口尺寸變化
● 藉助柵格組件能力監聽不同斷點的變化
4.2.1 窗口對象監聽斷點變化
- 在UIAbility的onWindowStageCreate生命週期回調中,通過窗口對象獲取啓動時的應用窗口寬度並註冊回調函數監聽窗口尺寸變化。將窗口尺寸的長度單位由px換算為vp後,即可基於前文中介紹的規則得到當前斷點值,此時可以使用狀態變量記錄當前的斷點值方便後續使用。
// MainAbility.ts
import { window, display } from '@kit.ArkUI';
import { UIAbility } from '@kit.AbilityKit';
export default class MainAbility extends UIAbility {
private windowObj?: window.Window;
private curBp: string = '';
//...
// 根據當前窗口尺寸更新斷點
private updateBreakpoint(windowWidth: number) :void{
// 將長度的單位由px換算為vp
let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels;
let newBp: string = '';
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;
// 使用狀態變量記錄當前斷點值
AppStorage.setOrCreate('currentBreakpoint', this.curBp);
}
}
onWindowStageCreate(windowStage: window.WindowStage) :void{
windowStage.getMainWindow().then((windowObj) => {
this.windowObj = windowObj;
// 獲取應用啓動時的窗口尺寸
this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width);
// 註冊回調函數,監聽窗口尺寸變化
windowObj.on('windowSizeChange', (windowSize)=>{
this.updateBreakpoint(windowSize.width);
})
});
// ...
}
//...
}
a. 在頁面中,獲取及使用當前的斷點。
@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%')
}
}
b. 運行及驗證效果。
4.2.2 媒體查詢監聽斷點變化
媒體查詢提供了豐富的媒體特徵監聽能力,可以監聽應用顯示區域變化、橫豎屏、深淺色、設備類型等等,因此在應用開發過程中使用的非常廣泛。
本小節僅介紹媒體查詢跟斷點的結合,即如何藉助媒體查詢能力,監聽斷點的變化。
1.對通過媒體查詢監聽斷點的功能做簡單的封裝,方便後續使用
// common/breakpointsystem.ets
import { mediaquery } from '@kit.ArkUI';
export type BreakpointType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
export interface Breakpoint {
name: BreakpointType;
size: number;
mediaQueryListener?: mediaquery.MediaQueryListener;
}
export class BreakpointSystem {
private static instance: BreakpointSystem;
private readonly breakpoints: Breakpoint[] = [
{ name: 'xs', size: 0 },
{ name: 'sm', size: 320 },
{ name: 'md', size: 600 },
{ name: 'lg', size: 840 }
]
private states: Set<BreakpointState<Object>>;
private constructor() {
this.states = new Set();
}
public static getInstance(): BreakpointSystem {
if (!BreakpointSystem.instance) {
BreakpointSystem.instance = new BreakpointSystem();
}
return BreakpointSystem.instance;
}
public attach(state: BreakpointState<Object>): void {
this.states.add(state);
}
public detach(state: BreakpointState<Object>): void {
this.states.delete(state);
}
public start() {
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)`;
}
breakpoint.mediaQueryListener = mediaquery.matchMediaSync(condition);
if (breakpoint.mediaQueryListener.matches) {
this.updateAllState(breakpoint.name);
}
breakpoint.mediaQueryListener.on('change', (mediaQueryResult) => {
if (mediaQueryResult.matches) {
this.updateAllState(breakpoint.name);
}
})
})
}
private updateAllState(type: BreakpointType): void {
this.states.forEach(state => state.update(type));
}
public stop() {
this.breakpoints.forEach((breakpoint: Breakpoint, index) => {
if (breakpoint.mediaQueryListener) {
breakpoint.mediaQueryListener.off('change');
}
})
this.states.clear();
}
}
export interface BreakpointOptions<T> {
xs?: T;
sm?: T;
md?: T;
lg?: T;
xl?: T;
xxl?: T;
}
export class BreakpointState<T extends Object> {
public value: T | undefined = undefined;
private options: BreakpointOptions<T>;
constructor(options: BreakpointOptions<T>) {
this.options = options;
}
static of<T extends Object>(options: BreakpointOptions<T>): BreakpointState<T> {
return new BreakpointState(options);
}
public update(type: BreakpointType): void {
if (type === 'xs') {
this.value = this.options.xs;
} else if (type === 'sm') {
this.value = this.options.sm;
} else if (type === 'md') {
this.value = this.options.md;
} else if (type === 'lg') {
this.value = this.options.lg;
} else if (type === 'xl') {
this.value = this.options.xl;
} else if (type === 'xxl') {
this.value = this.options.xxl;
} else {
this.value = undefined;
}
}
}
- 在頁面中,通過媒體查詢,監聽應用窗口寬度變化,獲取當前應用所處的斷點值。
// MediaQuerySample.ets
import { BreakpointSystem, BreakpointState } from '../common/breakpointsystem';
@Entry
@Component
struct MediaQuerySample {
@State compStr: BreakpointState<string> = BreakpointState.of({ sm: "sm", md: "md", lg: "lg" });
@State compImg: BreakpointState<Resource> = BreakpointState.of({
sm: $r('app.media.sm'),
md: $r('app.media.md'),
lg: $r('app.media.lg')
});
aboutToAppear() {
BreakpointSystem.getInstance().attach(this.compStr);
BreakpointSystem.getInstance().attach(this.compImg);
BreakpointSystem.getInstance().start();
}
aboutToDisappear() {
BreakpointSystem.getInstance().detach(this.compStr);
BreakpointSystem.getInstance().detach(this.compImg);
BreakpointSystem.getInstance().stop();
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Column()
.height(100)
.width(100)
.backgroundImage(this.compImg.value)
.backgroundImagePosition(Alignment.Center)
.backgroundImageSize(ImageSize.Contain)
Text(this.compStr.value)
.fontSize(24)
.margin(10)
}
.width('100%')
.height('100%')
}
}
4.3 柵格佈局監聽斷點變化
柵格佈局基於屏幕寬度將界面劃分為若干等寬列,通過控制元素橫跨的列數實現精準佈局,並能在不同斷點下動態調整元素佔比,確保響應式適配。
柵格佈局默認將屏幕寬度劃分為12個等寬列,在不同的斷點下,元素所佔列數不同,則可以有如下不同的顯示效果。
● 在sm斷點下,每一個元素佔3列,則可形成4個柵格
● 在md斷點下:每一個元素佔2列,則可形成6個柵格
4.3.1 柵格組件介紹
● GridRow: 表示柵格容器組件
● GridCol: 必須使用在GridRow容器內,表示一個柵格子組件
4.3.2 默認柵格列數
柵格系統的總列數可以使用默認值(12列),也可以自己指定列數,還可以根據屏幕的寬度動態調整列數。
默認柵格列數。
@Entry
@Component
struct Index {
@State items: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]
build() {
GridRow() {
ForEach(this.items,(item:number)=>{
GridCol() {
Row() {
Text(`${item}`)
}
.width('100%')
.height(50)
.border({ width: 1, color: Color.Black, style: BorderStyle.Solid })
.justifyContent(FlexAlign.Center)
}
})
}.height(300).backgroundColor(Color.Pink)
}
}
4.3.3 指定柵格列數
通過GridRow{columns:6}參數可以指定柵格總列數。
● 比如下面案例中,柵格總列數為6,一共24個柵格,那麼一行就是6個,一共4行;超過一行的部分自動換行。
@Entry
@Component
struct Index {
@State items: 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]
build() {
//指定柵格容器的最大列數
GridRow({ columns: 6 }) {
ForEach(this.items, (item: number) => {
GridCol() {
Row() {
Text(`${item}`)
}
.width('100%')
.height(50)
.border({ width: 1, color: Color.Black, style: BorderStyle.Solid })
.justifyContent(FlexAlign.Center)
}
})
}.backgroundColor(Color.Pink)
}
}
4.3.4 動態柵格列數
為了適應不同屏幕尺寸下的佈局,柵格系統的總列數可以根據不同的屏幕尺寸動態調整。不同屏幕尺寸的設備,依靠“斷點”進行區分,根據斷點的不同動態調整柵格列數。
如下代碼:根據斷點設備設置柵格總列數
import { List } from '@kit.ArkTS'
@Entry
@Component
struct Index {
@State items: 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]
build() {
GridRow({
//設置屏幕寬度各斷點區間值
breakpoints: {
value: ['320vp', '520vp', '840vp', '1080vp', '1920vp']
},
//設置對應斷點所佔列數
columns: {
xs: 3, //最小寬度型設備3列
sm: 6, //小寬度設備6列
md: 8, //中型寬度設備8列
lg: 12 //大型寬度設備12列
},
}) {
ForEach(this.items, (item: number) => {
GridCol() {
Row() {
Text(`${item}`)
}
.width('100%')
.height(50)
.border({ width: 1, color: Color.Black, style: BorderStyle.Solid })
.justifyContent(FlexAlign.Center)
}
})
}.backgroundColor(Color.Pink)
}
}
4.3.5 設置柵格間距
柵格的樣式由Margin、Gutter、Columns三個屬性決定。
● Margin是相對應用窗口、父容器的左右邊緣的距離,決定了內容可展示的整體寬度。
● Gutter是相鄰的兩個Column之間的距離,決定內容間的緊密程度。
● Columns是柵格中的列數,其數值決定了內容的佈局複雜度。
單個柵格的寬度是系統結合Margin、Gutter和Columns自動計算的,不需要也不允許開發者手動配置。
- 通過GridRow {gutter: 10}參數可以調整柵格子之間的間距,默認為0。
GridRow({
gutter:10, //指定柵格間距
columns:{ //指定柵格列數
xs:3,
sm:6,
md:9,
lg:12
}
})
4.3.6 設置柵格佔用列數
● 通過設置GridCol{span:3}來設置柵格佔用的列數,GridRow採用默認列數12列;
○ 在xs斷點時:一個柵格元素佔12列,一行可容納1個柵格
○ 在sm斷點時,一個柵格元素佔6列,一行可容納2個柵格
○ 在md斷點時:一個柵格元素佔4列,一行可容納3個柵格
○ 在lg斷點時:一個柵格元素佔3列,一行可容納4個柵格
import { List } from '@kit.ArkTS'
@Entry
@Component
struct Index {
@State items: 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]
//當前斷點
@State currentBreakPoint: string = "sm"
build() {
GridRow({
gutter: 10
}) {
ForEach(this.items, (item: number, index: number) => {
GridCol() {
Row() {
Text(`${this.currentBreakPoint} #${item}`)
}
.width('100%')
.height(50)
.border({ width: 1, color: Color.Black, style: BorderStyle.Solid })
.justifyContent(FlexAlign.Center)
}.span({
xs: 12,
sm: 6,
md: 4,
lg: 3
})
})
}.backgroundColor(Color.Pink)
.padding(10)
//監聽斷點變化
.onBreakpointChange((breakpoints: string) => {
this.currentBreakPoint = breakpoints
})
}
}