鸿蒙原生 ArkTS 布局方式之 MediaQuery 媒体查询:设备特征检测

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

HarmonyOS NEXT 需要运行在手机、平板、折叠屏、2 合 1 笔记本等多种设备上。如何用一套代码优雅地适配形态各异的设备,是每个鸿蒙开发者必须掌握的技能。MediaQuery(媒体查询) 正是解决这一问题的核心技术,它允许开发者根据设备类型、屏幕尺寸、分辨率、深色模式等特征动态调整 UI 布局。本文基于一个完整的演示应用,深入剖析 ArkTS 中媒体查询的实现原理和最佳实践。


二、核心概念与 API

2.1 什么是媒体查询?

媒体查询是一种响应式设计技术,通过检测设备或运行环境的特征,动态应用不同的样式和布局规则。鸿蒙 ArkTS 提供了 mediaquery 模块实现这一能力。

2.2 核心 API

API 作用
mediaquery.matchMediaSync(condition) 创建媒体查询监听器,返回 MediaQueryListener
listener.on('change', callback) 订阅查询结果变化,callback 接收 MediaQueryResult
listener.off('change') 取消订阅,页面销毁时务必调用

2.3 查询条件语法

条件字符串遵循类似 CSS 的格式:(属性: 值)(属性: 值) and (属性: 值)

支持的条件类型:

  • 设备类型phonetablet2in1wearablecartv
  • 屏幕方向portraitlandscape
  • 屏幕宽度min-widthmax-width(单位 vp
  • 深色模式dark-mode: true
  • 分辨率resolution: 123

三、应用场景与架构

演示应用 「MediaQuery 媒体查询演示」 实现了一个完整的设备特征检测面板,可实时展示设备特征,并根据屏幕宽度动态调整卡片列表的列数。

3.1 页面布局

┌─────────────────────────────────┐
│  📱 MediaQuery 媒体查询演示      │  ← 标题区
├─────────────────────────────────┤
│  📊 当前设备特征                 │
│  ┌──────────┬──────────┐       │
│  │ 设备类型 │ 屏幕方向 │       │  ← 特征总览网格
│  ├──────────┼──────────┤       │    (2列 × 3行)
│  │ 断点等级 │  分辨率  │       │
│  ├──────────┼──────────┤       │
│  │ 深色模式 │  状态    │       │
│  └──────────┴──────────┘       │
├─────────────────────────────────┤
│  🎯 动态布局演示(列数随断点变化)│  ← 1~4 列
├─────────────────────────────────┤
│  📋 当前生效的查询条件           │  ← 实时列表
└─────────────────────────────────┘

3.2 技术选型

技术 用途
@Entry @Component 声明页面入口和组件
@State 响应式状态,驱动 UI 自动更新
Grid / GridItem 网格布局,动态控制列数
Scroll 可滚动容器
@Builder 可复用 UI 片段
try-catch 异常保护,防止白屏

四、核心实现详解

4.1 导入模块

import { mediaquery } from '@kit.ArkUI';

在 HarmonyOS NEXT(API 12+)中,媒体查询 API 归属于 @kit.ArkUI 套件。

4.2 定义状态变量

@State private deviceType: string = '检测中...';
@State private orientation: string = '检测中...';
@State private breakPoint: string = 'sm (小屏)';
@State private resolution: number = 0;
@State private isDark: boolean = false;
@State private activeQueries: string[] = [];

关键设计:所有 @State 变量都必须提供默认值。这样即使查询全部失败,页面也能正常渲染而非白屏。

4.3 安全注册监听器

aboutToAppear(): void {
  try {
    this.initListeners();
  } catch (e) {
    console.error('[MQ] init failed: ' + e);
  }
}

addListener(cond: string, cb: (m: boolean) => void): void {
  try {
    const l = mediaquery.matchMediaSync(cond);
    l.on('change', (r: mediaquery.MediaQueryResult) => {
      cb(r.matches);
      this.toggleQuery(cond, r.matches);
    });
    this.listeners.push(l);
  } catch (e) {
    console.error('[MQ] skip condition: ' + cond);
  }
}

安全模式aboutToAppear 用外层 try-catch 兜底,每个 addListener 也有独立 try-catch。某一条查询条件不被支持时,仅跳过该条件,不影响其他条件。

4.4 释放资源

aboutToDisappear(): void {
  try {
    this.listeners.forEach((l) => l.off('change'));
    this.listeners.splice(0);
  } catch (e) {
    console.error('[MQ] dispose error');
  }
}

必须执行! 每个 matchMediaSync 创建的监听器都必须在页面销毁时调用 off('change'),否则可能导致内存泄漏甚至崩溃。


五、五大设备特征检测详解

5.1 设备类型

this.addListener('(device-type: phone)', (m) => { if (m) this.deviceType = 'Phone' });
this.addListener('(device-type: tablet)', (m) => { if (m) this.deviceType = 'Tablet' });
this.addListener('(device-type: 2in1)', (m) => { if (m) this.deviceType = '2in1' });
this.addListener('(device-type: wearable)', (m) => { if (m) this.deviceType = 'Wearable' });

同时注册多种设备类型,每台设备只会命中一种。注意:不要使用 (device-type: unknown),该条件在某些 API 版本中不被支持。

5.2 屏幕方向

this.addListener('(orientation: landscape)', (m) => { if (m) this.orientation = '横屏' });
this.addListener('(orientation: portrait)', (m) => { if (m) this.orientation = '竖屏' });

最常见的媒体查询场景之一。用户旋转设备时特征卡片实时更新。

5.3 宽度断点(五级体系)

this.addListener('(max-width: 319vp)', (m) => { if (m) this.breakPoint = 'xs' });
this.addListener('(min-width: 320vp) and (max-width: 599vp)', (m) => { if (m) this.breakPoint = 'sm' });
this.addListener('(min-width: 600vp) and (max-width: 839vp)', (m) => { if (m) this.breakPoint = 'md' });
this.addListener('(min-width: 840vp) and (max-width: 1279vp)', (m) => { if (m) this.breakPoint = 'lg' });
this.addListener('(min-width: 1280vp)', (m) => { if (m) this.breakPoint = 'xl' });

参考 HarmonyOS 响应式布局规范:

断点 宽度 典型设备
xs < 320vp 穿戴设备
sm 320~599vp 手机竖屏
md 600~839vp 平板竖屏
lg 840~1279vp 平板横屏
xl ≥ 1280vp 桌面大屏

5.4 深色模式

this.addListener('(dark-mode: true)', (m) => { this.isDark = m; });

build() 中根据 isDark 动态选择颜色方案:

.backgroundColor(this.isDark ? '#16213E' : '#F0F2F5')
.fontColor(this.isDark ? '#FFFFFF' : '#1A1A2E')

无需额外的 CSS 文件或主题切换逻辑。

5.5 分辨率

this.addListener('(resolution: 1)', (m) => { if (m) this.resolution = 1; });
this.addListener('(resolution: 2)', (m) => { if (m) this.resolution = 2; });
this.addListener('(resolution: 3)', (m) => { if (m) this.resolution = 3; });

注意:某些 API 版本不支持小数值(如 1.53.5),建议仅使用整数值。


六、动态布局:列数随断点变化

6.1 核心逻辑

colCount(): number {
  const b = this.breakPoint;
  if (b.startsWith('xs') || b.startsWith('sm')) return 1;
  if (b.startsWith('md')) return 2;
  if (b.startsWith('lg')) return 3;
  if (b.startsWith('xl')) return 4;
  return 2;
}

colTpl(): string {
  const n = this.colCount();
  let s = '';
  for (let i = 0; i < n; i++) {
    if (i > 0) s += ' ';
    s += '1fr';
  }
  return s;
}

colCount() 返回列数,colTpl() 转为 columnsTemplate 格式(如 "1fr 1fr 1fr")。

6.2 断点与列数映射

断点 列数 适用场景
xs / sm 1列 手机竖屏单手操作
md 2列 平板竖屏
lg 3列 平板横屏
xl 4列 桌面大屏

6.3 在 Grid 中使用

Grid() {
  GridItem() { this.demoCard('📌 MediaQuery', 'matchMediaSync 创建', '#FF6B6B', this.isDark) }
  // ... 更多卡片
}
.columnsTemplate(this.colTpl())  // ← 动态绑定

当断点变化时,@State breakPoint 更新触发重渲染,colTpl() 返回新模板,网格自动调整列数。


七、@Builder 复用 UI

7.1 特征单元格

@Builder
featCell(label: string, value: string, icon: string, dark: boolean) {
  Column({ space: 6 }) {
    Text(icon).fontSize(26)
    Text(label).fontSize(12).fontColor(dark ? '#AAAAAA' : '#888888')
    Text(value).fontSize(13)
      .fontColor(dark ? '#FFFFFF' : '#1A1A2E')
      .textAlign(TextAlign.Center).maxLines(2)
  }
  .width('100%').height('100%')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
  .backgroundColor(dark ? '#1A1A3E' : '#FFFFFF')
  .borderRadius(12)
  .shadow({ radius: 4, ... })
}

设计要点:颜色值通过参数 dark 传入,@Builder 变成纯函数,不依赖外部状态。

7.2 演示卡片

@Builder
demoCard(title: string, desc: string, color: string, dark: boolean) {
  Column({ space: 6 }) {
    Row().width('100%').height(50).backgroundColor(color)
      .borderRadius({ topLeft: 12, topRight: 12 })
    Text(title).fontSize(15).fontWeight(FontWeight.Bold)
      .fontColor(dark ? '#FFFFFF' : '#1A1A2E')
    Text(desc).fontSize(12)
      .fontColor(dark ? '#AAAAAA' : '#888888')
      .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
  }
  .backgroundColor(dark ? '#1A1A3E' : '#FFFFFF')
  .borderRadius(12)
  .shadow({ radius: 4, ... })
}

八、常见错误与排错指南

8.1 白屏

最常见错误。排查步骤:

  1. 页面注册:检查 main_pages.json 是否包含页面路径
  2. 查询条件兼容性:某些条件(如 (device-type: unknown)(round-screen: true)(resolution: 1.5))不被支持时会抛异常
  3. 添加 try-catch:每个 matchMediaSyncaboutToAppear 都要用 try-catch 保护

8.2 @Styles 与 @Builder 区分

@Styles @Builder
参数 ❌ 不支持 ✅ 支持
用途 封装样式链 封装 UI 片段
调用 直接使用 this.xxx()

@Styles 函数不能接受参数,参数化 UI 必须用 @Builder

8.3 资源泄漏

页面销毁时务必调用 listener.off('change'),否则监听器会持续回调已销毁的组件。


九、运行效果

  1. 首次加载:页面正常渲染,特征显示"检测中…"
  2. 初始化完成(< 1秒):监听器首次回调,特征值更新
  3. 旋转设备:方向卡片实时变化
  4. 切换深色模式:背景从浅灰变深蓝,文字卡片自动适配
  5. 调整窗口大小:断点变化,下方卡片列数 1→2→3→4 动态调整
  6. 底部列表:实时展示所有匹配的查询条件

十、总结

核心技术点

  • mediaquery.matchMediaSync():创建监听器
  • on('change'):订阅结果变化,驱动 UI 更新
  • @State:将查询结果与 UI 绑定
  • @Builder:封装可复用 UI
  • Grid.columnsTemplate 动态绑定:根据断点切换列布局

设计原则

  1. 安全优先:每个可能抛异常的操作都用 try-catch 保护
  2. 默认值兜底:所有 @State 有合理的初始值
  3. 资源必释放aboutToDisappear 中统一取消订阅
  4. 参数化复用@Builder 通过参数接收配置,不依赖组件内部状态

适用场景

  • 多设备适配:手机 / 平板 / 折叠屏 / 2合1
  • 横竖屏切换:视频播放、阅读器
  • 深色模式适配:自动跟随系统
  • 响应式布局:基于宽度断点的动态调整

MediaQuery 是一把利器,掌握它将使你的鸿蒙应用真正实现"一次开发,多端部署"。

Logo

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

更多推荐