深色模式
RenderBox 源码分析
RenderBox
介绍
一个2D笛卡尔坐标系统中的渲染对象。
每个盒子的[size]以宽度和高度表示。每个盒子都有自己的坐标系统,其左上角位于(0,0)。 因此盒子的右下角位于(width, height)。盒子包含所有点,包括左上角并延伸到右下角, 但不包括右下角。
盒子布局是通过将[BoxConstraints]对象向下传递到树中来执行的。盒子约束为子项的宽度 和高度建立最小值和最大值。在确定其大小时,子项必须遵守其父项给予的约束。
这个协议足以表达许多常见的盒子布局数据流。例如,要实现宽度输入-高度输出的数据流, 可以用一组具有固定宽度值的盒子约束调用子项的[layout]函数(并为parentUsesSize传递true)。 在子项确定其高度后,使用子项的高度来确定你的大小。
编写RenderBox子类:
实现新的[RenderBox]子类的目的是描述新的布局模型、新的绘制模型、新的命中测试模型或 新的语义模型,同时保持在[RenderBox]协议定义的笛卡尔空间内。
要创建新协议,请考虑继承[RenderObject]。
新RenderBox子类的构造函数和属性
构造函数通常为类的每个属性接收一个命名参数。该值随后被传递给类的私有字段, 构造函数会断言其正确性(例如,如果它不应为空,则断言它不为空)。
属性具有如下形式的getter/setter/field组:
dart
AxisDirection get axis => _axis;
AxisDirection _axis = AxisDirection.down; // 或在构造函数中初始化
set axis(AxisDirection value) {
if (value == _axis) {
return;
}
_axis = value;
markNeedsLayout();
}
setter通常以调用[markNeedsLayout]结束(如果布局使用此属性),或调用[markNeedsPaint] (如果只有painter函数使用)。(无需同时调用两者,[markNeedsLayout]隐含了[markNeedsPaint]。)
考虑到布局和绘制是昂贵的操作;要保守地调用[markNeedsLayout]或[markNeedsPaint]。 只有在布局(或绘制)实际发生变化时才应调用它们。
子项
如果渲染对象是叶子节点,即不能有任何子项,则可以忽略本节。(叶子渲染对象的例子有 [RenderImage]和[RenderParagraph]。)
对于有子项的渲染对象,有四种可能的场景:
单个[RenderBox]子项。在这种情况下,考虑继承[RenderProxyBox](如果渲染对象调整自身 大小以匹配子项)或[RenderShiftedBox](如果子项将小于盒子且盒子将在其内部对齐子项)。
单个子项,但它不是[RenderBox]。使用[RenderObjectWithChildMixin] mixin。
单个子项列表。使用[ContainerRenderObjectMixin] mixin。
更复杂的子项模型。
使用RenderProxyBox
默认情况下,[RenderProxyBox]渲染对象会调整自身大小以适应其子项,如果没有子项则尽可能 小;它将所有命中测试和绘制传递给子项,固有尺寸和基线测量同样代理给子项。
[RenderProxyBox]的子类只需要重写[RenderBox]协议中关心的部分。例如,[RenderOpacity] 只重写paint方法(以及[alwaysNeedsCompositing]以反映paint方法的作用, 和[visitChildrenForSemantics]方法使得当子项不可见时对无障碍工具隐藏), 并添加[RenderOpacity.opacity]字段。
[RenderProxyBox]假设子项与父项大小相同且位于0,0位置。如果不是这样, 则使用[RenderShiftedBox]。
查看 proxy_box.dart 获取继承[RenderProxyBox]的示例。
使用RenderShiftedBox
默认情况下,[RenderShiftedBox]的行为很像[RenderProxyBox],但不假设子项位于0,0位置 (使用记录在子项[parentData]字段中的实际位置),也不提供默认布局算法。
查看 shifted_box.dart 获取继承[RenderShiftedBox]的示例。
子项类型和子项特定数据
[RenderBox]的子项不一定要是[RenderBox]。可以为[RenderBox]的子项使用[RenderObject] 的其他子类。详见[RenderObject]的讨论。
子项可以有父项拥有但存储在子项上的额外数据,使用[parentData]字段。用于该数据的类 必须继承自[ParentData]。[setupParentData]方法用于在子项附加时初始化子项的[parentData]字段。
按照惯例,具有[RenderBox]子项的[RenderBox]对象使用[BoxParentData]类,该类有一个 [BoxParentData.offset]字段来存储子项相对于父项的位置。([RenderProxyBox]不需要 这个偏移量,因此是这个规则的一个例外。)
使用RenderObjectWithChildMixin
如果渲染对象有单个子项但它不是[RenderBox],那么[RenderObjectWithChildMixin]类 (这是一个处理管理子项样板代码的mixin)将会很有用。
它是一个泛型类,有一个类型参数,即子项的类型。例如,如果你正在构建一个RenderFoo
类, 它接受一个单个RenderBar
子项,你可以这样使用mixin:
dart
class RenderFoo extends RenderBox
with RenderObjectWithChildMixin<RenderBar> {
// ...
}
由于在这种情况下RenderFoo
类本身仍然是一个[RenderBox],你仍然需要实现[RenderBox] 布局算法,以及固有尺寸和基线、绘制和命中测试等功能。
使用ContainerRenderObjectMixin
如果渲染盒可以有多个子项,那么可以使用[ContainerRenderObjectMixin] mixin来处理样板代码。 它使用链表来模型化子项,这种方式易于动态修改并且可以高效遍历。在这个模型中随机访问 效率不高;如果你需要随机访问子项,请考虑下一节关于更复杂子项模型的内容。
[ContainerRenderObjectMixin]类有两个类型参数。第一个是子对象的类型。第二个是它们的 [parentData]类型。用于[parentData]的类必须自身混入[ContainerParentDataMixin]类; 这是[ContainerRenderObjectMixin]存储链表的地方。[ParentData]类可以扩展 [ContainerBoxParentData];这本质上是[BoxParentData]混入了[ContainerParentDataMixin]。 例如,如果一个RenderFoo
类想要有一个[RenderBox]子项的链表,可以这样创建 FooParentData
类:
dart
class FooParentData extends ContainerBoxParentData<RenderBox> {
// (这些子项可能需要的任何字段)
}
在[RenderBox]中使用[ContainerRenderObjectMixin]时,考虑混入 [RenderBoxContainerDefaultsMixin],它提供了一组实现[RenderBox]协议常用部分的 实用方法(比如绘制子项)。
因此,RenderFoo
类本身的声明将如下所示:
dart
// 继续前面的示例...
class RenderFoo extends RenderBox with
ContainerRenderObjectMixin<RenderBox, FooParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, FooParentData> {
// ...
}
当遍历子项时(例如在布局期间),通常使用以下模式(在这种情况下假设所有子项都是 [RenderBox]对象,并且这个渲染对象为其子项的[parentData]字段使用FooParentData
对象):
dart
// 继续前面的示例...
RenderBox? child = firstChild;
while (child != null) {
final FooParentData childParentData = child.parentData! as FooParentData;
// ...操作child和childParentData...
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
更复杂的子项模型
渲染对象可以有更复杂的模型,例如以枚举为键的子项映射、可高效随机访问的2D子项网格、 多个子项列表等。如果渲染对象有一个无法通过上述mixin处理的模型,它必须按如下方式 实现[RenderObject]子项协议:
任何时候移除子项时,都要用该子项调用[dropChild]。
任何时候添加子项时,都要用该子项调用[adoptChild]。
实现[attach]方法,使其在每个子项上调用[attach]。
实现[detach]方法,使其在每个子项上调用[detach]。
实现[redepthChildren]方法,使其在每个子项上调用[redepthChild]。
实现[visitChildren]方法,使其对每个子项调用其参数,通常按绘制顺序(从后到前)。
实现[debugDescribeChildren],使其为每个子项输出一个[DiagnosticsNode]。
实现这七点基本上就是前面提到的两个mixin所做的全部工作。
布局
[RenderBox]类实现布局算法。它们接收一组约束,并基于这些约束以及它们可能有的 任何其他输入(例如,它们的子项或属性)来确定自身大小。
在实现[RenderBox]子类时,必须做出选择。它是完全基于约束来确定自身大小,还是 使用其他信息来确定大小?完全基于约束确定大小的一个例子是增长以适应父项。
完全基于约束确定大小允许系统进行一些重要的优化。使用这种方法的类应该重写 [sizedByParent]返回true,然后重写[computeDryLayout]仅使用约束来计算[Size],例如:
dart
@override
bool get sizedByParent => true;
@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.smallest;
}
否则,大小在[performLayout]函数中设置。
[performLayout]函数是渲染盒决定(如果它们不是[sizedByParent])它们应该是什么 [size]的地方,也是它们决定它们的子项应该在哪里的地方。
RenderBox子项的布局
[performLayout]函数应该调用每个(盒子)子项的[layout]函数,传递一个[BoxConstraints] 对象,描述子项可以渲染的约束。向子项传递紧约束(参见[BoxConstraints.isTight]) 将允许渲染库应用一些优化,因为它知道如果约束是紧的,即使子项本身的布局改变, 子项的尺寸也不能改变。 如果[performLayout]函数将使用子项的大小来影响布局的其他方面,例如如果渲染盒围绕 子项调整大小,或基于这些子项的大小来定位多个子项,那么它必须指定子项的[layout] 函数的parentUsesSize
参数,将其设置为true。
这个标志会关闭一些优化;不依赖子项大小的算法将更有效率。(特别是,依赖子项的[size] 意味着如果子项被标记为需要布局,父项可能也会被标记为需要布局,除非父项给予子项的 [constraints]是紧约束。)
对于不继承自[RenderProxyBox]的[RenderBox]类,一旦它们布局了它们的子项,它们也应该 定位它们,方法是设置每个子项的[parentData]对象的[BoxParentData.offset]字段。
非RenderBox子项的布局
[RenderBox]的子项不必是[RenderBox]。如果它们使用另一个协议(如[RenderObject]中 所讨论的),那么父项不会传递[BoxConstraints],而是传递适当的[Constraints]子类, 并且父项不会读取子项的大小,而是读取该布局协议的[layout]输出。parentUsesSize
标志仍用于指示父项是否要读取该输出,如果子项有紧约束(由[Constraints.isTight] 定义),优化仍然会生效。
绘制
要描述渲染盒如何绘制,需实现[paint]方法。它接收一个[PaintingContext]对象和一个 [Offset]。绘制上下文提供影响图层树的方法以及可用于添加绘制命令的[PaintingContext.canvas]。 canvas对象不应在调用[PaintingContext]的方法之间缓存;每次调用[PaintingContext] 的方法时,canvas都可能改变标识。offset指定盒子左上角在[PaintingContext.canvas] 坐标系中的位置。
要在canvas上绘制文本,使用[TextPainter]。
要在canvas上绘制图像,使用[paintImage]方法。
使用[PaintingContext]方法引入新图层的[RenderBox]应该重写[alwaysNeedsCompositing] getter并将其设置为true。如果对象有时需要有时不需要,它可以在某些情况下返回true, 在其他情况下返回false。在这种情况下,每当返回值要改变时,调用 [markNeedsCompositingBitsUpdate]。(当添加或删除子项时,这会自动完成,所以如果 [alwaysNeedsCompositing] getter仅基于子项的存在或不存在而改变值,你不需要显式 调用它。) 任何时候对象上发生的变化会导致[paint]方法绘制不同的内容(但不会导致布局改变), 对象都应该调用[markNeedsPaint]。
绘制子项
[paint]方法的context
参数有一个[PaintingContext.paintChild]方法,应该为每个 要绘制的子项调用它。它应该给出对子项的引用,以及一个[Offset],给出子项相对于 父项的位置。
如果[paint]方法在绘制子项之前对绘制上下文应用变换(或通常应用超出其本身作为参数 接收的偏移量的额外偏移量),那么也应该重写[applyPaintTransform]方法。该方法必须 以与它在绘制给定子项之前转换绘制上下文和偏移量相同的方式调整给定的矩阵。这被 [globalToLocal]和[localToGlobal]方法使用。
命中测试
渲染盒的命中测试通过[hitTest]方法实现。此方法的默认实现委托给[hitTestSelf]和 [hitTestChildren]。在实现命中测试时,你可以重写这后两个方法,或者忽略它们而 直接重写[hitTest]。
[hitTest]方法本身接收一个[Offset],并且必须返回true如果对象或其子项之一已吸收 命中(防止此对象下面的对象被命中),或false如果命中可以继续到此对象下面的其他 对象。
对于每个子[RenderBox],应该用相同的[HitTestResult]参数调用子项的[hitTest]方法, 并且点应该转换到子项的坐标空间(以与[applyPaintTransform]方法相同的方式)。 默认实现委托给[hitTestChildren]来调用子项。[RenderBoxContainerDefaultsMixin] 提供了一个[RenderBoxContainerDefaultsMixin.defaultHitTestChildren]方法, 假设子项是轴对齐的、未转换的,并根据[parentData]的[BoxParentData.offset]字段 定位;更复杂的盒子可以相应地重写[hitTestChildren]。 如果对象被命中,那么它也应该将自己添加到作为[hitTest]方法参数的[HitTestResult] 对象中,使用[HitTestResult.add]。默认实现委托给[hitTestSelf]来确定盒子是否被命中。 如果对象在子项可以添加自己之前添加自己,那么就好像对象在子项之上。如果它在子项之后 添加自己,那么就好像它在子项之下。添加到[HitTestResult]对象的条目应该使用 [BoxHitTestEntry]类。系统随后按添加顺序遍历这些条目,对于每个条目,调用目标的 [handleEvent]方法,传入[HitTestEntry]对象。
命中测试不能依赖于绘制已经发生。
语义
为了使渲染盒可访问,需实现[describeApproximatePaintClip]、[visitChildrenForSemantics] 和[describeSemanticsConfiguration]方法。对于仅影响布局的对象,默认实现就足够了, 但代表交互组件或信息(图表、文本、图像等)的节点应该提供更完整的实现。更多信息, 请参见这些成员的文档。
固有尺寸和基线
布局、绘制、命中测试和语义协议对所有渲染对象都是通用的。[RenderBox]对象必须实现 两个额外的协议:固有尺寸和基线测量。
有四个方法需要实现用于固有尺寸,以计算盒子的最小和最大固有宽度和高度。这些方法的 文档详细讨论了协议:[computeMinIntrinsicWidth]、[computeMaxIntrinsicWidth]、 [computeMinIntrinsicHeight]、[computeMaxIntrinsicHeight]。
如果你确实重写了这些方法中的任何一个,请确保在单元测试中将[debugCheckIntrinsicSizes] 设置为true,这将添加额外的检查来帮助验证你的实现。
此外,如果盒子有任何子项,它必须实现[computeDistanceToActualBaseline]。 [RenderProxyBox]提供了一个简单的实现,将其转发给子项;[RenderShiftedBox]提供了 一个实现,通过子项相对于父项的位置来偏移子项的基线信息。但是,如果你没有继承这些 类中的任何一个,你必须自己实现算法。
_computeIntrinsics()
参数:
type
: 一个_CachedLayoutCalculation
类型的参数,用于指定计算类型input
: 输入参数computer
: 一个函数,接收Input
类型参数并返回Output
类型结果
主要功能:
这是一个用于计算和缓存布局相关值的工具方法,方法会根据条件决定是否使用缓存
dart
bool shouldCache = true;
assert(() {
shouldCache = !RenderObject.debugCheckingIntrinsics;
return true;
}());
如果需要缓存(shouldCache = true
),则调用_computeWithTimeline
:
dart
return shouldCache ? _computeWithTimeline(type, input, computer) : computer(input);
如果不需要缓存,则直接执行计算函数computer(input)
使用场景:
- 计算内在尺寸(intrinsic dimensions)
- 计算基线位置(baseline)
- 进行"干"布局计算(dry layout)
被以下几个方法调用:
getMinIntrinsicWidth()
getMaxIntrinsicWidth()
getMinIntrinsicHeight()
getMaxIntrinsicHeight()
getDryLayout()
getDryBaseline()
getMinIntrinsicWidth()
作用
用于获取渲染框在不裁剪内容的情况下可以正确绘制其内容的最小宽度。
特点
- 只能在父组件上调用其子组件的此方法
- 调用此方法会将子组件与父组件耦合,当子组件布局改变时会通知父组件
- 性能开销较大,可能导致O(N²)的行为
- 不应该被重写,而是应该重写
computeMinIntrinsicWidth()
调用链
getMinIntrinsicWidth()
-> _computeIntrinsics()
-> computeMinIntrinsicWidth()
computeMinIntrinsicWidth()
作用
计算getMinIntrinsicWidth()
返回的值。
特点
- 应该被实现
performLayout()
的子类重写 height
参数在以下情况可以被忽略:- 布局算法与上下文无关(如总是尝试特定大小)
- 布局算法是width-in-height-out
- 布局算法同时使用传入的宽度和高度约束
height
参数在以下情况需要被使用:- 布局算法是严格的height-in-width-out
- 布局算法在宽度不受约束时是height-in-width-out
- 返回值不能为负数或无限大
- 如果算法依赖子组件的固有尺寸,应该使用
get
开头的方法获取,而不是compute
开头的方法
getMaxIntrinsicWidth()
作用
返回增加宽度之后,再增加宽度不会减少首选高度的最小宽度。
首选高度是指在该宽度下通过computeMinIntrinsicHeight计算得到的值。
getMaxIntrinsicWidth()
与getMaxIntrinsicWidth()
的联系
概念
返回渲染框在不裁剪内容的情况下能够正确绘制其内容所需的最小宽度
这是组件在不破坏布局的情况下可以被压缩到的最小宽度
数值
返回一个宽度值,超过这个宽度后,增加宽度不会减少首选高度
这通常是组件的"理想"或"自然"宽度
举例
对于
Text("Hello World")
getMinIntrinsicWidth
: 返回最长单词的宽度("Hello"或"World"中较长的那个)getMaxIntrinsicWidth
: 返回整个文本在单行显示时的宽度("Hello World"的完整宽度)
调用链
getMaxIntrinsicWidth()
-> _computeIntrinsics()
-> computeMaxIntrinsicWidth()
computeMaxIntrinsicWidth()
作用
计算getMaxIntrinsicWidth()
的结果。
特点
height
参数不会为负数或null
,但可能为无限大- 返回值不能为负数或无限大
- 返回值应该大于或等于
computeMinIntrinsicWidth()
的返回值 - 如果布局算法是严格的height-in-width-out,则应该返回与
computeMinIntrinsicWidth()
相同的值 - 如果需要获取子组件的固有尺寸,应使用
get
开头的方法而不是compute
开头的方法
getMinIntrinsicHeight()、computeMinIntrinsicHeight()
调用链
getMinIntrinsicHeight()
-> _computeIntrinsics()
-> computeMinIntrinsicHeight()
getMaxIntrinsicHeight()、computeMaxIntrinsicHeight()
调用链
getMaxIntrinsicHeight()
-> _computeIntrinsics()
-> computeMaxIntrinsicHeight()
getDryLayout()
作用
- 用于预计算在特定约束下渲染框的大小
- 不会改变渲染对象的任何内部状态,不触发实际布局
- 返回的大小保证与在相同约束下实际布局计算的大小一致
特点
- 它被称为"干"布局(dry layout),与常规的"湿"布局(wet layout)相对。
- 只能在父节点上调用其子节点的此方法
- 调用此方法会建立父子节点之间的布局依赖关系
- 当子节点布局发生变化时,会通过
markNeedsLayout()
通知父节点 - 调用此方法代价较高,可能导致 O(N²) 的时间复杂度
- 不应直接重写此方法,而是实现
computeDryLayout
代码示例
dart
RenderBox child = ...;
BoxConstraints constraints = ...;
// 获取子节点在给定约束下的预期大小
Size childSize = child.getDryLayout(constraints);
// 使用计算得到的大小进行进一步处理
// 注意:这不会改变子节点的实际大小
调用链
getDryLayout()
-> _computeIntrinsics()
-> _computeDryLayout()
-> computeDryLayout()
computeDryLayout()
作用
供getDryLayout()
调用,计算RenderBox
在给定约束条件下的期望尺寸,但不改变任何内部状态。
特点
不应直接调用此方法,而应使用
getDryLayout()
这是一个受保护的方法,意味着只能被子类访问和重写
需要在以下情况下重写此方法:
- 实现了
performLayout()
的子类 - 实现了
performResize()
的子类 - 设置
sizedByParent
为true
但没有重写performResize()
的情况
- 实现了
返回的
Size
必须与RenderBox
在performLayout()
或performResize()
中最终计算的实际size
相匹配如果依赖子元素的尺寸,应该使用子元素的
getDryLayout()
方法获取
getDryBaseline()
作用
计算渲染盒子(RenderBox
)基线位置的方法。它返回从盒子顶部到其内容第一个基线的距离。
特点
- 返回类型
double?
表示基线距离,null
代表没有基线 - 通常由父
RenderBox
在其computeDryBaseline()
或computeDryLayout()
实现中调用 - 子类不应该重写此方法,应该重写
computeDryBaseline()
调用链
getDryBaseline()
-> _computeIntrinsics()
-> _computeDryBaseline()
-> computeDryBaseline()
computeDryBaseline()
作用
供getDryBaseline()
调用。
_size
, get size
, set size
get size
RenderBox
可以随时访问自己的size
- 父节点只有在layout过程中明确声明了
parentUsesSize=true
时才能访问子节点的size
set size
只能在以下两种情况下设置size
:
- 如果
sizedByParent
为true
,必须在performResize()
中设置 - 如果
sizedByParent
为false
,必须在performLayout()
中设置
performResize()
默认使用computeDryLayout()
的结果,作为size
performLayout()
无实际逻辑