HarmonyOS Next极力宣传的能力一次开发多端部署,即一套代码工程,一次开发上架,多端按需部署。为了适配各种尺寸的智能终端,ArkUI采用了响应式布局方式,响应式布局是指页面内的元素可以根据特定的特征(如窗口宽度、屏幕方向等)自动变化以适应外部容器变化的布局能力。断点和媒体查询是官方提供的实现方案,对于开发者来说也是实现一多项目的基础。
断点
概念:断点以应用窗口宽度为切入点,将应用窗口在宽度维度上分成了几个不同的区间即不同的断点,在不同的区间下,开发者可根据需要实现不同的页面布局效果。参考文档
断点名称 | 取值范围( vp ) | 设备 |
---|---|---|
xs | [0, 320) | 手表等超小屏 |
sm | [320, 600) | 手机竖屏 |
md | [600, 840) | 手机横屏,折叠屏 |
lg | [840, +∞) | 平板,2in1 设备 |
通过判断应用当前处于何种断点,进而可以调整应用的布局,一般通过系统提供的window.WindowStage
窗口对象监听窗口尺寸变化。
在项目中的EntryAbility
文件中的onWindowStageCreate
添加监听窗口事件
import display from '@ohos.display'
import UIAbility from '@ohos.app.ability.UIAbility'
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)
})
});
// ...
}
//...
}
在UI页面中可以通过@StorageProp('currentBreakpoint') curBp: string = ''
来获取,从而通过条件渲染等方式来调整UI布局
媒体查询
概念:相比于通过窗口对象监听尺寸变化,媒体查询的功能会更为强大。媒体查询作为响应式设计的核心,在移动设备上应用十分广泛。媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。媒体查询常用于下面两种场景:参考文档
- 针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。
- 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。
媒体查询使用步骤:
// 1. 导入 模块
import { mediaquery } from '@kit.ArkUI';
@Entry
@Component
struct TestPage {
listenerXS: mediaquery.MediaQueryListener | null = null
listenerSM: mediaquery.MediaQueryListener | null = null
aboutToAppear(): void {
// 2. 创建监听器
this.listenerXS = mediaquery.matchMediaSync('(0vp<=width<320vp)');
this.listenerSM = mediaquery.matchMediaSync('(320vp<=width<600vp)');
// 3. 注册监听器
this.listenerXS.on('change', (res: mediaquery.MediaQueryResult) => {
console.log('changeRes:', JSON.stringify(res))
// 执行逻辑
})
this.listenerSM.on('change', (res: mediaquery.MediaQueryResult) => {
console.log('changeRes:', JSON.stringify(res))
// 执行逻辑
})
}
// 4. 移除监听器
// 即将销毁
aboutToDisappear(): void {
// 移除监听 避免性能浪费
this.listenerXS?.off('change')
this.listenerSM?.off('change')
}
build() {
Column() {
}
.height('100%')
.width('100%')
}
}
将上述代码导入项目中,打开DevEco-Studio工具中的预览器,拉动预览器屏幕大小来模拟设备宽度实时变化。在日志中可以观察媒体查询返回值。
核心APImediaquery.matchMediaSync(condition: string): MediaQueryListener
condition
参数是监听句柄,有自己的语法。参考文档
案例中查询的是屏幕宽度,也是项目开发中最常用的一种,官方提供了封装工具类,配合断点使用极大的方便了开发者。参考文档
import mediaQuery from '@ohos.mediaquery'
declare interface BreakPointTypeOption<T> {
xs?: T
sm?: T
md?: T
lg?: T
xl?: T
xxl?: 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 if (currentBreakPoint === 'xl') {
return this.options.xl
} else if (currentBreakPoint === 'xxl') {
return this.options.xxl
} else {
return undefined
}
}
}
interface Breakpoint {
name: string
size: number
mediaQueryListener?: mediaQuery.MediaQueryListener
}
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 }
]
private updateCurrentBreakpoint(breakpoint: string) {
if (this.currentBreakpoint !== breakpoint) {
this.currentBreakpoint = breakpoint
AppStorage.Set<string>('currentBreakpoint', this.currentBreakpoint)
console.log('on current breakpoint: ' + this.currentBreakpoint)
}
}
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')
}
})
}
}
核心用法:
- 导入 BreakpointSystem
- 实例化 BreakpointSystem
- aboutToAppear中注册监听事件 aboutToDisappear中移除监听事件
- 通过 AppStorage,结合 获取断点值即可
案例:
import { BreakPointType, BreakpointSystem, BreakPointKey } from '../../common/BreakPointsystem'
interface MovieItem {
title: string
img: ResourceStr
}
@Entry
@Component
struct BreakPointDemoPage {
items: MovieItem[] = [
{ title: '标题1', img: $r('app.media.image1') },
{ title: '标题2', img: $r('app.media.image2') },
{ title: '标题3', img: $r('app.media.image3') },
{ title: '标题4', img: $r('app.media.image4') },
{ title: '标题5', img: $r('app.media.image5') },
{ title: '标题6', img: $r('app.media.image6') },
{ title: '标题7', img: $r('app.media.image7') },
{ title: '标题8', img: $r('app.media.image8') },
{ title: '标题9', img: $r('app.media.image9') },
{ title: '标题10', img: $r('app.media.image10') },
]
breakpointSystem: BreakpointSystem = new BreakpointSystem()
@StorageProp(BreakPointKey) currentBreakpoint: string = 'sm'
aboutToAppear() {
this.breakpointSystem.register()
}
aboutToDisappear() {
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)
}
}
还是用预览器模拟不同宽度打开可以实现
- xs 及 sm 2 列
- md:3 列
- lg:4 列