概述
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
有了更多的状态。如下图:
-
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对子节点施加了其父级的约束。
我们从源码来看一下原因,找到对应的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),
),
)
我们同样可以看看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();
}
}
最后以Col
Widget的源码看一下总结一下布局流程,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的布局流程对比图如下
- ParentData可以存储child本生基于父布局的偏移信息和其兄弟节点
- 看某个Widget的布局流程直接看Widget对应的RenderObject就可以了
常见Widget分析
同学们在写UI布局的时候,每选用一个Widget都应该在心里想想布局是如何约束的。毕竟Flutter没有实时预览(虽然它有热重载)。这其实对Widget的熟悉有比较高的要求,下面就用源码分析一下布局中常见的Widget
前置知识
所有需要渲染类型的Widget有三种
- 叶子Widget
LeafRenderObjectWidget
- 带有一个孩子
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();
}
}
}
- 图解
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的源码了。如下表格定义:
字段/区域 | 定义 |
---|---|
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动画了解大概吧
关键四个函数
- 更新入口
setState()
:重新执行build的触发点,就是表明Widget Tree
的状态变了 - 标脏函数
markNeedToPaint()
:setState
后需要对Element Tree
上的节点进行从下至上的标脏处理,但是如果遇到isRepaintBoundary == true
的节点,则不再向上表脏,这个特性给优化提供Flutter视图性能提供了空间 - 构建回调函数
build()
: 标脏完成后对标脏节点从上到下依次build
,就是执行你重写Widget
的build
函数。 - Diff功能函数
canUpdate()
:build函数返回对对Widget Tree
的Diff结果,依据Diff结果对Element Tree
和RedenerObject 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的布局形式是声明式布局