最近同事在iPhone上开发的一个页面,到我手里的鸿蒙手机上就出了不少问题:按钮被底部安全区遮住一半、文字在小屏上挤成一团、横屏布局完全错乱。这些问题归根结底是没有考虑不同设备的差异。Flutter的MediaQuery是解决这类问题的核心工具,它能从context中获取当前设备的媒体信息,让代码根据环境做响应式适配。本文结合HarmonyOS 6.0和nova12u真机测试,分享MediaQuery的实战用法与常见坑点。
MediaQuery核心数据
MediaQuery是一个继承自InheritedWidget的组件,通过`MediaQuery.of(context)`返回当前设备的媒体信息。我这里习惯在每个页面的build方法第一行就缓存到变量中:- final mq = MediaQuery.of(context);
复制代码 主要包含以下数据:
- size:屏幕逻辑尺寸(逻辑像素,非物理像素)
- devicePixelRatio:设备像素比
- textScaler:用户设定的文字缩放系数
- orientation:屏幕方向(portrait / landscape)
- platformBrightness:亮度模式(light / dark)
- padding:系统UI占用区域(状态栏、刘海、底部导航条)
- viewInsets:键盘弹出时覆盖的区域
屏幕尺寸与像素比
`mq.size`返回的是逻辑像素(points),单位是px(Flutter中实际是逻辑像素,并非Android的dp)。例如nova12u的逻辑尺寸约393x852,设备像素比为3.0,因此物理分辨率为1179x2556。常用代码:- final double width = mq.size.width;
- final double height = mq.size.height;
- final double dpr = mq.devicePixelRatio;
- final double physicalWidth = width * dpr;
复制代码 注意:size不受键盘弹出影响,键盘弹出时只有viewInsets变化。
方向适配
通过`mq.orientation`判断横竖屏,在响应式布局中调整组件高度:- final isLandscape = mq.orientation == Orientation.landscape;
- Container(
- height: isLandscape ? 120 : 200,
- child: Center(child: Text(isLandscape ? '横屏模式' : '竖屏模式')),
- );
复制代码 需要留意的是,Android和鸿蒙上用户可能锁定了屏幕方向,此时orientation返回的是实际屏幕方向(而非传感器数据)。比如用户锁定竖屏后,即使物理上横持手机,orientation依然是portrait。
文字缩放处理
`mq.textScaler`会读取用户系统设置的字体缩放系数,默认1.0,设置为“特大”时可达1.3以上。如果不做处理,固定高度的按钮可能文字溢出。我的原则:正文内容让系统缩放生效(尊重用户设置);按钮文字和标签用FittedBox或限制maxLines防止溢出;图标和固定尺寸组件用textScaler限制最大缩放:- Text(
- '显示效果',
- style: TextStyle(fontSize: mq.textScaler.scale(14)), // 手动应用缩放
- );
复制代码 注意:`textScaler`影响性能——如果在大widget树中大量使用它做动态计算,文字缩放变化时会触发大面积重建。建议只在高风险组件(如固定高度的容器)中使用。
亮度模式与深色适配
通过`mq.platformBrightness`判断系统深色/浅色模式:- final isDark = mq.platformBrightness == Brightness.dark;
- Container(
- color: isDark ? Colors.grey[900] : Colors.grey[100],
- );
复制代码 更好的做法是将颜色统一放入Theme中,通过`Theme.of(context).colorScheme`获取,而不是在构建方法中到处判断。
边距信息:padding vs viewInsets
容易混淆的是两个属性:
- `mq.padding`:系统UI区域(状态栏、导航条、刘海缺口)
- `mq.viewInsets`:动态覆盖区域(主要是键盘弹出时覆盖的区域)
- `mq.viewPadding`:系统UI区域加上键盘弹出时的变化
SafeArea组件内部实际上就是使用了MediaQuery.padding。键盘弹出时调整输入框位置,应使用viewInsets:- padding: EdgeInsets.only(bottom: mq.viewInsets.bottom),
复制代码 但直接使用会出现输入框被硬顶的情况,建议配合AnimatedPadding或addListener监听键盘变化做平滑移动。
响应式断点适配
利用`mq.size.width`实现简单断点布局:- if (mq.size.width < 360) {
- // 小屏手机:紧凑纵向堆叠
- } else if (mq.size.width < 600) {
- // 主流手机:正常排布
- } else {
- // 平板/大屏:多列横向布局
- }
复制代码 注意:断点阈值需根据设计稿调整,这里只是通用参考。
避坑指南
1. MediaQuery.of(context)必须在MaterialApp内使用:如果在MaterialApp之前调用,会报“MediaQuery.of() called with a context that does not contain a MediaQuery”错误。因为MediaQuery是由MaterialApp注入到widget树中的。
2. 避免多次调用:如果一个build方法里多次调用MediaQuery.of(context),建议只调用一次并缓存到变量,既提高可读性又略微提升性能。
3. 键盘弹出时size不变:Flutter的size是逻辑屏幕尺寸,不受键盘弹出影响。如果需要避开键盘,使用viewInsets而不是size。
4. textScaler影响性能:在大量widget中使用textScaler进行动态计算会导致大面积重建,应限制使用范围。
与鸿蒙ArkTS媒体查询的对比
在HarmonyOS ArkTS中,获取屏幕信息的API为:- import { window } from '@kit.ArkUI';
- let windowClass = window.getLastWindow(getContext());
- let properties = windowClass.getWindowProperties();
- let width = properties.windowRect.width;
- let height = properties.windowRect.height;
- let isDark = properties.isDarkMode;
复制代码 ArkTS通过`window.getWindowProperties()`获取窗口信息,需要先获取窗口实例;Flutter的MediaQuery直接从context中读取InheritedWidget的值,更贴合组件树写法。深色模式判断上,ArkTS用布尔值`isDarkMode`,而Flutter用`Brightness`枚举(light/dark),本质相同。
总结:MediaQuery是Flutter响应式布局的基石,无论是判断屏幕尺寸做断点适配、获取安全区避开刘海,还是监听键盘弹出调整布局,都离不开它。建议页面build方法第一行就缓存`final mq = MediaQuery.of(context);`。当然,MediaQuery只负责“获取信息”,真正的响应式布局还需要配合LayoutBuilder、Flexible、Expanded等组件一起使用。文中代码均在HarmonyOS 6.0 · nova12u真机上验证通过。 |