Flutter盒模型布局原理

布局原理

flutter中布局(layout)和绘制(paint)的控制主要在RenderObject类中完成。所有flutter中涉及渲染的组件,都通过RenderObject完成最终的绘制。包括:

  • StatalessWidget
  • StatefulWidget
  • LeafRenderObjectWidget
  • SingleChildRenderObjectWidget
  • MultiChildRenderObjectWidget
    在绝大多数情况下,我们会使用盒模型(Box)来完成页面的布局。在flutter中盒模型的规则在RenderObject的子类RenderBox中实现。简单来说RenderObject制定了有由父节点和子节点嵌套完成的渲染规则,而子类RenderBox进一步盒模型的布局规则。

我们想要做一些高度自定义的组件(例如ios中的可折叠通知栏),就必须深入了解Flutter的布局和绘制,也就是要读懂这两个类。在理解他们之后,你就会理解大多数Flutter布局组件的原理,例如CenterRowStack。下面我们来具体分析一下这两个宝宝。

1. RenderObject的主要职责

RenderObject是所有布局协议(RenderBox就是一种布局协议)的基类。它制订了以父子组件嵌套形式的基本布局框架,以及在这个大框架下父子组件间的数据传递,组件中布局和渲染方法的调用时机,和很多性能优化措施。

RenderObject中并没有规定组件中孩子的数量(没有孩子、单个孩子、多个孩子),也没有规定布局所使用的坐标系统(是笛卡尔坐标系,还是极坐标)。这些细节都在它的子类中实现,这样做的好处是竟可能的满足框架的可扩展性,同时满足类的职责单一原则。

1.1 parentData属性

我们可以从名字上看出这个属性是负责父子组件的数据传递的。例如在RenderFlex类中,父组件会设置每一个子组件的parentData属性上的offset值,用于确定每个Item相对于父容器的位置。

子组件上的parentData属性必须有父容器设置,并且这个值对子组件是不透明的,绝大多数时候子组件无需处理这个属性。

1.2 继承RenderObject

绝大多数时候我们不需要直接集成RenderObject,我们可以集成它的子类例如RenderBox。如果你想定义新的坐标系统,那么你可以考虑集成RenderObject

1.3 布局计算过程

具体的布局过程发生在layoutperformLayoutpreformResize这几个方法中,详细的过程我们想在讲解RenderBox的时候讨论。我们先来了解一下页面的布局大致过程。

STEP 1 Constraints下发

首先根容器会向它的孩子传递Constraints,这里的Constraints表示子组件布局空间的限制。例如在RenderBox中,Constraints由四个属性构成(minWidth,maxWidth,minHeight,MaxHeight),他们决定了子组件的宽高的极值。父组件通过layout方法把Constraints传递给孩子,并通知孩子进行布局。

你可能已经发现了这是一个递归的过程,这个过程会一直传递到最内层的组件。

STEP 2 计算组件的Size

和步骤一相反,Size的计算是从最内层组件开始的(在RenderBox中组件的大小由Size属性,但是在其他布局协议中可能使用其他的属性)。当子组件的Size计算完成后,父组件可以更具子组件的大小来计算自己的Size

例如在Column组件中,父组件包含多个孩子,父组件等待所有子组件的高度计算完成后便可通过累加孩子的高度得出自己的高度。

在这个步骤中父组件会计算出子组件相对于父组件的位置偏移量。

例如在Column中第一个孩子在垂直方向的偏移量为0,第二个孩子的偏移量是第一个孩子的高度,依次类推。

1.3 布局中的性能优化

布局中的性能优化,主要体现在布局更新的过程中。我们知道父子组件的布局会相互作用,当子组件的的布局发生变化时,子组件会通知父组件本次更新(通过调用父级的markNeedsLayout方法)。这是一个冒泡的过程,如果中间不被打断,则整个页面上的组件都会发生重排。

RenderObject通过以下措施尽可能的减少需要重排的组件,将重排的范围降到最低。

parentUsesSize

父组件在向子组件传递Constraints的同时,还会向孩子传递parentUsesSize参数。

void layout(Constraints constraints, { bool parentUsesSize = false }) {}

如果这个参数为true,则表示父组件的布局依赖于子组件。也就是说如果子组件的布局发生变化,则父组件也会发生layout。反之如果这个参数为false,则子组件重排不会影响父组件的布局。如果我们自定义的组件的布局可以不依赖子组件,则需要传递false,这样可以尽可能的减少layout的工作量。

sizedByParent属性

sizedByParentRenderObject的属性。它参数表示这个组件的布局完全由父级控制,不收子组件的影响。也就是说这个组件为true时,子组件的布局变化不会引起父组件布局的变化。和parentUsesSize相似它也可以优化layout阶段的工作量。

parent is! RenderObject

这表示这个组件是一个渲染层(layer)的根。

constraints.isTight

当一个组件布局时收到的constraints是紧的时,它的布局也不会收的孩子的影响。同理可以优化layout阶段的工作量。

1.4 layout方法

在了解了1.3中的各种layout阶段的概念后,我们可以具体讨论一下layout方法中的细节。

layout方法串联了Flutter的布局过程的工作流,具体步骤如下:

SETP 1 判断当前组件是否是渲染边界(relayoutBoundary)

渲染边界的含义是:当前组件的layout不收孩子的影响。如果一个组件被判定为渲染边界,那么layout方法会进行性能优化。

满足1.3节中的任意一个条件,则改组件可以被判定为渲染边界。

SETP 2 判断当前组件是判断时候需要执行布局

前面已经提到RenderObject会尽量避免布局,从而是性能得到提升。

同时满足一下三个条件,则说明该组件不需要重新布局。

  • _needsLayout属性为false:这说明该组件没有被自己或孩子标记为脏。
  • constraints == _constraints :父级的限制没有发生变化。
  • relayoutBoundary没有发生变化:说明该组件所属的渲染边界没有发生变化(不清楚何时会变化)。

如果满足这三个条件则layout方法提前return。组件的性能得到了优化。否则执行下一步。

SETP 3 更新constraints和渲染边界

走到这一步说明该组件需要重新布局。

SETP 4 执行performResize方法

当属性sizedByParent为true时,RenderObject会执行performResize方法。我们可以在这个方法中改组件的Size,和孩子的布局。

SETP 5 执行performLayout方法

我们可以在这个方法中改组件的Size,和孩子的布局。

SETP 6 执行markNeedsPaint方法

这一步是为了通知Flutter在下一个Frame进行重绘。

至此RenderObject的布局工作已经完成。

在这一章中我们主要探讨了RenderObject的布局规则,并且很多规则都已RenderBox为例。在下一讲中,我们会学习如何继承RenderBox高效的实现自定义布局组件。

待补充

其它的渲染协议

你可能感兴趣的:(Flutter,Flutter,RenderObject,RenderBox,布局,layout)