MediaQuery作用是什么呢,我们先来看看源码文档注释
/// Establishes a subtree in which media queries resolve to the given data.
///
/// For example, to learn the size of the current media (e.g., the window
/// containing your app), you can read the [MediaQueryData.size] property from
/// the [MediaQueryData] returned by [MediaQuery.of]:
/// `MediaQuery.of(context).size`.
///
/// Querying the current media using [MediaQuery.of] will cause your widget to
/// rebuild automatically whenever the [MediaQueryData] changes (e.g., if the
/// user rotates their device).
///
/// If no [MediaQuery] is in scope then the [MediaQuery.of] method will throw an
/// exception, unless the `nullOk` argument is set to true, in which case it
/// returns null.
大致意思子树上的节点能访问到它的data数据信息,data信息包含很多,比如获取当前app窗口window的大小,你可以通过读取MediaQueryData.size属性,MediaQueryData结构通过MediaQuery.of(context)获取,获取当前media通过MediaQuery.of方法获取会导致MediaQueryData的数据变化时(比如:屏幕旋转,键盘的显示与隐藏),你的widget会自动重建,如果当前访问访问不到MediaQuery,即当前widget的祖先节点没有MediaQuery widget,该方法会抛异常,除非你传了nullOk参数为true
先看下MediaQuery的定义
class MediaQuery extends InheritedWidget {
/// Creates a widget that provides [MediaQueryData] to its descendants.
///
/// The [data] and [child] arguments must not be null.
const MediaQuery({
Key key,
@required this.data,
@required Widget child,
}) : assert(child != null),
assert(data != null),
super(key: key, child: child);
/// Contains information about the current media.
///
/// For example, the [MediaQueryData.size] property contains the width and
/// height of the current window.
final MediaQueryData data;
从定义我们可以看出它派生自InheritedWidget, 而InheritedWidget能有效地将数据在当前Widget树中向它的子widget树传递,子widget能沿着element树找到最近的祖先widget,从而获取其相关数据,子widget访问其数据时会在InheritedWidget中添加依赖,InheritedWidget的数据变化时会通知依赖的子widget,我们来看看如果访问MediaQuery的共享数据MediaQueryData。
/// The data from the closest instance of this class that encloses the given
/// context.
///
/// You can use this function to query the size an orientation of the screen.
/// When that information changes, your widget will be scheduled to be
/// rebuilt, keeping your widget up-to-date.
///
/// Typical usage is as follows:
///
/// ```dart
/// MediaQueryData media = MediaQuery.of(context);
/// ```
///
/// If there is no [MediaQuery] in scope, then this will throw an exception.
/// To return null if there is no [MediaQuery], then pass `nullOk: true`.
///
/// If you use this from a widget (e.g. in its build function), consider
/// calling [debugCheckHasMediaQuery].
static MediaQueryData of(BuildContext context, { bool nullOk = false }) {
assert(context != null);
assert(nullOk != null);
final MediaQuery query = context.dependOnInheritedWidgetOfExactType();
if (query != null)
return query.data;
if (nullOk)
return null;
throw FlutterError.fromParts([
ErrorSummary('MediaQuery.of() called with a context that does not contain a MediaQuery.'),
ErrorDescription(
'No MediaQuery ancestor could be found starting from the context that was passed '
'to MediaQuery.of(). This can happen because you do not have a WidgetsApp or '
'MaterialApp widget (those widgets introduce a MediaQuery), or it can happen '
'if the context you use comes from a widget above those widgets.'
),
context.describeElement('The context used was')
]);
}
从源码中我们看到是通过context.dependOnInheritedWidgetOfExactType()获取的, 继续跟进到framework
@override
T dependOnInheritedWidgetOfExactType({Object aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
_inheritedWidgets为所有子widget祖先链中的InheritedWidget节点,目的是为了能快速访问到节点,源码中最后又调用到dependOnInheritedElement方法,继续跟进
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
到这里我们终于找到了正主,访问时会将其添加到_dependencies中并返回InheritedWidget节点,这样就可以访问到其共享数据了,至于共享数据变化时怎么自动重建子widget,大家可以看下InheritedElement,Element是用来管理树中节点的更新,删除的
获取window的size很简单,但我们平常开发中常用到的属性还有 padding viewPadding 和 viewInsets属性,这几个属性比较迷惑人,如果你要处理键盘遮挡问题,这时你需要viewInsets属性获取键盘的高度,看看它的定义
/// The parts of the display that are completely obscured by system UI,
/// typically by the device's keyboard.
///
/// When a mobile device's keyboard is visible `viewInsets.bottom`
/// corresponds to the top of the keyboard.
///
/// This value is independent of the [padding] and [viewPadding]. viewPadding
/// is measured from the edges of the [MediaQuery] widget's bounds. Padding is
/// calculated based on the viewPadding and viewInsets. The bounds of the top
/// level MediaQuery created by [WidgetsApp] are the same as the window
/// (often the mobile device screen) that contains the app.
///
/// See also:
///
/// * [ui.window], which provides some additional detail about this property
/// and how it relates to [padding] and [viewPadding].
final EdgeInsets viewInsets;
viewInsets.bottom就可以获取到键盘的顶部,我们可以设置Padding的bottom为viewInsets.bottom从而使得可视区域为键盘遮挡后的剩余区域从而解决键盘遮挡了。
我们再来看看padding定义
/// The parts of the display that are partially obscured by system UI,
/// typically by the hardware display "notches" or the system status bar.
///
/// If you consumed this padding (e.g. by building a widget that envelops or
/// accounts for this padding in its layout in such a way that children are
/// no longer exposed to this padding), you should remove this padding
/// for subsequent descendants in the widget tree by inserting a new
/// [MediaQuery] widget using the [MediaQuery.removePadding] factory.
///
/// Padding is derived from the values of [viewInsets] and [viewPadding].
///
/// See also:
///
/// * [ui.window], which provides some additional detail about this
/// property and how it relates to [viewInsets] and [viewPadding].
/// * [SafeArea], a widget that consumes this padding with a [Padding] widget
/// and automatically removes it from the [MediaQuery] for its child.
final EdgeInsets padding;
意思就是被系统遮挡的部分,如iPhone X的挖孔和状态栏部分不可见区域的高度,其它的解释有点模糊,我实际使用了得出我的结论,以iPhone X为例,键盘不显示时它的padding.top为44,padding.bottom为34,如果键盘显示时padding.top为44,padding.bottom为0
而viewPadding 不管键盘显示或隐藏,viewPadding.top = 44和viewPadding.bottom = 34 都不变
知道这个我们来看看开发中常用的SafeArea
Widget build(BuildContext context) {
return Material(
child: SafeArea(
child: Center(
child: Container(
color: Colors.red,
child: TextField(
controller: _controller,
),
),
)
),
);
}
点击输入框键盘显示时会发现输入框下移了,先说下怎么解决,其实只要设置SafeArea的属性maintainBottomViewPadding为true就可以了,我们看看其源码
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
final MediaQueryData data = MediaQuery.of(context);
EdgeInsets padding = data.padding;
// Bottom padding has been consumed - i.e. by the keyboard
if (data.padding.bottom == 0.0 && data.viewInsets.bottom != 0.0 && maintainBottomViewPadding)
padding = padding.copyWith(bottom: data.viewPadding.bottom);
return Padding(
padding: EdgeInsets.only(
left: math.max(left ? padding.left : 0.0, minimum.left),
top: math.max(top ? padding.top : 0.0, minimum.top),
right: math.max(right ? padding.right : 0.0, minimum.right),
bottom: math.max(bottom ? padding.bottom : 0.0, minimum.bottom),
),
child: MediaQuery.removePadding(
context: context,
removeLeft: left,
removeTop: top,
removeRight: right,
removeBottom: bottom,
child: child,
),
);
}
关键的是设置bottom这句
if (data.padding.bottom == 0.0 && data.viewInsets.bottom != 0.0 && maintainBottomViewPadding)
padding = padding.copyWith(bottom: data.viewPadding.bottom);
意思键盘弹出时使用viewPadding.bottom,而键盘弹出时viewPadding.bottom == 没弹键盘时的padding.bottom,从而达到TextField位置不变