Flutter MediaQuery探究

Flutter MediaQuery探究

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位置不变

你可能感兴趣的:(Flutter开发技术,Flutter,UI)