阅读 354

图解Flutter Widgets体系结构及实践

概述

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中基本一样。

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

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

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

我们用如下图描述 递归布局流程

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

FlutterAndroid
最小宽度:minWidth宽度测量模式:widthMode = MesureSpec.getMode(widthMeasureSpec)
最小高度:minHeight高度测量模式:heightMode = MesureSpec.getMode(heightMeasureSpec)
最大宽度:maxWidth最大宽度:width = MesureSpec.getSize(widthMeasureSpec).
最大高度:maxHeight最大高度:height = MesureSpec.getSize(heightMeasureSpec)
对比观察可以发现都有最大宽高的约束。不同的是前面两项,其实从本质上来看前面两项也基本是一样的,因为可以通过minWidth和minHeight的取值推断出宽度高度的测量模式,如下表格列举了两个等价的例子
FlutterAndroid
minWidth = 500 & maxWidth = 500width = 500 & widthMode = EXACLY
minWidth = 0 & maxWidth = double.infinitywidth = -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对子节点施加了其父级的约束。

实验结论

我们从源码来看一下原因,找到对应的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),
    ),
)
复制代码

实验结论2

我们同样可以看看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<FlexParentData> {
    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
remaindExtentandroid和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的 关键四个函数

  • 更新入口setState():重新执行build的触发点,就是表明Widget Tree的状态变了
  • 标脏函数markNeedToPaint(): setState后需要对Element Tree上的节点进行从下至上的标脏处理,但是如果遇到isRepaintBoundary == true的节点,则不再向上表脏,这个特性给优化提供Flutter视图性能提供了空间
  • 构建回调函数build(): 标脏完成后对标脏节点从上到下依次build,就是执行你重写Widgetbuild函数。
  • Diff功能函数canUpdate():build函数返回对对Widget Tree的Diff结果,依据Diff结果对Element TreeRedenerObject Tree做相应的处理。具体逻辑可以看下面流程图
 // 这里通过判断key和runtimeType是否一致来判断是否可以更新UI
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
复制代码

更新子树的四种可能

如何自定义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<int> 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) {
      // 业务逻辑
  }
}
复制代码

布局实践 Flutter趟坑记录之Dialog满屏了😭

总结

应该说Widget体系结构是Flutter中的核心,它从Framework层面上阐述了Flutter是如何根据开发者定义的Widget结构去计算每个Widget的位置,大小等属性。另外和android不一样的是其用了三棵树去保证绘制效率,可以让用户主动设置不用重绘的区域以减少树的遍历。和android一样的是在RenderObject Tree中的布局约束传递和计算。这一切的一切的第一性原理是Flutter的布局形式是声明式布局。最后解答一下文章开头提出的三个问题结束本文 问:从理论层面知道Flutter是如何去布局的?并能够在没有实时预览的情况下提升布局准确率? 答:Flutter在使用者的维度用声明式的布局方式。在Framework的维度是通过三棵树的互相协作,关键是找到Widget对应的RenderObject中的PerfromLayout函数。另外自己在布局的时候要想清楚父布局约束和对子布局的约束,然后确定自己的约束,如果不熟悉,可以先看官方Widget API或者网上查资料。如果还有细节问题就需要看看PerfromLayout函数了。 问:如何在应用层面从众多可以实现的布局中快速筛选出最好最优的布局? 答:

  • 在合适的场景下(子布局更新不用更新父布局)使用文章提到RepaintBoundaryWidget包裹子Widget
  • 减少渲染型Widget的嵌套,在不熟悉的情况下,可以通过源码看看,是否其父类命名是否包含Render字符串
  • 通过《Flutter是如何更新RendObject Tree的?》这一小节可以知道,关键在于canUpdate是否触发,所以在编写Widget代码时,可以封装一些Widget然后保存起来,这样canUpdate返回false,就能减少遍历深度。或者用Dart中的Const特性。这里用个demo简单说下
class VarDemo {
  final String name;
  final int age;
  // 可以用const修饰
  const VarDemo(this.name, this.age);
}

void test1() {
  // 添加const
  final c1 = const VarDemo("zy", 12);
  final c2 = const VarDemo("zy", 12);
  // 检查是否是同一个实例
  print(identical(c1, c2));
}

main() {
  test1();
}
复制代码

问:如何轻松自定义widget? 答:通过三种方式,优先选用组合,其次选择覆盖,实在个性化要求太高就选择继承自RenderObject

文章分类
Android
文章标签