深色模式
RenderObject 源码分析
RenderObject 分类
RenderObject
的直接子类:
RenderObject
的 mixin
:
RenderBox
的子类:
RenderSliver
的子类:
RenderObject
分2类:
RenderBox
:基于盒子布局模型RenderSliver
:基于滑片布局模型
核心源码分析
RenderObject
介绍
渲染树中的一个对象
[RenderObject] 类层次结构是渲染库的核心,决定了它的存在意义。
[RenderObject] 有一个 [parent],并且有一个名为 [parentData] 的槽,父 [RenderObject] 可以在其中存储子项的特定数据,例如子项的位置。 [RenderObject] 类还实现了基本的布局和绘制协议。
然而, [RenderObject] 类并没有定义子节点模型(例如,节点是否有零个、一个或多个子节点)。它也没有定义坐标系统(例如,子节点是否以笛卡尔坐标系、极坐标系等进行定位),或者一个特定的布局协议(例如,布局是宽度和高度的关系、约束与大小的关系,或者父节点是在子节点布局之前还是之后设置子节点的大小和位置;或者是否允许子节点读取其父节点的 [parentData] 插槽)。
[RenderBox] 子类引入了一个观点,即布局系统使用笛卡尔坐标系。
生命周期
当 [RenderObject] 不再需要时,必须调用 [dispose] 方法。对象的创建者负责销毁它。通常,创建者是一个 [RenderObjectElement],该元素会在卸载时销毁它所创建的对象。
[RenderObject] 负责在调用 [dispose] 时清理它持有的任何昂贵资源,如 [Picture] 或 [Image] 对象。这包括它直接创建的任何 [Layer]。 [dispose] 方法的基本实现将使 [layer] 属性为空。子类还必须清除它直接创建的任何其他图层。
编写 [RenderObject] 子类
在大多数情况下,直接继承 [RenderObject] 是过度的,通常 [RenderBox] 是更好的起点。然而,如果一个渲染对象不想使用笛卡尔坐标系,那么它应该直接继承自 [RenderObject]。这允许它使用新的 [Constraints] 子类来定义自己的布局协议,而不是使用 [BoxConstraints],并且可以使用一整套新的对象和数值来表示输出结果,而不仅仅是 [Size]。这种灵活性是以不能依赖于 [RenderBox] 特性为代价的。例如, [RenderBox] 实现了一个内在的尺寸协议,它允许你在没有完全布局子项的情况下测量子项,以便在子项大小发生变化时,父项重新布局(考虑到子项的新尺寸)。这是一项微妙且容易出错的功能。
编写 [RenderBox] 时的大多数方面同样适用于编写 [RenderObject],因此建议在阅读 [RenderBox] 的背景资料时,也阅读相关内容。主要的区别在于布局和命中测试,因为这些是 [RenderBox] 主要专注的方面。
布局
布局协议从 [Constraints] 的子类开始。有关如何编写 [Constraints] 子类的更多信息,请参阅 [Constraints] 的讨论。
[performLayout] 方法应该接受 [constraints] 并应用它们。布局算法的输出是设置在对象上的字段,这些字段描述了对象的几何形状,供父节点的布局使用。例如,对于 [RenderBox],输出是 [RenderBox.size] 字段。只有在父节点调用 [layout] 时,如果父节点指定了 parentUsesSize 为 true,父节点才应读取该输出。
每当渲染对象的任何内容发生变化,可能会影响该对象的布局时,它应调用 [markNeedsLayout]。
命中测试
命中测试比布局更加开放。没有需要重写的方法,您需要提供一个。
命中测试方法的一般行为应该与 [RenderBox] 中描述的行为相似。主要的区别是输入不一定是 [Offset]。在将条目添加到 [HitTestResult] 时,您也可以使用 [HitTestEntry] 的不同子类。当调用 [handleEvent] 方法时,将传递与添加到 [HitTestResult] 的对象相同的对象,因此可以用来跟踪精确的命中坐标,采用新布局协议所使用的坐标系统。
从一个协议适配到另一个协议
一般来说,Flutter 渲染对象树的根是 [RenderView]。该对象有一个子对象,必须是 [RenderBox]。因此,如果你想在渲染树中使用自定义的 [RenderObject] 子类,你有两个选择:要么需要替换 [RenderView] 本身,要么需要拥有一个 [RenderBox],将你的类作为它的子类。(后者是更常见的情况。)
该 [RenderBox] 子类将从盒子协议转换为你的类协议。
具体来说,这意味着对于命中测试,它重写了 [RenderBox.hitTest],并调用你类中的命中测试方法。
类似地,它重写了 [performLayout],为你的类创建一个合适的 [Constraints] 对象,并将其传递给子项的 [layout] 方法。
渲染对象之间的布局交互
通常,渲染对象的布局只应依赖于其子项布局的输出,并且只有在 [layout] 调用中将 parentUsesSize 设置为 true 时才会发生。此外,如果设置为 true,则父节点必须调用子项的 [layout] 方法,如果子项需要渲染,因为否则父节点不会在子项更改其布局输出时得到通知。
可以设置渲染对象协议来传递额外的信息。例如,在 [RenderBox] 协议中,你可以查询子项的内在尺寸和基准几何形状。然而,如果这样做,那么当子项的几何形状发生变化时,必须确保子项调用 [markNeedsLayout] 来标记父节点为脏数据,前提是父节点在上次布局阶段使用了这些信息。有关如何实现这一点的示例,请参见 [RenderBox.markNeedsLayout] 方法。它重写了 [RenderObject.markNeedsLayout],这样如果父节点查询了内在或基准信息,每当子项的几何形状发生变化时,父节点就会被标记为脏数据。
parentData
parentData
是一个用于存储父渲染对象所需信息的字段。它的类型是 ParentData?
(可空类型)。
parentData
对渲染对象自身是透明的,它是给父渲染对象使用的,它的读写都由父渲染对象完成。- 不能直接设置
parentData
字段,必须通过父节点的setupParentData()
方法来设置。 - 一般用来保存子对象布局相关信息。
parent
当前RenderObject
的父节点,对于根RenderObject
(即RenderView
),它的parent
是null
。
_relayoutBoundary
重新布局边界。
重新布局边界是指在渲染树中,当该节点需要重新布局时,不会导致其父节点也需要重新布局的节点。
特点
类型是
RenderObject?
,值有3种情况:null
:不是边界this
:边界是自身parent!._relayoutBoundary!
:边界是父结点的边界
当一个渲染对象满足以下条件之一时,它就是重新布局边界:
- 其大小在重新布局时不会改变
- 其父节点不使用子节点的大小来进行自身布局
constraints
父对象提供的约束,它可能改变。
markNeedsLayout()
layout()
作用是布局自己,并布局所有子元素。
子类不应该重写layout()
方法,而是实现performResize()
和performLayout()
。
sizedByParent
作用:
表示一个渲染对象的大小是否仅由其父级提供的约束(constraints
)决定,而不依赖于其子节点。
代码:
dart
@protected
bool get sizedByParent => false;
如果返回
true
代表当前组件的大小只取决于父组件传递的约束,而不会依赖后代组件的大小,可以提高性能。
只能在
performResize()
中改变size
。如果返回
false
代表当前组件的大小受子组件的影响。
只能在
performLayout()
中改变size
。
返回true
的例子:
dart
class FixedSizeRenderObject extends RenderBox {
@override
bool get sizedByParent => true; // 因为大小是固定的
@override
void performResize() {
size = constraints.constrain(const Size(100, 100));
}
}
dart
class ConstrainedRenderObject extends RenderBox {
@override
bool get sizedByParent => true; // 大小只由约束决定
@override
void performResize() {
size = constraints.biggest; // 使用约束中的最大尺寸
}
}
用代码描述sizedByParent
的作用:
dart
if (sizedByParent) {
// 只在 performResize() 中设置大小
// performLayout() 只处理子节点布局
} else {
// 在 performLayout() 中同时处理大小和子节点布局
}
performResize()
抽象方法
performLayout()
抽象方法
paint()
空实现
RenderObjectWithChildMixin
它是一个 mixin
类,主要用于处理只有单个子节点的渲染对象(RenderObject
)。
_child
get child
set child
访问子节点
attach()
重写了 RenderObject
的 attach()
方法,添加子节点。
detach()
重写了 RenderObject
的 detach()
方法,移除子节点。
redepthChildren()
重写了 RenderObject
的 redepthChildren()
方法,更新子节点在渲染树中的深度。
visitChildren()
支持使用访问者模式遍历子节点
ContainerRenderObjectMixin
用于含有多个子元素的RenderObject
,提供对子元素的操作。
RenderBoxContainerDefaultsMixin
继承 ContainerRenderObjectMixin
提供默认的绘制、命中测试。