前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >flutter源码:widget是如何绘制出来的

flutter源码:widget是如何绘制出来的

作者头像
韦东锏
发布2022-04-11 17:52:11
7760
发布2022-04-11 17:52:11
举报
文章被收录于专栏:Android码农

用一个很简单的widget,跟踪源码一步步查看它是如何被绘制出来的,涉及widget生成element,element生成renderObject,renderObject的layout布局,renderObject的paint绘制

初始代码

用一个非常简单的container widget来举例,代码如下

代码语言:javascript
复制
void main() {
  runApp(MaterialApp(
    home: const TestPage(),
  ));
}

然后加载一个很简单的布局

代码语言:javascript
复制
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初始化

先看下container的初始化方法中,会初始化constraints对象

代码语言:javascript
复制
constraints =                                                   
 (width != null || height != null)                              
   ? constraints?.tighten(width: width, height: height)         
     ?? BoxConstraints.tightFor(width: width, height: height)   
   : constraints,                                               
super(key: key);                                                

constraints就是约束信息,因为我们设置了宽高,会走到tighten方法

代码语言:javascript
复制
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,比如如下代码,效果也是一样的

代码语言:javascript
复制
body: Container(                                                
  constraints: BoxConstraints.tightFor(width: 100, height: 100),
  color: Colors.green,                                          
),                                                               

container的加载

在上一篇,我们知道,widget的加载,都是因为父widget的element调用了inflateWidget,然后调用了当前widget的createElementmount方法,我们再看下

代码语言:javascript
复制
Element inflateWidget(Widget newWidget, Object? newSlot) {                        
  final Element newChild = newWidget.createElement();                             
  newChild.mount(this, newSlot);                                                  
  return newChild;                                                                
}   

我们看下container的源码,它是继承了statelessWidget

代码语言:javascript
复制
class Container extends StatelessWidget

对应的createElement方法父类中,自己没有override

代码语言:javascript
复制
abstract class StatelessWidget extends Widget {                  
  const StatelessWidget({ Key? key }) : super(key: key);        
  @override                                                     
  StatelessElement createElement() => StatelessElement(this);   
  }

所以,container对应生成的element是StatelessElement,然后调用它的mount方法,在它的父类ComponentElement实现

代码语言:javascript
复制
@override                                              
void mount(Element? parent, Object? newSlot) {         
  super.mount(parent, newSlot);      
  _firstBuild();                      
}                                                      

_firstBuild最终调用到的是container的build方法,这个方法还有点长,这里一步步看

代码语言:javascript
复制
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的继承关系

代码语言:javascript
复制
class ConstrainedBox extends SingleChildRenderObjectWidget

class ColoredBox extends SingleChildRenderObjectWidget

abstract class SingleChildRenderObjectWidget extends RenderObjectWidget

abstract class RenderObjectWidget extends Widget

可以发现,它们都是继承RenderObjectWidget,既不是我们熟悉的statelessWidget,也不是statefulWidget

RenderObjectWidget

RenderObjectWidget跟我们熟知的widget不一样,看下它的createElement方法

代码语言:javascript
复制
RenderObjectElement createElement(); 

它生成的是RenderObjectElement,跟之前的ComponentElement是什么区别呢

ComponentElement是为了组建出其他的element,本身不会生成RenderObject,而RenderObjectElement会生成最终RenderObject,最终负责布局跟绘制的,正是RenderObject ComponentElement并不会参与最终的绘制,只是起到一个桥梁的作用

通过它的源码,也可以看到上面的差别,看下RenderObjectElement的mount方法

代码语言:javascript
复制
void mount(Element? parent, Object? newSlot) {           
  super.mount(parent, newSlot);                          
  _renderObject = widget.createRenderObject(this);       
                            
  attachRenderObject(newSlot);                        
}                                                        

先调用createRenderObject,生成一个renderObject,然后再调用attachRenderObject,把这个renderObject加到renderObject树中

回到container build生成的ConstrainedBox

代码语言:javascript
复制
@override                                                            
RenderConstrainedBox createRenderObject(BuildContext context) {      
  return RenderConstrainedBox(additionalConstraints: constraints);   
}          

class RenderConstrainedBox extends RenderProxyBox

RenderConstrainedBox虽然最终也是继承RenderObject,又实现了performLayout,但是没有实现paint方法,说明ConstrainedBox有参与layout布局,但是没有参与最终的绘制,绘制还是由它的child来执行

performLayout

flutter在大多数设备上,都是60帧的刷新,大概16ms刷新一次,所以底层engine会固定频率,发送一个刷新的回调SchedulerBinding.handleDrawFrame,performLayout就是在这个刷新回调中被调用到的

继续看下RenderConstrainedBoxperformLayout的具体代码

代码语言:javascript
复制
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)

代码语言:javascript
复制
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

代码语言:javascript
复制
RenderObject createRenderObject(BuildContext context) {  
  return _RenderColoredBox(color: color);                
}                                                        

接着调用_RenderColoredBoxlayout方法

代码语言:javascript
复制
void layout(Constraints constraints, { bool parentUsesSize = false }) {
    _constraints = constraints;
    try {
      performLayout();
      markNeedsSemanticsUpdate();
    } catch (e, stack) {
    }
    _needsLayout = false;
    markNeedsPaint();
  }

还是调用到了_RenderColoredBoxperformLayout方法

代码语言:javascript
复制
void performLayout() {                               
  if (child != null) {                               
    child!.layout(constraints, parentUsesSize: true);
    size = child!.size;                              
  } else {                                           
    size = computeSizeForNoChild(constraints);       
  }                                                  
}                                                    

由于_RenderColoredBox已经没有child的了,上面的代码会走到了else里面

代码语言:javascript
复制
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

代码语言:javascript
复制
void drawFrame() {   
  //这里触发layout,计算布局跟大小
  pipelineOwner.flushLayout(); 
  pipelineOwner.flushCompositingBits()
  //这里触发绘制,真正内容绘制到canvas上
  pipelineOwner.flushPaint();     
}                                     

通过上面的代码,参与最终绘制的是_RenderColoredBox的paint方法

代码语言:javascript
复制
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都是在同个线程执行

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android码农 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 初始代码
  • container初始化
  • container的加载
  • RenderObjectWidget
  • performLayout
  • 绘制
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档