【学习目标】
- 掌握 媒体查询的引入与Stage模型标准使用流程,理解监听句柄与生命周期管理
- 吃透 媒体查询完整语法规则:媒体类型、逻辑操作符、核心媒体特征
- 实现 横竖屏切换、深浅色模式跟随、多设备适配 三大高频响应式场景
- 封装 可跨页面复用的媒体查询工具类,一行代码实现状态监听
一、工程目录结构
text
MediaQueryDemo/
├── entry/src/main/ets/
│ ├── utils/
│ │ └── MediaQueryUtil.ets // 媒体查询复用工具类
│ └── pages/
│ └── Index.ets // 综合示例与工具测试
└── resources/ // 工程原生资源目录
二、媒体查询核心基础
2.1 什么是媒体查询
媒体查询是鸿蒙响应式设计的核心能力,它可以根据设备的固有属性(设备类型、屏幕尺寸)、应用的实时状态(可绘制宽高、横竖屏方向)、系统配置(深浅色模式),动态修改应用的布局与样式,实现「一套代码,多设备适配」。
2.2 两大核心应用场景
- 静态适配:针对不同设备类型(手机/平板/车机/穿戴),预设匹配的布局规则
- 动态响应:监听设备状态实时变化(横竖屏切换、深浅色切换、分屏),同步更新页面布局
2.3 使用步骤
步骤1:导入媒体查询模块
javascript
import { mediaquery } from '@kit.ArkUI';
步骤2:创建查询条件,获取监听句柄
通过 getUIContext().getMediaQuery().matchMediaSync() 接口设置查询条件,获取监听句柄 MediaQueryListener。
javascript
private listener: mediaquery.MediaQueryListener | null = null;
aboutToAppear() {
// 必须在aboutToAppear中初始化,确保UIContext已就绪
const mediaQueryListener = this.getUIContext().getMediaQuery().matchMediaSync('(orientation: landscape)');
}
步骤3:绑定回调函数,监听状态变化
给监听句柄绑定 on('change') 回调,当媒体特征发生变化时,会自动触发回调,通过 mediaQueryResult.matches 判断是否匹配查询条件。
javascript
aboutToAppear() {
// 必须在aboutToAppear中初始化 监听屏幕方向
const mediaQueryListener = this.getUIContext().getMediaQuery().matchMediaSync('(orientation: landscape)');
this.listener = mediaQueryListener
// 初始化时手动触发一次,获取初始状态
this.onOrientationChange(mediaQueryListener)
// 绑定回调
this.listener.on('change', (result: mediaquery.MediaQueryResult) => {
this.onOrientationChange(result);
});
}
onOrientationChange(mediaQueryResult: mediaquery.MediaQueryResult) {
if (mediaQueryResult.matches) {
console.info('当前为横屏状态');
// 横屏布局逻辑
} else {
console.info('当前为竖屏状态');
// 竖屏布局逻辑
}
}
步骤4:页面销毁时解绑回调,避免内存泄漏
必须在 aboutToDisappear 生命周期中解绑已注册的回调函数,否则会造成内存泄漏。
javascript
aboutToDisappear() {
if (this.listener) {
this.listener.off('change');
this.listener = null;
}
}
2.3 媒体查询完整语法规则
媒体查询条件由三部分组成,语法格式如下:
[媒体类型] [逻辑操作符] [(媒体特征)]
媒体类型
| 类型 | 说明 | 使用规范 |
|---|---|---|
screen |
按屏幕相关参数进行媒体查询 | 唯一常用类型,省略时默认使用,必须写在查询条件开头 |
逻辑操作符
| 操作符 | 逻辑 | 说明 | 示例 |
|---|---|---|---|
and |
与 | 所有条件同时满足时成立 | screen and (orientation: landscape) and (width > 600vp) |
or / , |
或 | 任一条件满足时成立 | (max-height: 1000vp) or (round-screen: true) |
not |
非 | 对查询结果取反 | not screen and (min-width: 600vp) |
>= / <= / > / < |
范围 | 用于数值类特征的范围判断 | (width >= 600vp) |
核心媒体特征
宽高类特征支持 vp 和 px 单位,无单位时默认使用 px,开发中推荐使用vp单位。
| 特征 | 说明 | 可选值/示例 |
|---|---|---|
width / height |
应用页面可绘制区域的宽/高(实时更新) | (width: 360vp) / (height > 600vp) |
orientation |
屏幕横竖屏方向 | portrait(竖屏) / landscape(横屏) |
dark-mode |
系统深浅色模式 | true(深色模式) / false(浅色模式) |
device-type |
设备类型 | phone / tablet / tv / car / wearable / 2in1 |
三、媒体查询工具类封装
将核心能力封装成单例工具类,实现一行代码完成状态监听,统一管理生命周期,避免内存泄漏。
工具类代码(utils/MediaQueryUtil.ets)
javascript
import { mediaquery } from '@kit.ArkUI';
class InternalKey {
static readonly BREAKPOINT: string = 'internal_bp';
static readonly ORIENTATION: string = 'internal_ori';
static readonly DARK_MODE: string = 'internal_dark';
static readonly DEVICE_TYPE: string = 'internal_device';
static readonly ROUND_SCREEN: string = 'internal_round';
}
/** 栅格断点类型(支持6个断点) */
export type GridBreakpointType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
/** 设备类型 */
export type DeviceType = 'default' | 'phone' | 'tablet' | 'tv' | 'car' | 'wearable' | '2in1';
/** 屏幕方向类型 */
export type OrientationType = 'portrait' | 'landscape';
/** 媒体状态接口 */
export interface MediaFullState {
/** 栅格断点 */
breakpoint: GridBreakpointType;
/** 屏幕方向 */
orientation: OrientationType;
/** 是否深色模式 */
isDarkMode: boolean;
/** 设备类型 */
deviceType: DeviceType;
/** 是否圆形屏幕 */
isRoundScreen: boolean;
}
/** 监听项内部管理接口 */
interface MediaQueryListenerItem {
/** 监听句柄 */
listener: mediaquery.MediaQueryListener;
/** 绑定的回调函数 */
callback: (result: mediaquery.MediaQueryResult) => void;
}
// ==================== 媒体查询核心工具类 ====================
export class MediaQueryUtil {
/** 单例实例 */
private static instance: MediaQueryUtil | null = null;
/** UI上下文 */
private uiContext?: UIContext;
/** 监听句柄管理Map */
private listenerMap: Map<string, MediaQueryListenerItem> = new Map();
/** 默认栅格断点 */
private static readonly DEFAULT_BREAKPOINTS: number[] = [320, 600, 840, 1440, 1600];
/** 私有构造函数,禁止外部new实例 */
private constructor() {}
/**
* 获取单例实例
* @returns 工具类全局唯一实例
*/
static getInstance(): MediaQueryUtil {
if (!MediaQueryUtil.instance) {
MediaQueryUtil.instance = new MediaQueryUtil();
}
return MediaQueryUtil.instance;
}
/**
* 初始化工具类
* @param uiContext 应用上下文
* @param customBreakpoints 自定义栅格断点
*/
init(uiContext: UIContext, customBreakpoints?: number[]): void {
if (this.uiContext) return;
this.uiContext = uiContext;
if (customBreakpoints?.length) {
MediaQueryUtil.DEFAULT_BREAKPOINTS.splice(0, MediaQueryUtil.DEFAULT_BREAKPOINTS.length, ...customBreakpoints);
}
}
// ==================== 核心内部方法 ====================
/**
* 注册监听
*/
private register(
key: string,
condition: string,
onChange: (isMatch: boolean) => void
): void {
if (!this.uiContext) {
throw new Error('MediaQueryUtil 未初始化,请在应用启动时调用 init(uiContext)');
}
this.removeListener(key);
const listener = this.uiContext.getMediaQuery().matchMediaSync(condition);
const callback = (result: mediaquery.MediaQueryResult) => {
onChange(!!result.matches);
};
listener.on('change', callback);
this.listenerMap.set(key, { listener, callback });
onChange(!!listener.matches);
}
/**
* 移除单个监听
*/
private removeListener(key: string): void {
const item = this.listenerMap.get(key);
if (item) {
item.listener.off('change', item.callback);
this.listenerMap.delete(key);
}
}
// ==================== 对外:取消单个监听 ====================
/** 取消断点监听 */
offBreakpointChange(): void {
this.removeListener(InternalKey.BREAKPOINT);
}
/** 取消横竖屏监听 */
offOrientationChange(): void {
this.removeListener(InternalKey.ORIENTATION);
}
/** 取消深色模式监听 */
offDarkModeChange(): void {
this.removeListener(InternalKey.DARK_MODE);
}
/** 取消设备类型监听 */
offDeviceTypeChange(): void {
this.removeListener(InternalKey.DEVICE_TYPE);
}
/** 取消圆形屏幕监听 */
offRoundScreenChange(): void {
this.removeListener(InternalKey.ROUND_SCREEN);
}
// ==================== 对外:移除所有监听 ====================
removeAllListeners(): void {
this.listenerMap.forEach(item => {
item.listener.off('change', item.callback);
});
this.listenerMap.clear();
}
/** 销毁工具类 */
destroy(): void {
this.removeAllListeners();
this.uiContext = undefined;
MediaQueryUtil.instance = null;
}
/** 监听栅格断点变化 */
onBreakpointChange(onChange: (breakpoint: GridBreakpointType) => void): void {
const update = () => {
const mq = this.uiContext!.getMediaQuery();
const bps = MediaQueryUtil.DEFAULT_BREAKPOINTS;
let bp: GridBreakpointType = 'xs';
if (mq.matchMediaSync(`(width >= ${bps[4]}vp)`).matches) bp = 'xxl';
else if (mq.matchMediaSync(`(width >= ${bps[3]}vp)`).matches) bp = 'xl';
else if (mq.matchMediaSync(`(width >= ${bps[2]}vp)`).matches) bp = 'lg';
else if (mq.matchMediaSync(`(width >= ${bps[1]}vp)`).matches) bp = 'md';
else if (mq.matchMediaSync(`(width >= ${bps[0]}vp)`).matches) bp = 'sm';
onChange(bp);
};
this.register(InternalKey.BREAKPOINT, '(width >= 0vp)', update);
}
/** 监听横竖屏 */
onOrientationChange(onChange: (ori: OrientationType) => void): void {
this.register(InternalKey.ORIENTATION, '(orientation: portrait), (orientation: landscape)', () => {
const isLand = this.uiContext!.getMediaQuery().matchMediaSync('(orientation: landscape)').matches;
onChange(isLand ? 'landscape' : 'portrait');
});
}
/** 监听深色模式 */
onDarkModeChange(onChange: (isDark: boolean) => void): void {
this.register(InternalKey.DARK_MODE, '(dark-mode: true)', onChange);
}
/** 监听设备类型 */
onDeviceTypeChange(onChange: (type: DeviceType) => void): void {
const update = () => {
const mq = this.uiContext!.getMediaQuery();
const types: DeviceType[] = ['phone', 'tablet', 'tv', 'car', 'wearable', '2in1'];
for (const t of types) {
if (mq.matchMediaSync(`(device-type: ${t})`).matches) {
onChange(t);
return;
}
}
onChange('default');
};
this.register(InternalKey.DEVICE_TYPE, '(device-type: phone)', update);
}
/** 全量监听 */
onFullStateChange(onChange: (state: MediaFullState) => void): void {
let fullState: MediaFullState = {
breakpoint: 'xs',
orientation: 'portrait',
isDarkMode: false,
deviceType: 'default',
isRoundScreen: false
};
this.onBreakpointChange((bp) => {
fullState.breakpoint = bp;
onChange(fullState);
});
this.onOrientationChange((ori) => {
fullState.orientation = ori;
onChange(fullState);
});
this.onDarkModeChange((dark) => {
fullState.isDarkMode = dark;
onChange(fullState);
});
this.onDeviceTypeChange((device) => {
fullState.deviceType = device;
onChange(fullState);
});
this.register(
InternalKey.ROUND_SCREEN,
'(round-screen: true)',
(round) => {
fullState.isRoundScreen = round;
onChange(fullState);
}
);
}
/** 取消全量监听 */
offFullStateChange(): void {
this.offBreakpointChange();
this.offOrientationChange();
this.offDarkModeChange();
this.offDeviceTypeChange();
this.offRoundScreenChange();
}
}
四、综合示例测试功能
完整代码(pages/Index.ets)
javascript
import { MediaQueryUtil, MediaFullState } from '../utils/MediaQueryUtil';
@Entry
@Component
struct Index {
// 【响应式状态】
@State isLandscape: boolean = false;
@State isDarkMode: boolean = false;
@State currentBreakpoint: string = 'xs';
@State currentDeviceType: string = 'default';
@State isRoundScreen: boolean = false;
// 【样式参数】
@State pageBgColor: string = '#F5F5F5';
@State cardBgColor: string = '#FFFFFF';
@State textColor: string = '#000000';
aboutToAppear(): void {
this.initMediaQuery();
}
build() {
Column() {
// 标题
Text('媒体查询工具类演示')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(this.textColor)
.margin({ top: 40, bottom: 30 })
// 状态信息卡片
Column({ space: 12 }) {
Text(`当前断点:${this.currentBreakpoint}`)
.fontSize(18)
.fontColor(this.textColor)
Text(`横竖屏:${this.isLandscape ? '横屏 Landscape' : '竖屏 Portrait'}`)
.fontSize(18)
.fontColor(this.textColor)
Text(`深浅色模式:${this.isDarkMode ? '深色 Dark' : '浅色 Light'}`)
.fontSize(18)
.fontColor(this.textColor)
Text(`设备类型:${this.currentDeviceType}`)
.fontSize(18)
.fontColor(this.textColor)
Text(`是否圆形屏幕:${this.isRoundScreen ? '是' : '否'}`)
.fontSize(18)
.fontColor(this.textColor)
}
.width('90%')
.padding(20)
.borderRadius(16)
.backgroundColor(this.cardBgColor)
.shadow({ radius: 8, color: '#00000010', offsetY: 4 })
// 自适应网格布局示例
Text('响应式网格示例')
.fontSize(22)
.fontWeight(FontWeight.Medium)
.fontColor(this.textColor)
.alignSelf(ItemAlign.Start)
.margin({ left: '5%', top: 25, bottom: 15 })
GridRow({ columns: this.currentBreakpoint === 'xs' ? 2 : 4, gutter: { x: 15, y: 15 } }) {
ForEach([1, 2, 3, 4, 5, 6, 7, 8], (id: number) => {
GridCol() {
Column() {
Text(`卡片 ${id}`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(this.textColor)
Text('99.00元')
.fontSize(14)
.fontColor('#FF3B30')
}
.width('100%')
.aspectRatio(0.9)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.cardBgColor)
.borderRadius(12)
.shadow({ radius: 5, color: '#00000008', offsetY: 2 })
}
})
}
.width('90%')
}
.width('100%')
.height('100%')
.backgroundColor(this.pageBgColor)
}
/**
* 初始化媒体查询监听
* 封装成独立函数,逻辑更清晰
*/
private initMediaQuery(): void {
const mediaUtil = MediaQueryUtil.getInstance();
mediaUtil.init(this.getUIContext());
mediaUtil.onFullStateChange((state: MediaFullState) => {
this.isLandscape = state.orientation === 'landscape';
this.isDarkMode = state.isDarkMode;
this.currentBreakpoint = state.breakpoint;
this.currentDeviceType = state.deviceType;
this.isRoundScreen = state.isRoundScreen;
this.updateTheme(state.isDarkMode);
});
}
/**
* 更新主题配色
* @param isDark 是否深色模式
*/
private updateTheme(isDark: boolean): void {
if (isDark) {
this.pageBgColor = '#121212';
this.cardBgColor = '#1E1E1E';
this.textColor = '#FFFFFF';
} else {
this.pageBgColor = '#F5F5F5';
this.cardBgColor = '#FFFFFF';
this.textColor = '#000000';
}
}
/**
* 页面销毁时解绑
* 防止内存泄漏
*/
aboutToDisappear(): void {
MediaQueryUtil.getInstance().removeAllListeners();
}
}
测试结果

五、内容总结
- 核心作用:媒体查询是鸿蒙响应式布局的核心,实现「一套代码,多设备适配」。
- 标准流程:导入模块 → aboutToAppear中创建监听 → 绑定change回调 → aboutToDisappear中解绑回调。
- 语法规则:查询条件由「媒体类型 + 逻辑操作符 + 媒体特征」组成,宽高优先使用vp单位。
- 工具类封装:单例模式统一管理监听,一行代码实现状态监听,彻底避免内存泄漏。
- 综合测试:在一个页面中同时测试横竖屏、深浅色、多设备适配三大高频场景。
六、代码仓库
- 工程名称:MediaQueryDemo
- 仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git
七、下节预告
下一节我们将正式学习 鸿蒙响应式布局核心:栅格布局 (GridRow/GridCol),彻底搞定多设备布局的标准化方案:
- 掌握 GridRow 栅格容器的断点规则、总列数配置、排列方向与间距设置
- 吃透 GridCol 栅格子组件的 span(占用列数)、offset(偏移列数)、order(排序)三大核心属性
- 实现 手机/平板/折叠屏 多设备自适应布局,一套代码适配全尺寸设备
- 理解 栅格组件的嵌套使用,完成复杂页面的响应式布局