总结了30个例子之后,我悟到了Flutter的布局原理

14,519 阅读9分钟

总结了30个例子之后,我悟到了Flutter的布局原理

学习最忌盲目,无计划,零碎的知识点无法串成系统。学到哪,忘到哪,面试想不起来。这里我整理了Flutter面试中最常问以及Flutter framework中最核心的几块知识,大概二十篇左右文章分析,欢迎关注,共同进步。 Flutter framework.png

欢迎搜索公众号:进击的Flutter或者runflutter里面整理收集了最详细的Flutter进阶与优化指南。关注我,获取我的最新文章~

导语

UI原理部分:

1、为什么不建议大家使用setState()。

2、面试官问我State的生命周期,该怎么回答

3、Flutter的布局约束原理

4、实战Flutter绘制过程

读完本文你将收获:Flutter各种控件是如何实现布局的


引言

一切的开始来自于这个神奇的网站(深入理解Flutter布局约束),其中提供了30个让奇奇怪怪的布局例子让我大涨见识(30个相当不讲武德!!),有这样的 这样的

还有这样的

不得不说确实覆盖了很多场景!可是对于我这种记性不好的懒鬼来说,看完30个例子真的是太!费!劲!了!而且看完就忘!!实际中大概率不会出现一模一样的情况。所以我就在寻思,这背后究竟是啥原理可以我下次不用反复复习这30个例子呢?这就引出了今天的主题:Flutter的布局原理

本期如果点赞超过50,加更一期(15个代表例子的详细解析) 来了来了!15个例子最详细的解析 让你彻底掌握Flutter布局原理


正片开始:先宏观看看Flutter组件的分类

在原生上我们知道一个View控件的渲染过程大致分为onMeasure()[知道有多大],onLayout()[知道该放那],onDraw()[知道长啥样]三个过程。但Flutter的UI体系思路和这个不太一样,首先在Flutter的组件体系中,并非所有的Widget都会渲染到最后的页面上,整个Widget大概可以分为三类组合类代理类绘制类 -[这点面试必问!!]-

平时我们使用到最多的StatelessWidgetStatefulWidget其实只是组合类的控件,实际上他并不负责绘制,所有我们在屏幕上看到的UI最终几乎都会通过RenderObjectWidget实现。而RenderObjectWidget中有个createRenderObject()方法生成RenderObject对象,RenderObject实际负责实际的layout()和paint()。例如我们最常使用的Container组件其实只是一个组合类的控件,在其中封装了多个负责绘制的原子组件。想详细了解RenderObjectWidget可以看看深入研究Flutter布局原理写得非常好


开胃小菜:RenderObject的的绘制过程

RenderObject是如何完成渲染的呢,在原来我一直在错误的使用 setState()?中分析过,Flutter的渲染流程关键在于drawFrame()方法中

void drawFrame() {
  //在这之前已经完成了build()
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  renderView.compositeFrame(); // this sends the bits to the GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}

整个过程和原生分为三个阶段build(),layout(),paint()build()方法由组合类和代理类Widget实现,layout()paint()由RenderObject实现。这里设计思路和原生不太一样,在原生中layout()方法一般由ViewGroup实现,他需要规定child控件的位置。这样他的子节点就只用关心绘制即可。

而Flutter中的layout()更接近理解为Measure(!!理解这点非常重要,不能用原生的思路去学习),它的职能主要是计算控件自身的尺寸位置偏移。这里的计算是一个从最顶级的节点开始传递约束,从下开始返回测量结果的过程

测量结果我们很好理解,就是一个控件实际的宽高。而约束是个啥玩意儿??


硬菜来了: What's Constraints

约束Constraints 在Flutter中是一种布局协议,Flutter中有两大布局协议BoxConstraintsSliverConstraints。对于非滑动的控件例如Padding,Flex等一般都使用BoxConstraints盒约束

  BoxConstraints({
    this.minWidth,
    this.maxWidth,
    this.minHeight,
    this.maxHeight,
  });

看起来非常好理解,在盒约束中,只会限制子控件的最大最小宽高。经过我搜刮了网上几乎所有的布局原理文章之后,对于这个约束这个约束可以这样总结。 首先这个约束可以根据最大最小值分为两大类

  • 1、 tight(紧约束):当max和min值相等时,这时传递给子类的是一个确定的宽高值。
const BoxConstraints.expand({
    double width,
    double height,
  }) : minWidth = width ?? double.infinity,
       maxWidth = width ?? double.infinity,
       minHeight = height ?? double.infinity,
       maxHeight = height ?? double.infinity;

这个约束的使用的地方主要有两个

一个在Container中,当Container的 child==null&&||(constraints == null || !constraints.isTight))时。 另一个ModalBarrier,这个组件我们不太熟悉,但查看调用发现被嵌套在了Route中,所以每次我们push一个新Route的时候,默认新的页面就是撑满屏幕的模式。

  • 2、loose(松约束):当max和min不相等的时候,这种时候对子类的约束是一个范围,称为松约束。
BoxConstraints.loose(Size size)
    : minWidth = 0.0,
      maxWidth = size.width,
      minHeight = 0.0,
      maxHeight = size.height;

在我们最场使用的Scaffold组件中就采用了这种布局,所以Scaffold对于子布局传递的是一个松的约束。


酣畅过瘾: 举几个栗子

了解了上面的基础概念之后,我们先来看看如何对一些场景进行分析。当我们想知道一个控件的布局过程是怎样的,可以参考:

  • 在你的代码中找到一个 Column 并跟进到它的源代码。为此,请在 (Android Studio/IntelliJ) 中使用 command+B(macOS)或 control+B(Windows/Linux)。你将跳到 basic.dart 文件中。由于 Column 扩展了 Flex,请导航至 Flex 源代码(也位于 basic.dart 中)。
  • 向下滚动直到找到一个名为 createRenderObject() 的方法。如你所见,此方法返回一个 RenderFlex。它是 Column 的渲染对象,现在导航到 flex.dart 文件中的 RenderFlex 的源代码。
  • 向下滚动,直到找到 performLayout() 方法,由该方法执行列布局。

根据这个方法,我们试着分析几个有意思的栗子。本期如果点赞超过50,加更一期(20个例子的详细解析)

案例1(来自样例一)

如图,如果我们直接返回一个红色Container,这个时候他会撑满整个屏幕。首先Container是一个组合类的Widget,并不负责渲染。查看他的build方法,在这种情况下返回了三层RenderObject RenderDecoratedBox,RenderLimitedBox,RenderConstrainedBox

这三个类都继承自RenderProxyBox,这个类混入了RenderProxyBoxMixin,布局方法就在里面:

 @override
  void performLayout() {
    if (child != null) {
      child.layout(constraints, parentUsesSize: true);
      size = child.size;
    } else {
      performResize();
    }
  }

其实从这个类的名称我们可知一二,这是个代理类的渲染对象,如果他有子节点的的时候他会把自己父节点的约束constraints传递给节点,然后使用子节点的尺寸作为自己的。 那么最外层的RenderDecoratedBox的约束是什么呢。前面其实也提到了在紧约束中,BoxConstraints.expand被用在了Route上,所以每个页面默认是撑满屏幕的。这个约束就一直向下传递 但在最下面的RenderConstrainedBox重写了performLayout方法

@override
  void performLayout() {
    if (child != null) {
      child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
      size = child.size;
    } else {
      size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
        }
  }

其实和上面差不多,不过对RenderConstrainedBox我们可以添加约束信息_additionalConstraints属性,查看Container的build方法可知这个约束在这种情况下为BoxConstraints.expand ,在layout的时候也会被考虑。由于RenderConstrainedBox下没有child了所以走

size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
  ///返回新的框约束,它尊重给定的约束,同时与原始约束尽可能接近
  BoxConstraints enforce(BoxConstraints constraints) {
    return BoxConstraints(
      minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth),
      maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth),
      minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight),
      maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight),
    );
  }

关键在于enforce这个方法,这里他接受到的参数是来自页面中的紧约束(max=min=屏幕宽度),这样无论你为自身添加的约束_additionalConstraints是多少,他都会返回一个紧约束(max=min=屏幕宽度),而这个约束被再次向下传递

最终这个Container渲染的时候,撑满了整个屏幕!

案例2(来自样例9)

这个例子案例1外层增加了一个约束,并且内部也为自己设置了高度。你可能会猜想 Container 的尺寸会在 70 到 150 像素之间,但并不是这样,Container任然撑满了整个屏幕。我们还是来看Render树的结构 整个树构造变成了这样,我们一步步来看。 首先最外层的ConstrainedBox接收到来自页面的紧约束(max=min=屏幕宽度),计算采用上面的计算方式还是得到了一个同样的约束,之后这个约束向下传递,就变成和案例一一样的情况


总结

1、Widget大概可以分为三类组合类代理类绘制类

2、所有我们在屏幕上看到的UI最终几乎都会通过RenderObjectWidget实现。而RenderObjectWidget中有个createRenderObject()方法生成RenderObject对象,RenderObject实际负责实际的layout()和paint()。

3、Container组件其实只是一个组合类的控件,在其中封装了多个负责绘制的原子组件。

4、layout() 职能主要是计算控件自身的尺寸位置偏移

5、整个布局过程就是向下约束 向上传值的过程

6、盒约束中有两种:

  • tight(紧约束):当max和min值相等时,这时传递给子类的是一个确定的宽高值。
  • loose(松约束):当max和min不相等的时候,这种时候对子类的约束是一个范围,称为松约束

一句话总结


最后

写作不易 欢迎关注点赞,本期如果点赞超过50,加更一期(十五个例子的详细解析)!! 来了来了!15个例子最详细的解析 让你彻底掌握Flutter布局原理

参考文章:

juejin.cn/post/689701… juejin.cn/post/684490… blog.csdn.net/weixin_4345…