主页 > 区块链钱包 > [imToken钱包]Flutter源码分析之自定义控件(RenderBox)指南

[imToken钱包]Flutter源码分析之自定义控件(RenderBox)指南

管理员 区块链钱包 2022年07月20日

一个看源码的好习惯就是看到一个新类先看注释,第一句话如下:

根据 child 的个数选择 RenderObjectWithChildMixin 或 ContainerRenderObjectMixin. 确认 sizedByParent 的值,如果 sizedByParent 为 true,直接在 performResize() 方法中确认自己的大小. 在 performLayout() 方法中对 child 进行布局.
void markNeedsLayout() { if (_needsLayout) { return; } if (_relayoutBoundary != this) { markParentNeedsLayout(); } else { _needsLayout = true; if (owner != null) { owner._nodesNeedingLayout.add(this); owner.requestVisualUpdate(); } } }

如果自身不是 relayoutBoundary,就继续向 parent 查找,一直向上查找到是 relayoutBoundary 的 RenderObject,再将这个 RenderObject 标记为 dirty 的。这样来看它的作用就比较明显了,意思就是当一个控件的大小被改变时可能会影响到它的 parent,因此 parent 也需要被重新布局,那么到什么时候是个头呢?答案就是 relayoutBoundary,如果一个 RenderObject 是 relayoutBoundary,就表示它的大小变化不会再影响到 parent 的大小了,于是 parent 也就不用重新布局了。知道这点后可以再重新考虑一下之前设置 relayoutBoundary 的四个判断条件,这么判断的原因应该很明确了,这里就不具体讲了。

在 paint 方法中实现自身与 child 的绘制,如果自身会频繁绘制,记得重写 isRepaintBoundary 的值为 true。

RenderBox 子类的常规写法
控件的绘制

除了 RenderBox 之外,还有一个类比较常用,那就是 RenderProxyBox,该类将布局绘制点击事件等方法的处理全部交由 child 来实现,可以理解为 child 的代理,具体代理了哪些方法可以参见 RenderProxyBoxMixin 的源码。

这两个 mixin 是非常常用的,看一下 Hierarchy 可以发现基本上每个 RenderBox 都混入了他们,省去了自己管理 child 的代码。

命名
Axis get direction => _direction; Axis _direction; set direction(Axis value) { if (_direction != value) { _direction = value; markNeedsLayout(); } } 布局、绘制、点击事件
命名

在安卓中,有 View 和 ViewGroup 的区分,前者不能有子 View,即为叶节点,后者可以有多个子 View,即父节点,那么 Flutter 中呢?答案是都是 RenderBox,child 的逻辑区别以 mixin 来解决,如果想拥有 child,混入上一节所讲的 RenderObjectWithChildMixin 或 ContainerRenderObjectMixin 就可以了。

根据需要实现hitTestSelf 和 hitTestChildren。

剩下的两个 mixin 还是比较关键的:

RenderBox

对 child 的布局在 performLayout() 中实现,布局后将 child 的 offset 放入 ParentData 中,注意调用 paintChild 时传入正确的 parentUsesSize 属性以优化性能。如果需要扩展 ParentData,那么重写 setupParentData 方法,ParentData 一般选择继承 ContainerBoxParentData。

/// 连续点赞Widget,对应连续点赞一帧的信息描述 class _RawMultiLike extends SingleChildRenderObjectWidget { final List<List<_SplashImage>> splashImages; final _DescriptionInfo descriptionInfo; final Size screenSize; const _RawMultiLike({ Widget child, this.splashImages, this.descriptionInfo, this.screenSize, }): super(child: child); @override _RenderMultiLike createRenderObject(BuildContext context) { return _RenderMultiLike( splashImageInfos: splashImages, descriptionInfo: descriptionInfo, screenSize: screenSize, configuration: createLocalImageConfiguration(context), ); } @override void updateRenderObject(BuildContext context, _RenderMultiLike renderObject) { renderObject ..splashImageInfos = splashImages ..descriptionInfo = descriptionInfo ..screenSize = screenSize ..configuration = createLocalImageConfiguration(context); } }

Element 层在 Widget 基类已经处理了,一般不用我们关心了。

布局 child 即计算出 child 相对 parent 展示的位置,将该位置赋值给 childParentData 的 offset 中就可以了,该 offset 会在后面的绘制过程中用到。

以上就是关于对Flutter源码分析之自定义控件(RenderBox)指南的详细介绍。欢迎大家对Flutter源码分析之自定义控件(RenderBox)指南内容提出宝贵意见

控件的点击事件处理

绘制 child 和处理 child 点击事件的默认逻辑在 RenderBoxContainerDefaultsMixin 中。

父节点的流程就相对复杂一些,因为除了测量外还要对子节点进行布局,步骤如下:

这里引出了另外一个问题,什么是 relayoutBoundary?

void paintChild(RenderObject child, Offset offset) { if (child.isRepaintBoundary) { stopRecordingIfNeeded(); _compositeChild(child, offset); } else { child._paintWithContext(this, offset); } } void _compositeChild(RenderObject child, Offset offset) { // Create a layer for our child, and paint the child into it. if (child._needsPaint) { repaintCompositedChild(child, debugAlsoPaintedParent: true); } else { // 省略assert逻辑 } child._layer.offset = offset; appendLayer(child._layer); }

可以看出在绘制 child 时,如果 isRepaintBoundary 为 true,那么会为该 child 新创建一个 layer,只有在不同 layer 的 RenderObject 才可以各自独立进行绘制。该属性很明显是为了提高渲染效率而存在的,它能够实现区域重绘功能,具体原理如下:

先无视用于滑动的 Sliver 相关的类和用于表格布局的 TabelCellParentData,我们来分析一下剩余的 ParentData类的作用。

总结前言

Flutter 本身提供了大量Widget以供开发,但是难免有通过组合完成不了的效果,此时就需要我们自己来实现 RenderObject 了,,本文会介绍一下实现一个 RenderObject 的基本步骤,帮助大家快速熟悉开发自定义控件的流程,当然这对于读懂原生 Widget 的实现源码也有很大的益处。

relayoutBoundary

回到 sizedByParent,为什么有这样一个属性呢?注释中发现是为了优化性能,这里分析一下 RenderObject 中用到它的代码:

ParentData
ParentDataBoxParentDataContainerBoxParentDataContainerParentDataMixin测量 child 大小
布局 child
bool hitTest(HitTestResult result, { @required Offset position }) { if (_size.contains(position)) { if (hitTestChildren(result, position: position) || hitTestSelf(position)) { result.add(BoxHitTestEntry(this, position)); return true; } } return false; } @protected bool hitTestSelf(Offset position) => false; @protected bool hitTestChildren(HitTestResult result, { Offset position }) => false;

hitTest 方法用来判断该 RenderObject 是否在被点击的范围内,同时负责将被点击的 RenderObject 添加到 HitTestResult 列表中,参数 position 为点击坐标,返回 true 则表示有 RenderObject 被点击了,反之没有。在默认实现中,简单的判断了 position 是否在 size 范围内,如果在自身范围内的话,继续判断是否有 child 在点击范围内,若没有 child 被点击,再判断自己是否被点击了。一般在子类中实现 hitTestSelf 和 hitTestChildren 即可。在 RenderBoxContainerDefaultsMixin 中有 hitTestChildren 的默认实现,即根据 child 的 hitTest 方法来判断是否被点击,如果没有特殊逻辑,直接使用该方法即可。

ParentData
ContainerBoxParentData

查看源码后发现该类是个空类,只是为了方便子类混入 ContainerParentDataMixin。


[imToken钱包]Flutter源码分析之自定义控件(RenderBox)指南

RenderObjectWithChildMixin 用于为只有 1 个 child 的 RenderObject 提供 child 管理模型。


[imToken钱包]Flutter源码分析之自定义控件(RenderBox)指南

成员变量
repaintBoundary

自定义单 child 布局:CustomSingleChildLayout

RenderBox
叶节点

根据上述流程完成布局与绘制后,我们理所应当的可能利用 GestureDetector 监听了一些手势,但是运行起来后发现手势完全没有生效,这是因为我们漏掉了关于点击事件处理相关方法的实现。在 RenderBox 中有三个方法与点击事件相关:

首先,介绍一下 RenderObject 子类的继承关系,通过 Android Studio 的 Hierarchy 功能可以直观地对类继承关系进行查看:

performResize 和 performLayout

在 RenderBox 中,控件大小的值为 _size 成员,它只包含宽高两个属性值,我们可以通过该成员的 set 和 get 方法访问或修改它的值。在测量时,parent 会传给当前 RenderBox 一个大小的限制,为 BoxConstraints 类型,通过 constraints 这个 get 方法可以获取到,最后测量得到的 size 必须满足这个限制,在 Flutter 的 debug 模式下对 size 是否满足 constraints 做了 assert 检查,如果检查未通过就会布局失败。所以测量上我们要做的是下面两点:

父节点
如果没有 child,那么根据自身的属性计算出满足 constraints 的 size. 如果有 child,那么综合自身的属性和 child 的测量结果计算出满足 constraints 的 size.

叶节点的测量和布局比较简单,首先根据需求确认 sizedByParent的值,然后通过自身属性和 constraints 计算出大小后调用 size 的 set 方法直接赋值给 size 就好了。由于是叶节点,是不用处理如何布局的问题的,只要知道自身的大小就足够了。

通常对一个已有的 RenderObject 做一些附加处理时会用到该类,如常见的 Opacity、DecoratedBox 等控件就是用该类实现的,它的各属性和 child 完全一致,因此我们专心处理对 child 的额外效果就可以了,避免了逻辑的拷贝。

意为 RenderView 根节点下只有唯一一个 RenderBox 作为叶节点,它的大小会充满整个绘制表面,由此可以看出,RenderBox 就是绘制上使用的基类了。继续观察一下 RenderObject 的子类继承树,发现有 3 个 Mixin 以及 RenderAbstractViewport 和 RenderSliver 没有继承自 RenderBox,这些类都是干什么用的呢?这里简单介绍下:

类似触发布局的方法,为了触发绘制,需要调用 markNeedsPaint(),分析下该方法的源码:

与 relayoutBoundary 相对应,对于绘制,也有一个 isRepaintBoundary 属性,与 relayoutBoundary 不同的是,这个属性需要由我们自己设置,默认为 false。注释中的第一句话表示了该属性的含义:

叶节点与父节点

除此之外还有一个类也有相当多的子类:RenderProxyBox,接下来就分别详细介绍一下继承 RenderBox 和 RenderProxyBox 实现自定义控件的正确姿势。

RenderBox 子类的常规写法

ParentData 类继承结构

SingleChildRenderObjectWidget,对应有一个 child 的 RenderObject. MultiChildRenderObjectWidget,对应有多个 child 的 RenderObject. LeafRenderObjectWidget 对应叶节点的 RenderObject.

RenderObject 的成员一般声明为 private,配以 set 和 get 方法,get 方法直接返回该成员即可,用来在类中获取该属性,set 方法一般先判断值是否与原值相同,若不同的话根据需要调用 markNeedsLayout 或 markNeedsPaint。

对于 child 可以遍历所有 child 并调用 context.paintChild(child, childParentData.offset + offset)方法完成 child 的绘制。除了这种方法以外,Flutter 还提供了 RenderBoxContainerDefaultsMixin,该类提供了一些 RenderBox 默认的行为方法,如上面绘制 child 的流程调用该类中的 defaultPaint(PaintingContext context, Offset offset) 就可以了,可以简化一些模板代码。

ContainerRenderObjectMixin 用于为有多个 child 的 RenderObject 提供 child 管理模型。

继承所需的类后,需要实现 createRenderObject 和 updateRenderObject 两个方法,前者用于创建新的 Object 实例,后者用于更新 RenderObject 的属性,示例如下:

测量一个 child 需要调用 RenderObject 中的 void layout(Constraints constraints, { bool parentUsesSize = false }),需要传入两个参数,constraints 即为父节点对子节点大小的限制,该值根据父节点的布局逻辑确定。调用完这个方法后,就可以通过 child.size 拿到 child 测量后的大小了。另外一个参数是 parentUsesSize,该值用于确定 relayoutBoundary,意为 child 的布局变化是否影响 parent,根据实际情况传入该值即可,默认为 false。

Whether this render object repaints separately from its parent.

绘制方法在 void paint(PaintingContext context, Offset offset) { } 中实现,RenderBox 需要在该方法中实现对自身的绘制以及所有 child 的绘制。

Flutter 原生提供了一些方便自定义功能的 Widget,如果可以满足需求的话,直接使用这些 Widget 是最方便的,下面列举一下:

绘制 child

首先要说明的是,与安卓的 onMeasure() 和 onLayout() 不同的是,Flutter 中测量和布局的过程都在 performLayout() 这一个方法中完成。

一些自定义控件相关的 Widget

自定义画布:CustomPaint

首先来讲一下如何触发布局的测量,之前有源码分析系列有提到过,在每一帧的绘制 drawFrame 方法中,会对标记为 dirty 的 RenderObject 进行重新布局,我们可以通过调用 markNeedsLayout() 方法将 RenderObject 的布局状态标记为 dirty。分析一下该方法的源码:

void markNeedsPaint() { if (_needsPaint) return; _needsPaint = true; if (isRepaintBoundary) { if (owner != null) { owner._nodesNeedingPaint.add(this); owner.requestVisualUpdate(); } } else if (parent is RenderObject) { final RenderObject parent = this.parent; parent.markNeedsPaint(); } else { if (owner != null) owner.requestVisualUpdate(); } }

本文网络收集整理,不构成任何投资建议。转载请注明出处:https://www.bnlive.com.cn/qklqb/8440.html