MediaQuery(媒体查询)
MediaQuery 是 Flutter 中获取设备信息的核心工具——屏幕多大、像素密度多少、键盘是否弹出、系统是深色还是浅色……这些信息都通过它获取。
核心理解
MediaQuery 的工作方式类似于 CSS 的 @media 查询,但更灵活——它不仅提供信息,还能监听变化(如屏幕旋转、键盘弹出)。
// 获取 MediaQuery 数据
final mediaQuery = MediaQuery.of(context);性能提示
MediaQuery.of(context) 会监听所有媒体数据的变化。如果你只需要某个属性(如 size),当键盘弹出导致 viewInsets 变化时,你的 Widget 也会重建。如果需要优化性能,可以使用 MediaQuery.sizeOf(context) 等专用方法(Flutter 3.8+)。
常用属性一览
| 属性 | 返回类型 | 说明 | 典型值 |
|---|---|---|---|
size | Size | 屏幕逻辑尺寸 | Size(375, 812) |
size.width | double | 屏幕宽度 | 375、390、414 |
size.height | double | 屏幕高度 | 812、844、896 |
devicePixelRatio | double | 设备像素比 | 2.0、3.0 |
textScaleFactor | double | 系统字体缩放倍率(旧 API) | 1.0 ~ 2.0 |
textScaler | TextScaler | 系统字体缩放(新 API,Flutter 3.16+) | TextScaler.linear(1.0) |
platformBrightness | Brightness | 系统亮度模式 | Brightness.light / dark |
orientation | Orientation | 屏幕方向 | portrait / landscape |
padding | EdgeInsets | 安全区内边距 | top: 47~59, bottom: 34 |
viewPadding | EdgeInsets | 不受键盘影响的安全区内边距 | 同 padding |
viewInsets | EdgeInsets | 被临时 UI 遮挡的区域(键盘) | bottom: 0~336 |
alwaysUse24HourFormat | bool | 是否 24 小时制 | — |
shortestSide | double | 宽高中较短的一边 | — |
final mq = MediaQuery.of(context);
mq.size // Size(375.0, 812.0)
mq.devicePixelRatio // 3.0
mq.platformBrightness // Brightness.light
mq.padding.top // 59.0(刘海屏)
mq.viewInsets.bottom // 0.0(键盘未弹出)判断屏幕大小与响应式布局
屏幕类型判断
根据屏幕宽度判断设备类型,这是响应式布局的基础:
import 'package:flutter/material.dart';
class ScreenTypePage extends StatelessWidget {
const ScreenTypePage({super.key});
@override
Widget build(BuildContext context) {
// ─── ★ 获取屏幕宽度 ──────────────
final width = MediaQuery.of(context).size.width;
// ─── ☆ 获取屏幕宽度 ──────────────
String deviceType;
if (width < 600) {
deviceType = '手机';
} else if (width < 1024) {
deviceType = '平板';
} else {
deviceType = '桌面';
}
return Scaffold(
appBar: AppBar(title: const Text('屏幕类型判断')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('屏幕宽度: ${width.toStringAsFixed(0)}'),
Text('设备类型: $deviceType', style: const TextStyle(fontSize: 24)),
],
),
),
);
}
}用最短边更可靠
使用 shortestSide(宽高中的较小值)判断,可以避免横竖屏切换导致布局跳变:
final shortestSide = MediaQuery.of(context).size.shortestSide;
if (shortestSide < 600) {
// 手机布局(无论横竖屏)
}响应式布局组件
封装一个根据屏幕宽度自动切换布局的组件:
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget desktop;
const ResponsiveLayout({
super.key,
required this.mobile,
this.tablet,
required this.desktop,
});
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width >= 1024) return desktop;
if (width >= 600) return tablet ?? mobile;
return mobile;
}
}
// 使用
ResponsiveLayout(
mobile: const MobileLayout(),
tablet: const TabletLayout(),
desktop: const DesktopLayout(),
)LayoutBuilder:基于父组件约束的响应式
MediaQuery 获取的是整个屏幕的信息。但在嵌套布局中,你通常需要根据父组件分配给你的空间来决定布局,这时用 LayoutBuilder:
LayoutBuilder(
builder: (context, constraints) {
// constraints 是父组件给你的约束
if (constraints.maxWidth > 600) {
return Row(children: [LeftPanel(), RightPanel()]);
} else {
return Column(children: [TopPanel(), BottomPanel()]);
}
},
)| 工具 | 获取的是什么 | 适用场景 |
|---|---|---|
MediaQuery | 整个屏幕的尺寸 | 页面级布局切换 |
LayoutBuilder | 父组件的约束 | 组件级自适应 |
适配键盘
当软键盘弹出时,viewInsets.bottom 会变成键盘的高度。这是处理键盘遮挡问题的核心数据。
import 'package:flutter/material.dart';
class KeyboardAwarePage extends StatelessWidget {
const KeyboardAwarePage({super.key});
@override
Widget build(BuildContext context) {
// 获取键盘高度
// ─── ★ viewInsets.bottom ──────────────
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
// ─── ☆ viewInsets.bottom ──────────────
// 判断键盘是否弹出
final isKeyboardOpen = keyboardHeight > 0;
return Scaffold(
appBar: AppBar(title: const Text('键盘适配')),
body: Padding(
padding: EdgeInsets.only(bottom: keyboardHeight),
child: Column(
children: [
const Expanded(child: Center(child: Text('内容区域'))),
Text(isKeyboardOpen ? '键盘已弹出,高度: $keyboardHeight' : '键盘未弹出'),
const TextField(decoration: InputDecoration(hintText: '点击弹出键盘')),
],
),
),
);
}
}Scaffold 自动处理键盘
大多数情况下,Scaffold 的 resizeToAvoidBottomInset: true(默认值)会自动把 body 区域缩小以避开键盘,你不需要手动处理:
Scaffold(
// resizeToAvoidBottomInset 默认就是 true,body 会自动缩小避开键盘
body: Column(
children: [
Expanded(child: ListView(...)), // 列表可滚动
TextField(), // 输入框不会被键盘遮挡
],
),
)手动处理键盘
在特殊场景(如聊天页面底部固定输入栏)中,可能需要手动处理:
// 键盘弹出时,让底部输入栏跟着上移
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
Column(
children: [
Expanded(child: ListView(...)),
Container(
padding: EdgeInsets.only(bottom: keyboardHeight), // 键盘高度作为底部内边距
child: TextField(),
),
],
)padding vs viewPadding vs viewInsets
这三个属性容易混淆,理解它们的区别对处理安全区和键盘至关重要:
| 属性 | 含义 | 键盘未弹出时 | 键盘弹出时 |
|---|---|---|---|
padding | 永久性系统 UI 遮挡的区域 | bottom: 34(主页指示器) | bottom: 0(被键盘「挤掉」了) |
viewPadding | 同 padding,但不受键盘影响 | bottom: 34 | bottom: 34(值不变) |
viewInsets | 临时性系统 UI 遮挡(主要是键盘) | bottom: 0 | bottom: 336(键盘高度) |
final mq = MediaQuery.of(context);
// 键盘未弹出时:
mq.padding.bottom // 34(主页指示器)
mq.viewPadding.bottom // 34(同上)
mq.viewInsets.bottom // 0(没有键盘)
// 键盘弹出时:
mq.padding.bottom // 0!(padding 被键盘覆盖了)
mq.viewPadding.bottom // 34(值不变)
mq.viewInsets.bottom // 336(键盘高度)简单记忆:
padding= 安全区(会被键盘影响)viewPadding= 安全区(不会被键盘影响)viewInsets= 键盘(只在键盘弹出时有值)
和 SafeArea 的关系
SafeArea 默认使用 padding。如果需要在键盘弹出时仍然保持底部安全区(如侧边栏),使用 SafeArea(maintainBottomViewPadding: true),它会改用 viewPadding。详见 安全区(Safe Area)。
系统字体缩放
用户可以在系统设置中调大字体(如无障碍功能)。textScaleFactor / textScaler 就是系统字体缩放倍率。
final scaleFactor = MediaQuery.of(context).textScaleFactor; // 如 1.3
// 文字实际大小 = fontSize × textScaleFactor
// 如 TextStyle(fontSize: 16),在 1.3 倍缩放下,实际显示为 16 × 1.3 = 20.8禁用系统字体缩放
如果用户设置的超大字体破坏了你的布局,可以在特定子树中禁用缩放:
MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(1.0), // 固定 1.0 倍缩放
),
child: child,
)Flutter 3.16 变更
textScaleFactor 在 Flutter 3.16 中废弃,改用 textScaler: TextScaler.linear(value)。TextScaler 支持 Android 14 的非线性字体缩放,textScaleFactor 仅支持线性缩放。
深色/浅色模式判断
MediaQuery 可以获取系统的亮度模式:
// 获取系统当前是深色还是浅色模式
final isDark = MediaQuery.of(context).platformBrightness == Brightness.dark;注意
platformBrightness 反映的是系统的亮度设置,而不是应用实际使用的主题。如果你的应用强制使用亮色模式(themeMode: ThemeMode.light),系统是暗色但应用实际显示亮色,此时 platformBrightness 仍是 Brightness.dark。
要判断应用实际使用的主题,应该用:
final isDark = Theme.of(context).colorScheme.brightness == Brightness.dark;详见 主题(Theme) 文档的深色模式章节。
常见用法速查
| 场景 | 代码 |
|---|---|
| 屏幕宽度 | MediaQuery.of(context).size.width |
| 屏幕高度 | MediaQuery.of(context).size.height |
| 设备像素比 | MediaQuery.of(context).devicePixelRatio |
| 判断手机/平板/桌面 | width < 600 / < 1024 / ≥ 1024 |
| 键盘高度 | MediaQuery.of(context).viewInsets.bottom |
| 键盘是否弹出 | viewInsets.bottom > 0 |
| 安全区顶部高度 | MediaQuery.of(context).padding.top |
| 系统字体缩放倍率 | MediaQuery.of(context).textScaler |
| 系统深色/浅色 | MediaQuery.of(context).platformBrightness |
| 禁用字体缩放 | MediaQuery(data: copyWith(textScaler: TextScaler.linear(1.0))) |
| 父组件约束布局 | LayoutBuilder(builder: (ctx, constraints) {...}) |
相关链接
- 安全区(Safe Area) — SafeArea 组件详解
- 主题(Theme) — 深色/浅色模式适配详解
- 尺寸与单位 — 逻辑像素与物理像素
