flutter布局计算规则
之前主要是负责Web开发的,在刚开始写Flutter
的时候很容易把Container
组件当成div
标签来使用 ,以至于在碰到Container(width:100,height:100,color:Colors.red)
,代码并没有按预期展示宽高各100的元素时感到非常诧异。
后来查阅了相关文章之后,终于了解到:Flutter与Web浏览器的布局差异非常之大!本文主要整理Flutter渲染流程中的布局计算规则。
参考 距离上一次学习Flutter已经过去了很长时间,现在官方文档感觉写的已经比较详细了,建议flutter初学者可以先过一下官方的整体文档
- flutter 布局约束,非常建议先看看这篇
- Flutter 架构概览
Flutter渲染三棵树
Widget组件树
在UI开发中,Flutter的设计思想与React基本一致
UI = f(State)
Widget对应的是就是虚拟DOM,是不可变的,它的改变意味着重新创建。在Dart语言中,实例的初始化和销毁是非常迅速的,这也是为什么Flutter选择了Dart作为开发语言。
Widget是最基本的布局组件,大概可以分为三类组合类、代理类、绘制类
- 组合类,都继承自
StatefulWidget
或StatelessWidget
,通过实现build
方法返回Widget组件树 - 代理类,用来为child widget提供一些中间功能,如
InheritedWidget
等 - 绘制类,是最核心的Widget类型,所有需要绘制的Widget最后都继承自这个
RenderObjectWidget
类
RenderObject渲染树
前面提到所有需要绘制的Widget,最终都继承自RenderObjectWidget
abstract class RenderObjectWidget extends Widget {
@protected
@factory
RenderObject createRenderObject(BuildContext context);
}
影响Widget样式的实际类是RenderObject
,此类才是绘制和布局的核心。渲染树的任务是做组件的具体的布局渲染工作
渲染树上每个节点都是一个继承自 RenderObject 类的对象,负责布局约束和尺寸计算
RenderObject
定义了几个比较布局阶段比较重要的方法
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
@protected
void performLayout();
// 布局,主要由子节点调用
void layout(Constraints constraints, { bool parentUsesSize = false }) {}
// 在canvas上绘制最终的内容
void paint(PaintingContext context, Offset offset) { }
}
Flutter 中的控件在屏幕上绘制渲染之前需要先进行布局(Layout)操作,也就会调用performLayout
方法 。
来找个实现了createRenderObject
方法的Widget,比如Padding
abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {}
class Padding extends SingleChildRenderObjectWidget {
@override
RenderPadding createRenderObject(BuildContext context) {
return RenderPadding(
padding: padding,
textDirection: Directionality.maybeOf(context),
);
}
}
RenderPadding
是一个继承自RenderObject
的类,我们来看看他实现的相关方法
abstract class RenderBox extends RenderObject {
// RenderBox是RenderObject的一个非常重要的子类
// 因为RenderObject的定义比较顶层,甚至连宽高信息这一很重要的属性都没有定义
// 因而我们通常都需要继承RenderBox来进行定制RenderObject
}
abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {}
class RenderPadding extends RenderShiftedBox {
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
_resolve();
if (child == null) {
size = constraints.constrain(Size(
_resolvedPadding!.left + _resolvedPadding!.right,
_resolvedPadding!.top + _resolvedPadding!.bottom,
));
return;
}
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!);
// 将约束传递给子节点
child!.layout(innerConstraints, parentUsesSize: true);
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = Offset(_resolvedPadding!.left, _resolvedPadding!.top);
// 根据子节点的尺寸计算自己的尺寸
size = constraints.constrain(Size(
_resolvedPadding!.left + child!.size.width + _resolvedPadding!.right,
_resolvedPadding!.top + child!.size.height + _resolvedPadding!.bottom,
));
}
}
这里就可以看见performLayout
、也就是Flutter布局的具体工作,可分为两个线性过程:
- 从顶部向下传递约束
- 从底部向上传递布局尺寸信息
Flutter内置了许多Widget,大都已经实现了相关的performLayout
,因此我们完全也可以参考源码,实现各种自定义渲染的组件。
Element元素树
在业务开发中,我们最常接触的是Widget组件,实际渲染的又是RenderObject,这两者如何关联起来呢?
Widget对应的是虚拟DOM,那么肯定有一个与之对应的、最后用来真正绘制在屏幕上面的“真实DOM”对象。这就是Element
每个Widget都会对应一个Element,因此WIdget组件树也会对应一颗Element元素树。
abstract class Widget extends DiagnosticableTree {
@protected
@factory
Element createElement();
}
Element持有其对应 Widget 的引用,在Widget改变后,会被标记成dirty Element,在每次更新时,只会更新那些dirty Element的元素,从而提升性能。
前面提到,Widget是不可变的,每次改变都会生成一个新的Widget,那StatefulWidget
的state是谁来负责保存呢?也是由Element负责的
abstract class StatefulWidget extends Widget {
@override
StatefulElement createElement() => StatefulElement(this);
}
class StatefulElement extends ComponentElement {
/// Creates an element that uses the given widget as its configuration.
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
state._element = this;
state._widget = widget;
assert(state._debugLifecycleState == _StateLifecycle.created);
}
@override
Widget build() => state.build(this);
}
这里也可以看到,Widget的build方法中的BuildContext
实际上就是该widget对应的Element
class _MyHomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
// 这个context就是对应的element
}
}
此外,Element维持了获取renderObject实例的引用
abstract class Element extends DiagnosticableTree implements BuildContext {
RenderObject? get renderObject {
RenderObject? result;
void visit(Element element) {
assert(result == null); // this verifies that there's only one child
if (element._lifecycleState == _ElementLifecycle.defunct) {
return;
// 遍历Element子树,找到RenderObjectElement,并获取其renderObject
} else if (element is RenderObjectElement) {
result = element.renderObject;
} else {
element.visitChildren(visit);
}
}
visit(this);
return result;
}
}
可以看出,Element会遍历子节点,找到类型为RenderObjectElement
、也就是真正被渲染到屏幕上的那个renderObject。
所以,Element是Widget和RenderObject之间的中间人。
小结
了解了Flutter中的三棵树之后,总结一下Flutter的渲染流程
- 通过Widget的
build
获取组件树 - 通过RenderObject的
layout
计算布局所需的尺寸和位置,然后执行paint
将内容绘制在画布上
约束
前面已经提到,RenderObject
的layout
布局流程是从上到下传递约束,从下向上提供尺寸。这一章节来看看约束相关的知识。
约束定义
约束Constraints
在Flutter中是一种布局协议,约束实际上就是四个数值:最大/最小宽度,最大/最小高度
约束可以根据最大最小值分为两大类
- tight(紧约束):当max和min值相等时,这时传递给子类的是一个确定的宽高值。
- loose(松约束):当max和min不相等的时候,这种时候对子类的约束是一个范围,称为松约束。
Flutter 整个布局过程就是向下约束和向上传值的过程: 父节点传递约束,子节点向上传递尺寸,最后由父节点决定你的位置
Constraints go down. Sizes go up. Parent sets position.
大概流程就是
- 从根节点开始,Widget从其父节点获取自身可以用于布局的约束,对于根节点而言,就是屏幕大小的宽度和高度
- 然后Widget会依次遍历其子节点,然后计算剩余可供每个子节点布局的约束,并将该约束传递给子节点,
- 每个子节点布局的约束可能不一样,比如对于第一个子节点,还有100的高度可以布局,它用掉50个高度之后,对于后面的子节点,就只有50的高度可以布局了
- 子节点也会依次向其子节点传递约束,所以是一个递归过程,最后子节点会返回它需要用于布局的大小
- Widget获取其每个子节点的大小之后,就会对他们逐个进行布局,最后获得了Widget自己的大小,并返回给其父节点
因此Flutter的布局有一些重要的限制
- 一个 widget 仅在其父级给其约束的情况下才能决定自身的大小。这意味着 widget 通常情况下 不能任意获得其想要的大小。
- 一个 widget 无法知道,也不需要决定其在屏幕中的位置。因为它的位置是由其父级决定的。
- 当轮到父级决定其大小和位置的时候,同样的也取决于它自身的父级。所以,在不考虑整棵树的情况下,几乎不可能精确定义任何 widget 的大小和位置。
- 如果子级想要拥有和父级不同的大小,然而父级没有足够的空间对其进行布局的话,子级的设置的大小可能会不生效。 这时请明确指定它的对齐方式
工作流程
了解了布局约束,就不难猜测为什么我们写的Container(width:100,height:100)
在某些时候,得到的Container size为什么不是符合预期的100了。
来看看Container
的源码
class Container extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget? current = child;
if (child == null && (constraints == null || !constraints!.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
}
if (alignment != null)
current = Align(alignment: alignment!, child: current);
final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null)
current = Padding(padding: effectivePadding, child: current);
if (color != null)
current = ColoredBox(color: color!, child: current);
if (clipBehavior != Clip.none) {
assert(decoration != null);
current = ClipPath(
clipper: _DecorationClipper(
textDirection: Directionality.maybeOf(context),
decoration: decoration!,
),
clipBehavior: clipBehavior,
child: current,
);
}
if (decoration != null)
current = DecoratedBox(decoration: decoration!, child: current);
if (foregroundDecoration != null) {
current = DecoratedBox(
decoration: foregroundDecoration!,
position: DecorationPosition.foreground,
child: current,
);
}
if (constraints != null)
current = ConstrainedBox(constraints: constraints!, child: current);
if (margin != null)
current = Padding(padding: margin!, child: current);
if (transform != null)
current = Transform(transform: transform!, alignment: transformAlignment, child: current);
return current!;
}
}
可以看见Container更像是一个代理类型的Widget,根据各种参数,最后在build方法中返回对应的组件。
ConstrainedBox
回到文章开头的例子
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(width: 100, height: 100, color: Colors.red);
}
}
通过断点可以看见Container(width:100,height:100,color:Colors.red)
最后进入的是
if (color != null)
current = ColoredBox(color: color!, child: current);
if (constraints != null)
current = ConstrainedBox(constraints: constraints!, child: current);
其中ColoredBox
只是设置了一下renderObject的color属性,与布局无关,这里不展开。我们来看看ConstrainedBox
class ConstrainedBox extends SingleChildRenderObjectWidget {
@override
RenderConstrainedBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(additionalConstraints: constraints);
}
}
class RenderConstrainedBox extends RenderProxyBox {
@override
void performLayout() {
// 此时constraints是父元素传入的屏幕大小 BoxConstraints(w=428.0, h=926.0)
final BoxConstraints constraints = this.constraints;
// 此时 child 是ColoredBox
if (child != null) {
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
}
最后得到的size也就是Size(428.0, 926.0)
,因此我们就看到了一个铺满屏幕的红色容器,而不是一个Size(100.0, 100.0)
的容器了。
LimitedBox
趁热打铁,顺便看看Container第一种情况:在没有child且没有约束时的情况下返回的是LimitedBox
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
class LimitedBox extends SingleChildRenderObjectWidget {
@override
RenderLimitedBox createRenderObject(BuildContext context) {
// 找到对应的RenderObject
return RenderLimitedBox(
maxWidth: maxWidth,
maxHeight: maxHeight,
);
}
}
class RenderLimitedBox extends RenderProxyBox {
Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild }) {
if (child != null) {
final Size childSize = layoutChild(child!, _limitConstraints(constraints));
return constraints.constrain(childSize);
}
return _limitConstraints(constraints).constrain(Size.zero);
}
@override
void performLayout() {
// 计算子节点的尺寸,然后将其作为自身的尺寸大小
size = _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.layoutChild, // 实际上就是调用child.layout,然后返回child.size
);
}
}
class ChildLayoutHelper {
static Size layoutChild(RenderBox child, BoxConstraints constraints) {
child.layout(constraints, parentUsesSize: true);
return child.size;
}
}
根据这个流程,我们可以找到所有内置Widget对应RenderObject的布局计算规则,也就弄清楚了Container在不同场景下渲染不同结果的原因了。
自定义布局组件
了解了Flutter的布局约束原理,我们可以很方便地实现各种自定义组件,接下来我们实现一个自定义的居中组件
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
class CustomCenter extends SingleChildRenderObjectWidget {
const CustomCenter({Key? key, Widget? child}) : super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCustomCenter();
}
}
class RenderCustomCenter extends RenderShiftedBox {
RenderCustomCenter() : super(null);
@override
void performLayout() {
child!.layout(constraints.loosen(), parentUsesSize: true);
size = constraints.constrain(Size(child!.size.width, child!.size.height));
BoxParentData parentData = child!.parentData as BoxParentData;
parentData.offset = ((size - child!.size) as Offset) / 2;
}
}
现在看起来是不是非常简单了。真正居中的只有最后那一行代码:通过设置子节点的parentData.offset
来处理子节点的偏移。
将该计算公式进行调整,就可以得到居左、居右各种自定义组件了。
小结
本文首先通过分析Flutter源码,整理了Flutter渲染的基本流程,包括:build
、layout
和paint
;
然后整理了layout
中,也就是Flutter布局的基本原理:向下传递约束,向上传递尺寸;
最后实现了一个自定义组件。
在整个过程中,发现Flutter的源码还是比较容易阅读的。除了对Dart的部分语法不了解之外,比如number.clamp
double clamp<T extends num>(T number, T low, T high) =>
max(low * 1.0, min(number * 1.0, high * 1.0));
其他感觉还是挺轻松的。之后会尝试进一步深入阅读Flutter相关源码。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。