掌握Flutter布局:原理与实践

发表时间: 2020-09-11 09:47

1. Flutter UI架构

Flutter将视图数据抽象成为三个部分,即Widget树、Element树和RenderObject树。

  • Widget树:控件的配置信息,不涉及渲染,更新代价极低。
  • RenderObject树:真正的UI渲染树,负责渲染UI,更新代价极大。
  • Element树:Widget树和RenderObject树之间的粘合剂,负责将Widget树的变更以最低的代价映射到RenderObject树上。

2. Flutte布局原理

1. 源码分析

RenderObject是Flutter真正的UI渲染树,负责界面的测量,布局和绘制,Flutter页面布局相关的源码都在RenderObject及其子类中。

上图是RenderObject类中和布局相关的属性和方法.

  • constrainst属性由直接父布局提供的布局约束(最大高度,最小高度,最大宽度,最小宽度),
  • layout方法由父RenderObject调用,计算当前RenderObject的布局,父RenderObject传递给子RenderObject约束信息,子RenderObject必须服从约束信息。如果父RenderObject需要子RenderObject的布局信息,parentUsesSize参数应该传true,否则传false。
  • performLayout:子类必须重写的方法,计算当前RenderObject和子RenderObject的布局信息。
  • performResize:子类必须重写的方法,仅使用约束更新RenderObject的布局。RenderConstrainedBox是ConstrainedBox对应的RenderObject,最多有一个子RenderObject, 如果子RenderoObject不为空,则对子RenderObject进行布局,并计算布局的大小。RenderFlex是Column和Row对应的RenderObject,可以包含多个子RenderObject, 如果子RenderoObject不为空,则对子RenderObject依次进行布局,并计算布局的大小。

2. Flutte布局原理总结

  1. RenderObject会通过它的父级获得自身的约束。 约束实际上就是4个浮点类型的集合:最大/最小宽度,以及最大/最小高度。
  2. 然后,这个RenderObject将会逐个遍历它的children列表。向子级传递约束子级之间的约束可能会有所不同),然后询问它的每一个子级需要用于布局的大小。
  3. 然后,这个RenderObject就会对它子级的children逐个进行布局。(水平方向是x轴,竖直是y轴)
  4. 最后,RenderObject将会把它的大小信息向上传递至父RenderObject(包括其原始约束条件)。

3. Flutter布局限制

由于父RenderObject传递给子RenderObject约束信息,子RenderObject传递给父RenderObject大小信息,Flutter布局存在一些限制:

  • 一个RenderObject仅在其父级给其约束的情况下才能决定自身的大小。 这意味着RenderObject通常情况下不能任意获得其想要的大小。
  • 一个RenderObject无法知道,也不需要决定其在屏幕中的位置。 因为它的位置是由其父级决定的。
  • 当轮到父级决定其大小和位置的时候,同样的也取决于它自身的父级,所以,在不考虑整棵树的情况下,几乎不可能精确定义任何RenderObject的大小和位置。

4. Flutter布局实战

1.线性布局

线性布局的子布局在主轴上长度不能设置无限大(double.infinity),因为线性布局给子布局在主轴上的最大长度的约束就是无限大(double.infinity),会导致无限大的子布局无法计算当前布局的大小,布局失败。如下:

// 第一个子Container的高度设置为无限大,导致布局失败Column(  children: <Widget>[    Container(      width: 200.0,      height: double.infinity,      color: Colors.blue,    ),    Container(      width: 200.0,      height: 200.0,      color: Colors.red,    ),  ],);// 子Column的高度为无限大,导致布局失败Column(  children: <Widget>[    Column(      children: <Widget>[        Container(          width: 200.0,          height: 200.0,          color: Colors.blue,        ),      ],    ),    Container(      width: 200.0,      height: 200.0,      color: Colors.red,    ),  ],);

如果想要让子布局占满全屏可以增加Expanded布局。如下:

Column(  children: <Widget>[    Expanded(      child: Container(        width: 200.0,        height: double.infinity,        color: Colors.blue,      ),    ),    Container(      width: 200.0,      height: 200.0,      color: Colors.red,    ),  ],);Column(  children: <Widget>[    Expanded(      child: Column(        children: <Widget>[          Container(            width: 200.0,            height: 200.0,            color: Colors.blue,          ),        ],      ),    ),    Container(      width: 200.0,      height: 200.0,      color: Colors.red,    ),  ],);

2. 栈布局

栈布局的子布局设置相对位置推荐使用Padding代替Position,并把fit属性设置StackFit.loose,构建时Stack会自适应大小,否则需要使用Container设置Stack和Position的大小;并且Padding默认对上下左右都有相对距离(0),但Position没有默认的相对布局,如果忘记设置,会使Position的子布局无法自动换行或布局不显示。

不推荐:

// Text无法自动换行// Container布局不显示Stack(  children: <Widget>[    Positioned(      top: 100.0,      child: Text(          '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试'),    ),    Positioned(      top: 0.0,      child: Column(        children: <Widget>[          Container(            height: 100.0,            width: double.infinity,            color: Colors.red,          ),        ],      ),    )  ],);

推荐:

Stack(  children: <Widget>[    Padding(      padding: const EdgeInsets.only(top: 10.0),      child: Text(          '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试'),    ),    Padding(      padding: const EdgeInsets.only(top: 0.0),      child: Column(        children: <Widget>[          Container(            height: 100.0,            width: double.infinity,            color: Colors.red,          ),        ],      ),    )  ],);

3. 可滚动布局

可滚动布局在主轴方向上的长度是无限大,父布局需要转递给可滚动布局最大长度的约束。

Column(  children: <Widget>[    Expanded(      child: ListView(        children: <Widget>[          Text('测试'),        ],      ),    ),  ],);

横向可滚动布局必须设置高度。

Column(  children: <Widget>[    Container(      height: 200.0,      child: ListView(        scrollDirection: Axis.horizontal,        children: <Widget>[          Text('测试'),        ],      ),    ),  ],);

可滚动布局嵌套可滚动布局被嵌套布局的shrinkWrap属性必须设置为true,并且可以将physics属性设置为NeverScrollableScrollPhysics()来解决滑动冲突。

ListView(  children: <Widget>[    ListView(      shrinkWrap: true,      physics: NeverScrollableScrollPhysics(),      children: <Widget>[        Text('测试'),      ],    ),  ],);

5. 总结

本文章通过源码分析讲述Flutter布局的过程,分析实战中布局不显示的原因,并给出Flutter布局的一些建议。