HarmonyOS响应式布局之断点和媒体查询简单了解

569 阅读4分钟

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. 针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。
  2. 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。

媒体查询使用步骤:

// 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')
      }
    })
  }
}

核心用法:

  1. 导入 BreakpointSystem
  2. 实例化 BreakpointSystem
  3. aboutToAppear中注册监听事件 aboutToDisappear中移除监听事件
  4. 通过 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 列