前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >flutter源码:布局

flutter源码:布局

作者头像
韦东锏
发布2022-11-07 13:11:09
4030
发布2022-11-07 13:11:09
举报
文章被收录于专栏:Android码农

flutter的widget是如何计算尺寸和位置的,通过一个非常简单的代码结合源码来分析

背景知识

1、widget树生成element树,element树生成RenderObject树,实际参与布局的就是RenderObject树,后续的源码分析也是针对RenderObject

2、flutter的布局约束,都是采用BoxConstraints来实现,一共有四个参数

代码语言:javascript
复制
// 最小宽度
final double minWidth;
// 最大宽度
final double maxWidth;
// 最小高度
final double minHeight;
// 最大高度
final double maxHeight;
代码语言:javascript
复制
bool get isTight => hasTightWidth && hasTightHeight;

当minWidth==maxWidth并且minHeight==maxHeight,则isTight是true,代表是严格约束,宽高的值就是确定的了

简单的代码

先看下代码,非常简单,就是屏幕中间展示一个黄色的色块,色块的长宽分别是100

代码语言:javascript
复制
  runApp(Center(
      child: Container(
    width: 100,
    height: 100,
    color: const Color(0xFFFF9000),
  )));

运行后的效果图如下

可以在dev tool看到最终生成的widget树如下

最外层的root

最外层的root是系统生成的,上面有说过,所有的布局都是由renderObject来处理,最外层的root的renderObject就是RenderView,布局的调用逻辑,就是由外层的RenderObject调用内层,一级级调用下去,最外层就是root的performLayout方法

代码语言:javascript
复制
  void performLayout() {
    assert(_rootTransform != null);
    _size = configuration.size;
    assert(_size.isFinite);

    if (child != null)
      child!.layout(BoxConstraints.tight(_size));
  }

上面的child就是Center这个widget生成的对应的renderObject,至于_size是由configuration.size决定的,configuration的生成代码如下

代码语言:javascript
复制
  ViewConfiguration createViewConfiguration() {
    final double devicePixelRatio = window.devicePixelRatio;
    final Size size = _surfaceSize ?? window.physicalSize / devicePixelRatio;
    return ViewConfiguration(
      size: size,
      devicePixelRatio: devicePixelRatio,
    );
  }

configuration是系统的屏幕分辨率除以像素比,比如我目前的手机一加10T来说,屏幕分辨率是1080*2352,屏幕像素比是3.0,最终的尺寸就是Size(360.0, 784.0) 继续看下BoxConstraints.tight方法,就是返回严格约束,说明root布局约束的长宽就是屏幕的尺寸

代码语言:javascript
复制
 BoxConstraints.tight(Size size)
    : minWidth = size.width,
      maxWidth = size.width,
      minHeight = size.height,
      maxHeight = size.height;

接下来,由最外层的root去加载我们的第一个widget:Center

center组件的布局

先看下Center组件的源码

代码语言:javascript
复制
class Center extends Align {
  /// Creates a widget that centers its child.
  const Center({ Key? key, double? widthFactor, double? heightFactor, Widget? child })
    : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}

center继承了Align组件,什么属性都没改,非常简单,因为Align组件默认align属性是:Alignment.center,所以原始的代码,我们把Center换成Align效果也是一样的

代码语言:javascript
复制
  runApp(Align(
      child: Container(
    width: 100,
    height: 100,
    color: const Color(0xFFFF9000),
  )));

Align组件对应的renderObject是RenderPositionedBox,看下它布局的代码

代码语言:javascript
复制
  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
    final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;

    if (child != null) {
      child!.layout(constraints.loosen(), parentUsesSize: true);
      size = constraints.constrain(Size(
        shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
        shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
      ));
      alignChild();
    } else {
      size = constraints.constrain(Size(
        shrinkWrapWidth ? 0.0 : double.infinity,
        shrinkWrapHeight ? 0.0 : double.infinity,
      ));
    }
  }

由于child不为空,shrinkWrapWidth跟shrinkWrapHeight都是false,上面的代码,我做个精简,如下

代码语言:javascript
复制
  void performLayout() {
  // 这里的constraints就是手机的屏幕尺寸
    final BoxConstraints constraints = this.constraints;
    // 让child先布局
    child!.layout(constraints.loosen(), parentUsesSize: true);
    // 最终的size就还是手机屏幕尺寸
      size = constraints.constrain(Size(double.infinity,
        double.infinity,
      ));
      // 计算子布局的offset
      alignChild();
  }

先让child去参与布局计算尺寸,最终child计算的尺寸是Size(100.0, 100.0)(稍后分析),Center组件最终的size就是手机屏幕的尺寸,也就是Size(360.0, 784.0),那是如何实现child居中的效果的呢?

这个是因为每个RenderObject有一个ParentData.offset参数,用于告诉父renderObject在绘制子的时候,子布局的偏移量

代码语言:javascript
复制
  /// The offset at which to paint the child in the parent's coordinate system.
  Offset offset = Offset.zero;

而这个offset的设值,就是在alignChild方法上

代码语言:javascript
复制
  void alignChild() {
    _resolve();
    final BoxParentData childParentData = child!.parentData! as BoxParentData;
    childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
  }

根据居中的规格来计算偏移量,就是(父布局的尺寸-子布局计算的尺寸)/2,刚好得出的就是居中的效果

代码语言:javascript
复制
// 居中效果的x,y值都是0
static const Alignment center = Alignment(0.0, 0.0);
// 计算的偏移量刚好就是居中效果
  Offset alongOffset(Offset other) {
    final double centerX = other.dx / 2.0;
    final double centerY = other.dy / 2.0;
    return Offset(centerX + x * centerX, centerY + y * centerY);
  }

最终得出的偏移值是Offset(130.0, 342.0),采用这个偏移量去绘制出来的时候,就是刚好居中了

container的布局分析

Center组件嵌套的child,是一个contaienr组件,container是statelessWidget,本身没有对应的RenderObject,具体是根据不同的情况,会生成不同的RenderObject,就我们这个例子而言,是生成了RenderConstrainedBox,最终调用的绘制方法如下

代码语言: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的值是BoxConstraints(0.0<=w<=360.0, 0.0<=h<=784.0),代表父类对子类的约束,就是你的尺寸最大不要超过我,最小的我不管,随便你多小,_additionalConstraints是额外的约束,是我们代码上写的约束信息,值是:BoxConstraints(w=100.0, h=100.0),代表是写死的宽高都是100;

_additionalConstraints.enforce(constraints)的结果也就是约束的100*100,这个就是给child的约束尺寸信息

这里要重点看下child!.layout方法,方法源码很长,我就截取关键的部分做下解析

代码语言:javascript
复制
void layout(Constraints constraints, { bool parentUsesSize = false }) {
    // 看后面分析
    RenderObject? relayoutBoundary;
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
    }
   
    if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
      // 符合条件,直接返回,避免重复计算布局,优化性能
      return;
    }
    _constraints = constraints;
    RenderObject? debugPreviousActiveLayout;
    
    try {
      performLayout();
    } catch (e, stack) {
      
    }
    // 已经布局过了,把layout标记设置为false
    _needsLayout = false;
    // 因为重新布局了,所以标记下需要绘制,触发重新绘制
    markNeedsPaint();
  }

layout方法有个parentUsesSize参数,代表父类是否需要基于child的大小来计算其自身的大小,默认是false,这里传的是true,代表父类的布局跟child的大小有关,这样当child被设置为需要重新layout的时候,也会触发父布局的重新layout

还有个relayoutBoundary参数,是用于判断当前布局是基于哪个RenderObject来计算的,在下面四种场景 1、其父布局不需要它的尺寸计算自身的尺寸 2、当前子布局尺寸是完全由父布局约束决定,子布局自己内部节点等都不影响最终的尺寸 3、约束是严格约束 4、父布局不是一个RenderObject 符合上面四种的一种,就代表relayoutBoundary就是它自己,其他情况下,relayoutBoundary就是父布局的relayoutBoundary

在接下来,会判断relayoutBoundary没变,_needsLayout是false,并且约束也没变,就不会重新去计算布局了,提升性能

代码语言:javascript
复制
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
      return;
    }

继续看下最内部的child,实际的类型是_RenderColoredBox,就是用于绘制颜色的方块,继续看下它的layout方法

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

constrains的值就是BoxConstraints(w=100.0, h=100.0),由于child已经是最里面布局了,它没有child了,代码走到else里

代码语言:javascript
复制
  Size computeSizeForNoChild(BoxConstraints constraints) {
    return constraints.smallest;
  }

返回当前约束下最小的尺寸,由于尺寸是tight类型,最小布局也是100,所以最终的布局尺寸就是100*100,也就是中间色块的大小

总结

最外面的root是由系统生成的,尺寸就是屏幕的大小,然后由root逐渐往里面遍历,只遍历一次,就计算出各组件的尺寸了,所有子组件的尺寸都是由父组件的约束条件,加上子组件对自身的规格计算出来的

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景知识
  • 简单的代码
  • 最外层的root
  • center组件的布局
  • container的布局分析
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档