在 HarmonyOS(鸿蒙)“一次开发、多端部署” 的开发模式下,应用需要适配手机、平板、车机、折叠屏等不同设备,还要应对分屏、横竖屏切换、窗口缩放等动态场景。传统的固定布局或单一自适应属性已无法满足需求,而鸿蒙提供的MediaQuery(媒体查询)能力,可通过监听设备 / 应用的属性变化(如屏幕宽度、分辨率、显示模式),实现布局的 “动态响应式调整”,是解决多端适配和动态布局的核心方案。本文将基于实战代码,详解 MediaQuery 的核心用法、场景适配及最佳实践。

一、MediaQuery 核心价值:适配 “静态属性” 与 “动态变化”

鸿蒙 MediaQuery 的核心作用,就是为以下两类场景提供精准的布局适配能力:

  1. 静态属性适配:根据设备 / 应用的固定属性(如屏幕宽度、分辨率、深浅色模式),设计匹配的布局(比如手机显示单列列表,平板显示双列网格);
  2. 动态变化适配:当屏幕状态动态改变时(如分屏、横竖屏切换、折叠屏展开 / 收起),同步更新页面布局,保证用户体验一致性。

相较于传统的 “断点监听”(基于 UIAbility 监听窗口尺寸),MediaQuery 的优势在于:无需依赖应用生命周期,可在任意 UI 组件内直接使用,更轻量化、更聚焦 UI 层适配

二、核心代码解析:MediaQuery 基础用法

先看本文提供的核心代码(已做细节优化),这是 MediaQuery 的基础骨架,涵盖 “创建监听、响应变化、销毁监听” 全流程:

完整可运行代码

typescript

运行

// 1. 导入媒体查询核心模块
import { mediaquery } from '@kit.ArkUI';
import { AppStorage, Entry, Component, Column, Text, FlexAlign, FontWeight, StorageProp } from '@kit.ArkUI';

// 2. 定义标准化断点区间(避免重叠,鸿蒙官方推荐)
const BREAKPOINT_XS = '(0vp <= width < 320vp)'; // 超小屏(手表/折叠屏收起)
const BREAKPOINT_SM = '(320vp <= width < 600vp)'; // 小屏(手机)
const BREAKPOINT_MD = '(600vp <= width < 840vp)'; // 中屏(平板/折叠屏展开)
const BREAKPOINT_LG = '(840vp <= width)'; // 大屏(车机/智慧屏)

// 3. 创建媒体查询监听器(全局创建,避免重复初始化)
const listenerXS: mediaquery.MediaQueryListener = mediaquery.matchMediaSync(BREAKPOINT_XS);
const listenerSM: mediaquery.MediaQueryListener = mediaquery.matchMediaSync(BREAKPOINT_SM);
const listenerMD: mediaquery.MediaQueryListener = mediaquery.matchMediaSync(BREAKPOINT_MD);
const listenerLG: mediaquery.MediaQueryListener = mediaquery.matchMediaSync(BREAKPOINT_LG);

@Entry
@Component
struct Index {
  // 从AppStorage获取全局断点状态(自动响应变化)
  @StorageProp('currentBreakPoint') currentBreakPoint: string = 'sm';

  /**
   * 组件挂载时注册监听事件
   */
  aboutToAppear(): void {
    // 监听超小屏断点变化
    listenerXS.on('change', (res: mediaquery.MediaQueryResult) => {
      if (res.matches) { // matches为true表示当前满足该断点条件
        AppStorage.set('currentBreakPoint', 'xs');
        console.log('当前为超小屏(xs),宽度<320vp');
      }
    });

    // 监听小屏断点变化
    listenerSM.on('change', (res: mediaquery.MediaQueryResult) => {
      if (res.matches) {
        AppStorage.set('currentBreakPoint', 'sm');
        console.log('当前为小屏(sm),320vp≤宽度<600vp');
      }
    });

    // 监听中屏断点变化
    listenerMD.on('change', (res: mediaquery.MediaQueryResult) => {
      if (res.matches) {
        AppStorage.set('currentBreakPoint', 'md');
        console.log('当前为中屏(md),600vp≤宽度<840vp');
      }
    });

    // 监听大屏断点变化
    listenerLG.on('change', (res: mediaquery.MediaQueryResult) => {
      if (res.matches) {
        AppStorage.set('currentBreakPoint', 'lg');
        console.log('当前为大屏(lg),宽度≥840vp');
      }
    });
  }

  /**
   * 组件销毁时移除监听,避免内存泄露
   */
  aboutToDisappear(): void {
    listenerXS.off('change');
    listenerSM.off('change');
    listenerMD.off('change');
    listenerLG.off('change');
  }

  build() {
    Column() {
      // 实时显示当前断点
      Text(this.currentBreakPoint)
        .fontSize(50)
        .fontWeight(FontWeight.Bold);
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center);
  }
}

关键代码逐行解析

1. 模块导入与断点定义

typescript

运行

import { mediaquery } from '@kit.ArkUI';
// 标准化断点区间(无重叠,覆盖所有宽度)
const BREAKPOINT_XS = '(0vp <= width < 320vp)';
const BREAKPOINT_SM = '(320vp <= width < 600vp)';
const BREAKPOINT_MD = '(600vp <= width < 840vp)';
const BREAKPOINT_LG = '(840vp <= width)';
  • 核心模块mediaquery是鸿蒙 ArkUI 提供的媒体查询核心模块,封装了所有监听逻辑;
  • 断点规范:断点区间必须无重叠、无遗漏(原代码中listenerLG的区间是800vp<=width,与MD840vp重叠,已修正为840vp),避免同一宽度触发多个断点。
2. 创建媒体查询监听器

typescript

运行

const listenerXS = mediaquery.matchMediaSync(BREAKPOINT_XS);
  • matchMediaSync:同步创建媒体查询监听器,参数为 “查询条件”(支持width/height/orientation等属性);
  • 监听器全局创建:避免在组件内重复创建,减少性能开销。
3. 监听断点变化

typescript

运行

listenerXS.on('change', (res) => {
  if (res.matches) {
    AppStorage.set('currentBreakPoint', 'xs');
  }
});
  • change事件:当屏幕属性(如宽度)变化时触发;
  • res.matches:布尔值,表示当前是否满足该断点条件(比如宽度 < 320vp 时,listenerXSmatches为 true);
  • AppStorage.set:将断点值存入全局状态,供所有组件共享,实现 “一处变化、全局响应”。
4. 销毁监听(关键优化)

typescript

运行

aboutToDisappear(): void {
  listenerXS.off('change');
}

组件销毁时必须移除change监听,否则会导致内存泄露(监听器持有组件引用,无法被 GC 回收)。

三、实战场景:MediaQuery 适配静态 / 动态布局

MediaQuery 的核心价值体现在 “静态属性适配” 和 “动态变化适配” 两类场景,以下是结合这两类场景的实战示例:

场景 1:静态属性适配(不同设备显示不同布局)

需求

  • xs/sm(手表 / 手机):单列列表布局;
  • md(平板):双列网格布局;
  • lg(车机):三列卡片布局。

场景 2:动态变化适配(分屏 / 横竖屏切换)

需求

  • 手机竖屏(sm):单列列表;
  • 手机横屏(md):双列网格;
  • 分屏后宽度缩小(sm):自动切回单列。

完整实战代码

typescript

运行

import { mediaquery } from '@kit.ArkUI';
import { AppStorage, Entry, Component, Column, Text, FlexAlign, FontWeight, StorageProp, Grid, GridItem, Image, ForEach, Row } from '@kit.ArkUI';

// 定义断点
const BREAKPOINT_XS = '(0vp <= width < 320vp)';
const BREAKPOINT_SM = '(320vp <= width < 600vp)';
const BREAKPOINT_MD = '(600vp <= width < 840vp)';
const BREAKPOINT_LG = '(840vp <= width)';

// 创建监听器
const listenerXS = mediaquery.matchMediaSync(BREAKPOINT_XS);
const listenerSM = mediaquery.matchMediaSync(BREAKPOINT_SM);
const listenerMD = mediaquery.matchMediaSync(BREAKPOINT_MD);
const listenerLG = mediaquery.matchMediaSync(BREAKPOINT_LG);

@Entry
@Component
struct ResponsiveLayoutDemo {
  // 全局断点状态
  @StorageProp('currentBreakPoint') currentBreakPoint: string = 'sm';
  // 模拟商品数据
  private goodsList = Array.from({ length: 9 }, (_, i) => ({
    id: i + 1,
    name: `鸿蒙实战教程${i + 1}`,
    price: 99 + i,
    img: 'https://example.com/goods.jpg'
  }));

  // 根据断点返回网格列数
  private getColumnCount(): number {
    switch (this.currentBreakPoint) {
      case 'xs': return 1;
      case 'sm': return 1;
      case 'md': return 2;
      case 'lg': return 3;
      default: return 1;
    }
  }

  // 根据断点返回字体大小
  private getFontSize(): number {
    switch (this.currentBreakPoint) {
      case 'xs': return 12;
      case 'sm': return 14;
      case 'md': return 16;
      case 'lg': return 18;
      default: return 14;
    }
  }

  aboutToAppear(): void {
    // 注册所有断点监听
    [listenerXS, listenerSM, listenerMD, listenerLG].forEach((listener, index) => {
      const bp = ['xs', 'sm', 'md', 'lg'][index];
      listener.on('change', (res) => {
        if (res.matches) {
          AppStorage.set('currentBreakPoint', bp);
        }
      });
    });
  }

  aboutToDisappear(): void {
    // 移除所有监听
    [listenerXS, listenerSM, listenerMD, listenerLG].forEach(listener => {
      listener.off('change');
    });
  }

  build() {
    Column({ width: '100%', height: '100%', padding: 10, backgroundColor: '#f5f5f5' }) {
      // 断点提示
      Text(`当前断点:${this.currentBreakPoint}`)
        .fontSize(this.getFontSize() + 2)
        .fontWeight(FontWeight.Bold)
        .marginBottom(10);

      // 响应式网格布局
      Grid({
        columns: this.getColumnCount(), // 列数随断点变化
        columnSpacing: 10,
        rowSpacing: 10,
        width: '100%',
        flexGrow: 1
      }) {
        ForEach(this.goodsList, (item) => {
          GridItem() {
            Column({ width: '100%', padding: 8, backgroundColor: '#fff', borderRadius: 8 }) {
              // 商品图片(自适应宽高比)
              Image(item.img)
                .width('100%')
                .aspectRatio(1)
                .borderRadius(4);
              // 商品名称(字体随断点变化)
              Text(item.name)
                .fontSize(this.getFontSize())
                .marginTop(6)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis });
              // 商品价格
              Row({ justifyContent: FlexAlign.SpaceBetween, width: '100%', marginTop: 4 }) {
                Text(`¥${item.price}`)
                  .fontSize(this.getFontSize())
                  .fontColor('#ff4444');
                Text(`ID: ${item.id}`)
                  .fontSize(this.getFontSize() - 2)
                  .fontColor('#999');
              }
            }
          }
        });
      }
    }
  }
}

适配效果演示

场景 断点 布局效果
手表 / 折叠屏收起 xs 1 列列表,12 号字体
手机竖屏 sm 1 列列表,14 号字体
手机横屏 / 小平板 md 2 列网格,16 号字体
车机 / 智慧屏 lg 3 列网格,18 号字体
手机分屏(宽度缩小) sm 自动切回 1 列列表

四、MediaQuery 最佳实践

1. 断点区间必须 “无重叠、无遗漏”

  • 错误示例:lg断点设为800vp<=widthmd设为600vp<=width<840vp → 800-840vp 区间会同时匹配mdlg
  • 正确做法:断点区间首尾衔接(如xs: 0-320sm:320-600md:600-840lg:840+)。

2. 监听器全局创建,组件内仅注册事件

  • 避免在aboutToAppear内创建监听器(每次组件挂载都会新建,导致重复监听);
  • 推荐全局创建监听器,组件内仅注册 / 移除change事件。

3. 必做:组件销毁时移除监听

  • 未移除监听会导致内存泄露,尤其在页面频繁跳转的场景下,可能引发应用卡顿;
  • 推荐用数组批量管理监听器,简化移除逻辑(如示例中的forEach遍历移除)。

4. 结合 AppStorage 实现全局状态共享

  • 单个组件使用断点:用@State存储;
  • 多组件共享断点:用AppStorage + @StorageProp/@StorageLink,实现 “一处变化、全局更新”。

5. 避免过度监听(性能优化)

  • 仅监听核心属性(如width),避免监听resolutioncolorMode等高频变化的属性;
  • 断点变化后,仅更新必要的布局属性(如列数、字体大小),避免全量刷新。

五、MediaQuery vs 窗口监听(UIAbility)

很多开发者会混淆 MediaQuery 和 “UIAbility 监听窗口尺寸” 两种方式,这里做对比:

特性 MediaQuery(媒体查询) UIAbility 窗口监听
适用范围 UI 组件层(聚焦布局适配) 应用生命周期层(全局监听)
监听粒度 细粒度(可监听单个属性) 粗粒度(仅窗口尺寸)
使用复杂度 低(组件内直接使用) 中(需依赖 UIAbility)
性能开销
最佳场景 静态布局适配、动态布局切换 全局断点管理、跨页面适配

选型建议

  • 单个 / 少数组件适配:优先用 MediaQuery;
  • 全局统一断点管理:优先用 UIAbility 窗口监听;
  • 复杂场景:两者结合(UIAbility 管理全局断点,MediaQuery 处理局部组件适配)。

六、总结

鸿蒙 MediaQuery 媒体查询是实现响应式布局的 “轻量型利器”,核心要点可总结为:

  1. 核心能力:适配 “静态设备属性” 和 “动态屏幕变化” 两类场景,无需复杂逻辑即可实现布局自适应;
  2. 核心流程:创建监听器→注册 change 事件→响应断点变化→销毁监听,四步完成适配;
  3. 最佳实践:断点区间无重叠、监听器全局创建、组件销毁必移除监听、结合 AppStorage 共享状态;
  4. 选型原则:UI 层适配用 MediaQuery,全局断点管理用 UIAbility 窗口监听。

掌握 MediaQuery 后,你可轻松应对鸿蒙多端设备的布局适配,无论是手表、手机、平板还是车机,都能通过几行代码实现 “一套代码、全端适配”,最大化提升开发效率。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐