用一个很简单的widget,跟踪源码一步步查看它是如何被绘制出来的,涉及widget生成element,element生成renderObject,renderObject的layout布局,renderObject的paint绘制
用一个非常简单的container widget来举例,代码如下
void main() {
runApp(MaterialApp(
home: const TestPage(),
));
}
然后加载一个很简单的布局
class TestPage extends StatelessWidget {
const TestPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Demo Page'),
),
body: Container(
width: 100,
height: 100,
color: Colors.green,
),
);
}
}
一个scaffold,body是一个container,它的宽高都是100,颜色绿色,效果图如下
先看下container的初始化方法中,会初始化constraints
对象
constraints =
(width != null || height != null)
? constraints?.tighten(width: width, height: height)
?? BoxConstraints.tightFor(width: width, height: height)
: constraints,
super(key: key);
constraints
就是约束信息,因为我们设置了宽高,会走到tighten
方法
BoxConstraints tighten({ double? width, double? height }) {
return BoxConstraints(
minWidth: width == null ? minWidth : width.clamp(minWidth, maxWidth),
maxWidth: width == null ? maxWidth : width.clamp(minWidth, maxWidth),
minHeight: height == null ? minHeight : height.clamp(minHeight, maxHeight),
maxHeight: height == null ? maxHeight : height.clamp(minHeight, maxHeight),
);
}
可以知道,就是把当前container的宽高的最大宽高,最小宽高都设置成了100,初始尺寸,也可以直接设置constraints,比如如下代码,效果也是一样的
body: Container(
constraints: BoxConstraints.tightFor(width: 100, height: 100),
color: Colors.green,
),
在上一篇,我们知道,widget的加载,都是因为父widget的element调用了inflateWidget
,然后调用了当前widget的createElement
跟mount
方法,我们再看下
Element inflateWidget(Widget newWidget, Object? newSlot) {
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
我们看下container的源码,它是继承了statelessWidget
class Container extends StatelessWidget
对应的createElement
方法父类中,自己没有override
abstract class StatelessWidget extends Widget {
const StatelessWidget({ Key? key }) : super(key: key);
@override
StatelessElement createElement() => StatelessElement(this);
}
所以,container对应生成的element是StatelessElement
,然后调用它的mount
方法,在它的父类ComponentElement
实现
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
_firstBuild();
}
_firstBuild
最终调用到的是container的build
方法,这个方法还有点长,这里一步步看
Widget build(BuildContext context) {
// child是null,没有设child
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);
// 满足了判断条件,current变成了ColoredBox
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的build最终返回的widget是一个ConstrainedBox
,并且它的child是一个ColoredBox
,看下这两个widget的继承关系
class ConstrainedBox extends SingleChildRenderObjectWidget
class ColoredBox extends SingleChildRenderObjectWidget
abstract class SingleChildRenderObjectWidget extends RenderObjectWidget
abstract class RenderObjectWidget extends Widget
可以发现,它们都是继承RenderObjectWidget
,既不是我们熟悉的statelessWidget,也不是statefulWidget
RenderObjectWidget
跟我们熟知的widget不一样,看下它的createElement
方法
RenderObjectElement createElement();
它生成的是RenderObjectElement
,跟之前的ComponentElement
是什么区别呢
ComponentElement是为了组建出其他的element,本身不会生成RenderObject,而RenderObjectElement会生成最终RenderObject,最终负责布局跟绘制的,正是RenderObject ComponentElement并不会参与最终的绘制,只是起到一个桥梁的作用
通过它的源码,也可以看到上面的差别,看下RenderObjectElement
的mount方法
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
}
先调用createRenderObject
,生成一个renderObject,然后再调用attachRenderObject
,把这个renderObject加到renderObject树中
回到container build生成的ConstrainedBox
@override
RenderConstrainedBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(additionalConstraints: constraints);
}
class RenderConstrainedBox extends RenderProxyBox
RenderConstrainedBox虽然最终也是继承RenderObject
,又实现了performLayout
,但是没有实现paint
方法,说明ConstrainedBox有参与layout布局,但是没有参与最终的绘制,绘制还是由它的child来执行
flutter在大多数设备上,都是60帧的刷新,大概16ms刷新一次,所以底层engine会固定频率,发送一个刷新的回调SchedulerBinding.handleDrawFrame
,performLayout就是在这个刷新回调中被调用到的
继续看下RenderConstrainedBox
的performLayout
的具体代码
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
代码里的constraints
其实是container的parent的约束信息,断点可以看到是这个
BoxConstraints(0.0<=w<=360.0, 0.0<=h<=697.0)
代表它的child的宽度可以是0-360,高度可以是0-697,其实就是除了titleBar以外的屏幕尺寸
上面的_additionalConstraints
就是我们主动设置的尺寸,信息是
BoxConstraints(w=100.0, h=100.0)
看下_additionalConstraints.enforce(constraints)
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),
);
}
clamp方法,就是返回符合父布局约束的尺寸,这里的宽高都符合父布局,最终返回的约束信息还是w=100.0, h=100.0
然后给child测量尺寸,最终的尺寸,也是由child的大小决定,这里的child就是ColoredBox
生成的对应的RenderObject
RenderObject createRenderObject(BuildContext context) {
return _RenderColoredBox(color: color);
}
接着调用_RenderColoredBox
的layout
方法
void layout(Constraints constraints, { bool parentUsesSize = false }) {
_constraints = constraints;
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
}
_needsLayout = false;
markNeedsPaint();
}
还是调用到了_RenderColoredBox
的performLayout
方法
void performLayout() {
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
} else {
size = computeSizeForNoChild(constraints);
}
}
由于_RenderColoredBox
已经没有child的了,上面的代码会走到了else里面
Size computeSizeForNoChild(BoxConstraints constraints) {
return constraints.smallest;
}
Size get smallest => Size(constrainWidth(0.0), constrainHeight(0.0));
double constrainWidth([ double width = double.infinity ]) {
assert(debugAssertIsValid());
return width.clamp(minWidth, maxWidth);
}
最终的size就还是100的尺寸Size(100.0, 100.0)
,这个也就是_RenderColoredBox
的最终尺寸了
绘制是紧接着layout后执行,都是系统16ms每一帧后触发,看RenderbingBinding
void drawFrame() {
//这里触发layout,计算布局跟大小
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits()
//这里触发绘制,真正内容绘制到canvas上
pipelineOwner.flushPaint();
}
通过上面的代码,参与最终绘制的是_RenderColoredBox
的paint方法
void paint(PaintingContext context, Offset offset) {
if (size > Size.zero) {
//走到这里,绘制一个纯色的矩形
context.canvas.drawRect(offset & size, Paint()..color = color);
}
// 这次demo的child为空,走不到if里
if (child != null) {
context.paintChild(child!, offset);
}
}
最终的绘制,是调用了canvas.drawRect
绘制了一个绿色矩形,也就是我们看到的UI样式了,终于看到了最终的调用地方了;
如果有child,就会继续调用child的绘制,我们的这次的demo是没有的
1、widget树生成element树,element树生成renderObject树,最终布局跟绘制,是用的renderObject树 2、statelessWidget跟statefulWidget生成的element都是componentElement,不会参与最终的绘制,它的目的是为了更好的组建管理内部的child去参与绘制 3、参与绘制的element都是renderObjectElement以及它的子类 4、build后,widget还不是可见,要等到engine下一帧回调的时候,触发了布局跟绘制才算真的可见 5、mount,build,layout,paint都是在同个线程执行