‼️ 自适应布局的局限性

自适应布局可以保证窗口尺寸在一定范围内变化时,页面的显示是正常的。但是将窗口尺寸变化较大时(如窗口宽度从400vp变化为1000vp),仅仅依靠自适应布局可能出现图片异常放大或页面内容稀疏、留白过多等问题。

如下图所示,大屏设备上采用拉伸的自适应布局,就会导致留白过大的问题。
在这里插入图片描述
为了解决上述问题,响应式布局应运而生。

🔔 响应式布局简介

由于自适应布局能力有限,无法适应较大的页面尺寸调整,此时就需要借助响应式布局能力调整页面结构。

响应式布局中最常使用的特征是,可以将窗口宽度划分为不同的断点,当窗口宽度从一个断点变化到另一个断点时,改变页面布局以获得更好的显示效果。

🎯 断点

断点是将应用窗口在宽度维度上分成了几个不同的区间(即不同的断点),在不同的区间下,开发者可根据需要实现不同的页面布局效果。具体的断点如下所示。

断点名称 取值范围(vp)
xs [0, 320)
sm [320, 600)
md [600, 840)
lg [840, +∞)

说明

  • 开发者可以根据实际使用场景决定适配哪些断点。如xs断点对应的一般是智能穿戴类设备,如果确定某页面不会在智能穿戴设备上显示,则可以不适配xs断点。
  • 可以根据实际需要在lg断点后面新增xl、xxl等断点,但注意新增断点会同时增加UX设计师及应用开发者的工作量,除非必要否则不建议盲目新增断点。

🔍 监听断点变化方式

理解了断点含义之后,还有一件事情非常重要就是要监听断点的变化,判断应用当前处于何种断点,进而可以调整应用的布局。
常见的监听断点变化的方法如下所示:
● 获取窗口对象并监听窗口尺寸变化
● 通过媒体查询监听应用窗口尺寸变化
● 借助栅格组件能力监听不同断点的变化

🔴 窗口对象监听断点变化

  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);
      })
    });
   // ...
  }
    
  //...
}
  1. 在页面中,获取及使用当前的断点。
@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%')
  }
}
  1. 运行及验证效果。
** ** ** ** ** **

🟠 媒体查询监听断点变化

媒体查询提供了丰富的媒体特征监听能力,可以监听应用显示区域变化、横竖屏、深浅色、设备类型等等,因此在应用开发过程中使用的非常广泛。

本小节仅介绍媒体查询跟断点的结合,即如何借助媒体查询能力,监听断点的变化。

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;
    }
  }
}

2.在页面中,通过媒体查询,监听应用窗口宽度变化,获取当前应用所处的断点值。

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

🟢 栅格布局监听断点变化

栅格布局基于屏幕宽度将界面划分为若干等宽列,通过控制元素横跨的列数实现精准布局,并能在不同断点下动态调整元素占比,确保响应式适配。

栅格布局默认将屏幕宽度划分为12个等宽列,在不同的断点下,元素所占列数,则可以有如下不同的显示效果。

  • 在sm断点下,每一个元素占3列,则可形成4个栅格
  • 在md断点下:每一个元素占2列,则可形成6个栅格
sm断点 md断点

栅格组件介绍
  • GridRow: 表示栅格容器组件
  • GridCol: 必须使用在GridRow容器内,表示一个栅格子组件

默认栅格列数

栅格系统的总列数可以使用默认值(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)
  }
}
指定栅格列数

通过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)
  }
}
动态栅格列数

为了适应不同屏幕尺寸下的布局,栅格系统的总列数可以根据不同的屏幕尺寸动态调整。不同屏幕尺寸的设备,依靠“断点”进行区分,根据断点的不同动态调整栅格列数。

断点名称 取值范围(vp) 设备描述
xs [0, 320) 最小宽度类型设备
sm [320, 520) 小宽度类型设备
md [520, 840) 中等宽度类型设备
lg [840, 1080) 大宽度类型设备
xl [1080,1920) 特大宽度类型设备
xxl [1920,+♾) 超大宽度类型设备

如下代码:根据断点设备设置栅格总列数

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)
  }
}
设置栅格样式

栅格的样式由Margin、Gutter、Columns三个属性决定。

  • Margin是相对应用窗口、父容器的左右边缘的距离,决定了内容可展示的整体宽度。
  • Gutter是相邻的两个Column之间的距离,决定内容间的紧密程度。
  • Columns是栅格中的列数,其数值决定了内容的布局复杂度。

单个Column的宽度是系统结合Margin、Gutter和Columns自动计算的,不需要也不允许开发者手动配置。

  • 通过GridRow {gutter: 10}参数可以调整栅格子之间的间距,默认为0。
  • 通过设置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
    })
  }
}

若有收获,就点个赞吧

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐