一文了解Flutter Widget体系结构

概述

widgets体系结构是学习Flutter中第一个重难点。本文不想去阐述widges的体系结构,因为太过于理论。主要是想通过理论加实践的方式让读者明白以下几点

  • 从理论层面知道Flutter是如何去布局的?
  • 在应用层面从众多可以实现的布局中快速筛选出最好最优的布局?
  • 可以轻松自定义widget

前置知识

状态

状态

理解状态是对Flutter Widgets甚至整个Flutter的设计很重要的一环,说说我的几点理解。

私货: 我平时估工作量主要有两个关键点状态数和数据复杂度

  • 数据流动导致状态改变
  • 状态是数据流动的展现形式,在开发中甚至可以直接用数据表示一个状态

几个结论

从文章开头可以先给出几个结论,读者在后文自行验证

  • Flutter的布局基于Widget,但渲染基于RenderObject,所以有些布局看着很深但是实际性能较高
  • Flutter的布局从代码上看就是基于各个widget class的构造器,构造器的输入是(布局数据, 子Widget, 回调),其中布局数据用于确定自身的UI属性。
  • Flutter的Widget分三类,不带孩子的,带一个孩子的和带多个孩子的。
  • dart中有比较多的语法特性刚好适用于Flutter这样的布局模式
  • StatelessWidget表示这个Widgets只有一个状态

Flutter如何将你定义的Widget绘制上去的

到这一章同学应该了解了如何在Flutter里进行布局,这一部分我们再深一层从Framework看。其实Flutter是通过Widget Tree将你写的布局保存在一棵树上,然后对该树的每个节点映射一个Element节点又形成一棵树Element Tree,此树用于控制Widget Tree的各种状态,最后某些Element Tree上的节点映射一个RenderObject类型的节点形成一棵树RenderObject Tree, 此树负责实际的测量,布局和渲染。

三棵树

FrameWork中有关UI布局绘制的有三棵树分别是【Widget Tree, Element Tree, RenderObject Tree】, 可以对应理解为一个建筑工程中的【设计师,项目经理,建筑工人】。

  • Widget Tree用于整体的布局的动态配置(statefulWidget)当然也可以是静态配置(statelessWidget)

深入: Widget Tree是应用开发者根据业务需求写出来的,就像配置文件定了就定了,不会像android或者js一样提供动态操作树的能力,但不代表在Widget Tree中没有动态的能力,就像android中.gralde文件一样,其配置是根据输入的数据定下来的。这里也一样你可以在Widget Tree写出类似if else的代码提供动态能力,或者说让Widget Tree有了更多的状态。如下图:

Widget状态解释
  • Element Tree负责Widget的生命周期,管理父子关系

深入:这颗树是Flutter本身自己实现的,其内部提供了动态操作树的能力,比如mount就是添加(挂载)树的根节点,deactivateChild就是移除孩子节点。另外每个Elmenet Tree中的节点都持有对应的Widget,这个Widget的引用用于管理Widget的生命周期比如initState,build等。

  • RenderObject Tree负责确定大小并渲染

深入:此树可以对比android中的View Tree,负责测量,布局和渲染,其和Widget Tree不是一对一的关系,因为有些Widget就是单纯的配置Widget,比如Expand,下图展示了此树和上面两颗树的对应关系

三棵树的对应关系

Flutter中测量,布局与渲染

概述

前面小节提到RenderObject Tree负责测量,布局与渲染。其中测量在Flutter中和布局是一体的,渲染大部分情况比较偏底层,所以布局是这块的核心。理解了布局,同学就可以轻松的选用,组合甚至优化各种Widget 本小结主要聊聊布局的事

概念
  • 布局方向:和android一样,笛卡尔坐标系,方向也是手机(left, top)为原点
  • 主轴: 主布局方向,比如Col主布局方向是竖向所以他的主轴就是竖向
  • 交叉轴:除了主轴的另一个轴
  • 紧约束(Tight):强制子布局的宽高
  • 松约束(loose):子布局的大小要在我的控制的范围内就行了
Flutter中的布局流程

这一小节如果了解android的View tree的布局流程就特别好理解,Flutter中的布局流程和Android中基本一样。

资源:官方对布局流程的解释:https://flutter.cn/docs/development/ui/layout/constraints 官网用对话的形式解释布局流程很到位,推荐看看。

我觉得官方文档核心就是这三句话:

  • 上层widgetA向下层widgetB传递约束条件
  • 下层 widget B向上层 widget A 传递大小信息
  • 上层 widget A 决定下层 widget B 的位置

我们用如下图描述


递归布局流程

另外用如下表格看下其和android在约束条件上的异同点:

Flutter Android
最小宽度:minWidth 宽度测量模式:widthMode = MesureSpec.getMode(widthMeasureSpec)
最小高度:minHeight 高度测量模式:heightMode = MesureSpec.getMode(heightMeasureSpec)
最大宽度:maxWidth 最大宽度:width = MesureSpec.getSize(widthMeasureSpec).
最大高度:maxHeight 最大高度:height = MesureSpec.getSize(heightMeasureSpec)

对比观察可以发现都有最大宽高的约束。不同的是前面两项,其实从本质上来看前面两项也基本是一样的,因为可以通过minWidth和minHeight的取值推断出宽度高度的测量模式,如下表格列举了两个等价的例子

Flutter Android
minWidth = 500 & maxWidth = 500 width = 500 & widthMode = EXACLY
minWidth = 0 & maxWidth = double.infinity width = -1 & widthMode = AT_MOST
  • minWidth = 500 & maxWidth = 500 表示宽度只能为500
  • maxWidth = double.infinity: 表示child可以尽可能的大

下面用上面的理论来从源码角度分析一下官网的一个例子

ConstrainedBox(
    constraints: BoxConstraints(
        minWidth: 150, minHeight: 150, maxWidth: 150, maxHeight: 150),
    child: Container(color: red, width: 10, height: 10),

结论是红色的Container全部占满父布局,而不是150 * 150或者10 * 10的矩形,原因是ConstrainedBox对子节点施加了其父级的约束。

constrainedBox实验1结果.png

我们从源码来看一下原因,找到对应的RenderObject: RenderConstrainedBox,然后找PerformLayout()函数

@override
void performLayout() {
    // ConstrainedBox的父布局约束
    final BoxConstraints constraints = this.constraints;
    if (child != null) {
        //_additionalConstraints为ConstrainedBox参数中的约束,enforce函数为上述现象的原因
        child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
        size = child!.size;
    } else {
        size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
    }
}
BoxConstraints enforce(BoxConstraints constraints) {
    return BoxConstraints(
        // clamp意味着值一定在[low]-[high]之间
        // minWidth = 150, constraints.minWidth = 屏幕的宽,constraints.maxWidth = 屏幕的宽
        // 所以minWidth = 屏幕的宽
        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),
    );
}

那如何解决呢?包一层松约束的widget即可,比如Center

Center(
    child: ConstrainedBox(
        constraints: BoxConstraints(
            minWidth: 70, minHeight: 70, maxWidth: 150, maxHeight: 150),
        child: Container(color: red, width: 10, height: 10),
    ),
)
constrainedBox实验2结果.png

我们同样可以看看Center的performlayout()

/// shifted_box.dart -> RenderPositionedBox -> performLayout()
@override
void performLayout() {
    final BoxConstraints constraints = this.constraints;
    if (child != null) {
        // loosen函数即转constraints为松约束
        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();
    } 
}

最后以ColWidget的源码看一下总结一下布局流程,Col对应的RenderObject: RenderFlex

void performLayout() {
    // Col的上层Widget传递下来的约束
    final BoxConstraints constraints = this.constraints;
    // 计算Col的child的大小然后确认自己的大小即_LayoutSizes。这一步对应android中的onMeasure()
    final _LayoutSizes sizes = _computeSizes(
      layoutChild: ChildLayoutHelper.layoutChild,
      constraints: constraints,
    );

    final double allocatedSize = sizes.allocatedSize;
    double actualSize = sizes.mainSize;
    double crossSize = sizes.crossSize;
    double maxBaselineDistance = 0.0;
    size = constraints.constrain(Size(crossSize, actualSize));
    actualSize = size.height;
    crossSize = size.width;
    final double actualSizeDelta = actualSize - allocatedSize;
    _overflow = math.max(0.0, -actualSizeDelta);
    final double remainingSpace = math.max(0.0, actualSizeDelta);
    late final double leadingSpace;
    late final double betweenSpace;
    switch (_mainAxisAlignment) {
      case MainAxisAlignment.start:
        leadingSpace = 0.0;
        betweenSpace = 0.0;
        break;
      case MainAxisAlignment.end:
        leadingSpace = remainingSpace;
        betweenSpace = 0.0;
        break;
      case MainAxisAlignment.center:
        leadingSpace = remainingSpace / 2.0;
        betweenSpace = 0.0;
        break;
      case MainAxisAlignment.spaceBetween:
        leadingSpace = 0.0;
        betweenSpace = childCount > 1 ? remainingSpace / (childCount - 1) : 0.0;
        break;
      case MainAxisAlignment.spaceAround:
        betweenSpace = childCount > 0 ? remainingSpace / childCount : 0.0;
        leadingSpace = betweenSpace / 2.0;
        break;
      case MainAxisAlignment.spaceEvenly:
        betweenSpace = childCount > 0 ? remainingSpace / (childCount + 1) : 0.0;
        leadingSpace = betweenSpace;
        break;
    }

    // child在主轴的偏移量
    double childMainPosition = leadingSpace;
    RenderBox? child = firstChild;
    while (child != null) {
        final FlexParentData childParentData = child.parentData! as FlexParentData;
        // child在交叉轴的偏移量
        final double childCrossPosition;
        // 根据交叉轴布局方向计算child在交叉轴的偏移量
        switch (_crossAxisAlignment) {
            case CrossAxisAlignment.start:
            case CrossAxisAlignment.end:
                childCrossPosition = _startIsTopLeft(flipAxis(direction), textDirection, verticalDirection)
                                    == (_crossAxisAlignment == CrossAxisAlignment.start)
                                    ? 0.0
                                    : crossSize - _getCrossSize(child.size);
                break;
            case CrossAxisAlignment.center:
                childCrossPosition = crossSize / 2.0 - _getCrossSize(child.size) / 2.0;
                break;
            case CrossAxisAlignment.stretch:
                childCrossPosition = 0.0;
                break;
            case CrossAxisAlignment.baseline:
                if (_direction == Axis.horizontal) {
                assert(textBaseline != null);
                final double? distance = child.getDistanceToBaseline(textBaseline!, onlyReal: true);
                if (distance != null)
                    childCrossPosition = maxBaselineDistance - distance;
                else
                    childCrossPosition = 0.0;
                } else {
                childCrossPosition = 0.0;
                }
                break;
        }
        // 用parentData保存child的主轴和交叉轴上的偏移量,这一步才相当于android里的layout(),可以通过这个偏移量计算出child的具体位置
        childParentData.offset = Offset(childCrossPosition, childMainPosition);
        child = childParentData.nextSibling;
    }
  }
_LayoutSizes _computeSizes({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
    final double maxMainSize = _direction == Axis.horizontal ? constraints.maxWidth : constraints.maxHeight;
    double crossSize = 0.0;
    double allocatedSize = 0.0;
    RenderBox? child = firstChild;
    RenderBox? lastFlexChild;
    while (child != null) {
        // childParentData用于
        final FlexParentData childParentData = child.parentData! as FlexParentData;
        // 由于Col有自己的约束不能直接向其子布局传递Col父布局的约束,所以这里需要重新赋值
        final BoxConstraints innerConstraints;
        switch (_direction) {
            case Axis.horizontal:
                innerConstraints = BoxConstraints(maxHeight: constraints.maxHeight);
                break;
            case Axis.vertical:
                // 由于是`Col`布局方向是vertical所以走这里,查看源码可知这里是松约束
                // 这里给到的一个约束是父布局的最大宽度,意味着Col的child的宽度不能超过Col父布局给的宽度
                innerConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
                break;
        }
        // 这里看函数名可能会误以为是android中的layout,其实这一步只是测量child的size
        final Size childSize = layoutChild(child, innerConstraints);
        // 计算已经测量child的总的size
        allocatedSize += _getMainSize(childSize);
        crossSize = math.max(crossSize, _getCrossSize(childSize));
        child = childParentData.nextSibling;    
    }
   
    final double idealSize = allocatedSize;
     // allocatedSize,crossSize确定了自己的size
    return _LayoutSizes(
        mainSize: idealSize,
        crossSize: crossSize,
        allocatedSize: allocatedSize,
    );
}

小结:

  • 约束条件传递和android中的原理基本一致即childConstraint = f(SelfConstraint, ParentConstraint),f代表函数
  • 父布局决定自己大小的原理和android中原理也基本一致即先确定所有孩子的大小,然后才能确定自身的大小用伪代码表示selfSize = f(childs, padding, marigin, 布局模式)
  • 父布局layout自己和android中有些不一样,android中在onLayout的时候调用child.layout(x, y, width, height)去定位child的位置,而Flutter在layout中只用求出x,y即上述源码中的Offset,然后在paint这一步直接画。
  • 在代码层面和android的布局流程对比图如下


    和android绘制流程对比
  • ParentData可以存储child本生基于父布局的偏移信息和其兄弟节点
  • 看某个Widget的布局流程直接看Widget对应的RenderObject就可以了

常见Widget分析

同学们在写UI布局的时候,每选用一个Widget都应该在心里想想布局是如何约束的。毕竟Flutter没有实时预览(虽然它有热重载)。这其实对Widget的熟悉有比较高的要求,下面就用源码分析一下布局中常见的Widget

前置知识

所有需要渲染类型的Widget有三种

  • 叶子WidgetLeafRenderObjectWidget
  • 带有一个孩子SingleChildRenderObjectWidget
  • 多孩子的 MultiChildRenderObjectWidget

Container

  • 只是一个Widget的组合类容器,本身并不对应RenderObject, 不同条件下有不同widget
  • 使用最简单的组合的方式去自定义的一个widget
/// container中一个成员属性对应一个Widget,比如alignment -> Align, color -> ColoredBox
/// 多个属性情况下用嵌套组合的方式处理,当然里面涉及到嵌套的顺序
@override
Widget build(BuildContext context) {
    Widget? current = child;

    // ...

    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);

    // ...

    return current!;
}

调整布局中Widget的大小: Expand

  • 不属于渲染型的Widget,属性ProxyWidget
  • 作用相当于给子Widget添加Flex参数到其ParentData中,用于计算子Widget应该占用的空间
  • 源码分析
/// basic.dart -> Expanded

/// Using an [Expanded] widget makes a child of a [Row], [Column], or [Flex]
/// expand to fill the available space along the main axis
/// 上面官方注释的意思是Expanded大多数情况下是Row,Column,Flex的子节点,目的是为了占用剩余空间
class Expanded extends Flexible {
    // Expanded类特特别简单,接受一个flex参数即可,可类比于androidxml中的weight属性
    const Expanded({
        Key? key,
        int flex = 1,
        required Widget child,
    }) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);
}
/// basic.dart -> Flexible
class Flexible extends ParentDataWidget {
    const Flexible({
        Key? key,
        this.flex = 1,
        this.fit = FlexFit.loose,
        required Widget child,
    }) : super(key: key, child: child);

    /// 当Framework探查到Expand包裹的第一个渲染类型的Widget修改了或者新增了,会调用该函数
    @override
    void applyParentData(RenderObject renderObject) {
        assert(renderObject.parentData is FlexParentData);
        final FlexParentData parentData = renderObject.parentData! as FlexParentData;
        bool needsLayout = false;
        // 将flex参数放到parentData里,这个参数会用到其父布局中
        if (parentData.flex != flex) {
            parentData.flex = flex;
            needsLayout = true;
        }

        if (parentData.fit != fit) {
            parentData.fit = fit;
            needsLayout = true;
        }

        if (needsLayout) {
        final AbstractNode? targetParent = renderObject.parent;
        if (targetParent is RenderObject)
            targetParent.markNeedsLayout();
        }
    }
}
  • 图解
    Expand图解2

    RenderTree的计算过程是先计算出第二个Render Widget节点的Size,然后得到剩余Size: freeSpaceSize, 基于权重flex,得到边缘两个Render Widget节点Size: freeSpaceSize / 2, 最后从Row的第一个Render Widget开始布局,就形成了图顶部所给的布局样式。可以看到边缘两个Render Widget评分了剩余空间。

Align

  • 用于调整子组件位置
  • 可以根据子组件宽高调整自己的宽高
  • 使用覆盖的方式去自定义的Widget
  • Center基于Align
  • 源码分析
/// basic.dart -> Align
class Align extends SingleChildRenderObjectWidget {
    @override
    RenderPositionedBox createRenderObject(BuildContext context) {
        return RenderPositionedBox(
            alignment: alignment,
            widthFactor: widthFactor,
            heightFactor: heightFactor,
            textDirection: Directionality.maybeOf(context),
        );
    }
}
/// shifted_box.dart -> RenderPositionedBox
@override
void performLayout() {
    // 此处是父布局的constraints
    final BoxConstraints constraints = this.constraints;
    // widthFactor与Align自身宽高相关,如果为null,则填充父布局,如果不为空就给自己设置确定的宽高。下面代码可见
    final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
    final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;

    if (child != null) {
        // layout之后,就能得到child的size了
        child!.layout(constraints.loosen(), parentUsesSize: true);
        // 这里能看到_widthFactor的作用
        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,
        ));
    }
}
@protected
void alignChild() {
    _resolve();
    assert(child != null);
    assert(!child!.debugNeedsLayout);
    assert(child!.hasSize);
    assert(hasSize);
    assert(_resolvedAlignment != null);
    final BoxParentData childParentData = child!.parentData! as BoxParentData;
    // 确定孩子的位置, 跟Alignment相关的逻辑
    childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
}

Flutter中的滚动布局

  • Flutter给滑动布局做了一层Sliver的包裹
  • Sliver组件只应用在滚动布局中
  • 不同于其他约束类型,滚动布局中的约束类型传递的是SliverConstraints, 而最初的下发源头是在ViewPort对应的RenderViewPort
  • SliverConstraints记录包括滚动方向,子组件的偏移量等很多信息
  • 滚动的时候,只需要确定firstChild的offset和trailingChild的Offset就能确定所有RendSliverList中的所有child的具体位置了,这一点通过源码分析可以看到
  • 源码分析


    ListView滚动布局解析

    上图是ListView中重要的几个区域定义,懂了这几个定义就很好理解ListView的源码了。如下表格定义:

字段/区域 定义
grabage childs 回收区域,类似于android RecycleView中的Recycle
remaindExtent android和ios中也有类似的概念,此区域是预加载的部分区域
first child 很好理解,RenderSliveList中的第一个孩子节点
viewPort 很好理解,你看到的区域

下面看下关键源码

void performLayout() {
    final SliverConstraints constraints = this.constraints;
    // 这里可以看到scrollOffset是有上层的Widget确定,滚动的触发是上层Widget触发的,RenderSliveList只负责根据滚动scrollOffset调整自己子View的布局
    final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
    // 这里就是预留空间,是上层的约束中取值
    final double remainingExtent = constraints.remainingCacheExtent;
    earliestUsefulChild = firstChild;
    // 下面的循环是找到滚动后的firstChild的Offset(比如你ListView向上滚动的时候)
    for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!;
        earliestScrollOffset > scrollOffset;
        earliestScrollOffset = childScrollOffset(earliestUsefulChild)!) {
            // insertAndLayoutLeadingChild()函数便是向上从grabage childs里寻找item
            earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
            if(earliestUsefulChild == null) {
                //...
                break;
            }
            // 找到了就直接设置childParentData的offset为firstChildScrollOffset
            final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!);
            final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData;
            childParentData.layoutOffset = firstChildScrollOffset;
        }
    RenderBox? child = earliestUsefulChild;
    double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
    bool advance() { 
        // index + 1,找下一个child
        child = childAfter(child!);
        final SliverMultiBoxAdaptorParentData childParentData = child!.parentData! as SliverMultiBoxAdaptorParentData;
        // 给该child赋值offset
        childParentData.layoutOffset = endScrollOffset;
        endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
        return true;
    }

    // 这个循环就是同advance去确定从firstChild到trailingChild的Offset
    while (endScrollOffset < scrollOffset) {
      leadingGarbage += 1;
      if (!advance()) {
        // 做一些回收的逻辑,此处略过
        collectGarbage(leadingGarbage - 1, 0);
        return;
      }
    }
}

Flutter是如何更新RendObject Tree的?

前面章节只描述了初始状态的RendObject Tree,那Flutter是如何去更新RendObject Tree或者说更新UI的呢?先看个gif动画了解大概吧

flutter更新Tree动画V2.gif

关键四个函数

  • 更新入口setState():重新执行build的触发点,就是表明Widget Tree的状态变了
  • 标脏函数markNeedToPaint(): setState后需要对Element Tree上的节点进行从下至上的标脏处理,但是如果遇到isRepaintBoundary == true的节点,则不再向上表脏,这个特性给优化提供Flutter视图性能提供了空间
  • 构建回调函数build(): 标脏完成后对标脏节点从上到下依次build,就是执行你重写Widgetbuild函数。
  • Diff功能函数canUpdate():build函数返回对对Widget Tree的Diff结果,依据Diff结果对Element TreeRedenerObject Tree做相应的处理。具体逻辑可以看下面流程图
    更新子树的四种可能

如何自定义Widget

自定义Widget有三种方式

  • 组合
  • 覆盖
  • 完全自定义即继承RenderBox

组合

最简单的自定义Widget的形式,源码中的Container即为组合的形式。

覆盖

  • 覆盖CustomSingleChildLayout构造函数的delegate:可以理解为对单孩子的layout阶段进行hook

查看源码可以可知CustomSingleChildLayout也是一个SingleChildRenderObjectWidget, 你需要实现SingleChildLayoutDelegate


class MySingleChildLayoutDelegate extends SingleChildLayoutDelegate {
  @override
  bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) {
    throw UnimplementedError();
  }

  @override
  Size getSize(BoxConstraints constraints) {
    return super.getSize(constraints);
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    // 孩子基于父布局的偏移量
    return super.getPositionForChild(size, childSize);
  }

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return super.getConstraintsForChild(constraints);
  }
}

class CustomLayoutRoute extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return Center(
            child: CustomSingleChildLayout (
                delegate: MySingleChildLayoutDelegate(),
                child: Text("123")
            ),
        );
    }
}
  • 覆盖CustomMultiChildLayout构造函数的delegate:可以理解为对多孩子的layout阶段进行hook

模板实例如下

class MyMultiChildLayoutDelegate extends MultiChildLayoutDelegate {
  final List layoutIds;
  MyMultiChildLayoutDelegate(this.layoutIds);

  @override
  void performLayout(Size size) {
    // 这里需要自己对child进行layout,但是拿不到child的值. 此处只能通过layoutId布局
    // layoutChild(childId, constraints)
    for (final layoutId in layoutIds) {
      layoutChild(layoutId, BoxConstraints().loosen());
    }
  }

  @override
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) {
    throw UnimplementedError();
  }
}

class CustomMultiRoute extends StatelessWidget {
  final layoutIds = [1, 2, 3, 4];
  @override
  Widget build(BuildContext context) {
    return Center(
      child: CustomMultiChildLayout(
        delegate: MyMultiChildLayoutDelegate([1, 2, 3, 4]),
        children: [
          // 注意这里需要用LayoutId这个ProxWidget将id值保存到MyMultiChildLayoutDelegate的ParentData里
          LayoutId(id: layoutIds[0], child: Text("0")),
          LayoutId(id: layoutIds[1], child: Text("1")),
          LayoutId(id: layoutIds[2], child: Text("2")),
          LayoutId(id: layoutIds[3], child: Text("3")),
        ],
      ),
    );
  }
}

  • 继承CustomPaint可以理解为对Paint阶段进行hook,类似android中的onDraw

查看源码可知CustomPaint是一个SingleChildRenderObjectWidget, 你只需要传CustomPainter类的实例即可

class CustomPaintRoute extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return Center(
            child: CustomPaint(
                size: Size(300, 300), //指定画布大小
                painter: MyPainter(),
            ),
        );
    }
}
class MyPainter extends CustomPainter {
     @override
  void paint(Canvas canvas, Size size) {
      // 业务逻辑
  }
}

总结

应该说Widget体系结构是Flutter中的核心,它从Framework层面上阐述了Flutter是如何根据开发者定义的Widget结构去计算每个Widget的位置,大小等属性。另外和android不一样的是其用了三棵树去保证绘制效率,可以让用户主动设置不用重绘的区域以减少树的遍历。和android一样的是在RenderObject Tree中的布局约束传递和计算。最后来一句我的理解:这一切的一切的第一性原理是Flutter的布局形式是声明式布局

你可能感兴趣的:(一文了解Flutter Widget体系结构)