[翻译] Flutter 内幕之 Boxes

460 阅读10分钟

Boxes

原文地址:www.flutterinternals.org/data-model/…

Render box 有哪些组成部分?

  • RenderBox 是一个二维 box 的模型,它有宽度、高度和位置(RenderBox.size.widthRenderBox.size.heightRenderBox.parentData.offset)。盒子的左上角定义了它的原点,右下角对应(宽度,高度)。
  • BoxParentData将子节点的偏移量存储在父节点的坐标空间中(BoxParentData.offset)。通常来说,子节点没有权限读取这个数据。
  • BoxConstraints 描述了不可改变的约束,用最大和最小的宽度和高度表示,范围是从零到无穷大的闭区间。约束由落在这个范围内的具体尺寸来满足。
    • Box constraints 有以下几种类型:
      • BoxConstraints.isNormal:最小值比 0 大且比小于等于两个维度的最大值。
      • BoxConstraints.tight, BoxConstraints.isTight:最小值和最大值在两个维度上都相等。
      • BoxConstraint.loose:最小值为零,即使最大值也为零(即同时是losse 和 tight)。
      • BoxConstraints.hasBoundedWidthBoxConstraints.hasBoundedHeight:对应维度的最大值不是无穷大。
        • 最大值是无穷大的时候,约束是无界的。
      • BoxConstraints.expanding:最大最小值在同一纬度上都是无限的。
        • Expanding constraints 意味着对应的维度会由其他传入的约束来确定(比如由容器 UI 确定)。尺寸最终必须是有限的。
      • BoxConstraints.hasInfiniteWidthBoxConstraints.hasInfiniteHeight: 对应维度的最小值是无限的(因此,最大值也是无限的,这就跟 expanding 一样了)。
    • Box constraints 通过 BoxConstraints.isSatisfiedBy 方法来评估是否满足约束,通过BoxConstraints.constrain方法来转换成既满足约束又接近给定 size 的新尺寸,通过BoxConstraints.tightenBoxConstraints.loosenBoxConstraints.enforce等方法来转换成新的 Constraints。Constraints 也可以通过标准代数运算符进行缩放。
  • BoxHitTestResult 是一个HitTestResult的子类,按照优先级递减的方式存储着通过了 hit test 的 RenderBox(它是一个HitTestTarget)。
    • 它有一些辅助方法用来把全局坐标转换成局部坐标(例如BoxHitTestResult.addWithPaintOffsetBoxHitTestResult.addWithPaintTransform)。
  • BoxHitTestEntry 表示一个通过了 HitTest 测试的 box。它存储着触摸点的局部坐标(BoxHiTestEntry.localPostition)。

Boxes 如何管理子节点?

  • ContainerBoxParentData 继承 BoxParentData 并 mixin 了ContainterParentDataMixin。它将一个子节点的 offset(BoxParentData.offset)和前后指针(ContainerParentData.previousSiblingContainerParentData.nextSibling)组合在一起,用来描述一个存储着子节点的双链表结构。
  • RenderObjectWithChildMixinContainerRenderObjectMixin 都可以用作 RenderBox 的类型参数(很多 RenderBox mixin 了前两者)。ContainerRenderObjectMixin 接受一个 parent data 类型的参数;ContainerBoxParentData 兼容和支持 box chidlren(它是一个空类,为了方便子类混入 ContainerParentDataMixin)。
  • RenderBoxContainerDefaultsMixin 添加了很多实用的默认值到 ContainerRenderObjectMixin,用于拥有子节点的 render boxes。它通过类型约束要求子节点继承自 RenderBox,parent data 继承自 ContainerBoxParentData
    • 它提供了几个辅助方法:RenderBoxContainerDefaultsMixin.defaultHitTestChildrenRenderBoxContainerDefaultsMixin.defaultPaintRenderBoxContainerDefaultsMixin.getChildrenAsList
  • RenderProxyBox 把所有方法委托给一个 RenderBox 子节点,用子节点的 size 在作为自己的 size,把子节点布局在原点(origin)。这对于编写子类是很方便的,这些子类有选择地覆盖了盒子的某些但不是全部行为。它的实现由RenderProxyBoxMixin提供,而不是直接继承。Proxy boxes 使用 ParentData 而不是BoxParentData因为从不使用子节点的 offset。
    • 整个 framework 中有许多 RenderProxyBox 子类的例子:
      • RenderAbsorbPointer 重写了 RenderProxyBox.hitTest 来,当 RenderAbsorbPointer.absorbing 为 enabled 时禁用字树的 hit testing。
      • RenderAnimatedOpacity 监听动画(RenderAnimatedOpacity.opacity)获取不同的 opacity,再通过RenderAnimatedOpacity.paint方法渲染子节点。这个 box 通过重写RenderProxyBox.attachRenderProxyBox.detach来管理订阅者。它选择性的插入一个 OpacityLayer(仅为了控制translucent),在响应 animation 时调用的方法RenderAnimatedOpacityMixin._updateOpacity中设置 RenderBox.alwaysNeedsCompositing的值来控制是否应该添加一个 layer。
      • RenderIntrinsicWidth 重写了 RenderProxyBox.performLayout方法来采用与其子节点固有宽度相同切满足传入的约束的 size。由于它还支持大小对齐(RenderBox.stepWidth),因此它也覆盖了固有的大小调整方法。
  • RenderShiftedBox 把所有方法都代理给了 RenderBox 实例,但不去定义 layout 方法。它的子类重写了 RenderShiftedBox.performLayout 方法并通过 BoxParentData.offset 分配 position 给子节点。其他方面,它的子类表现得跟 RenderProxyBox 一样。
    • RenderPadding 重写 RenderShiftedBox.performLayout 以通过解析的 padding 值缩小传入的约束。使用新的 padding 约束对子节点进行布局,并将其放置在已经设置好 padding 的区域内。
    • RenderBaseline 覆盖 RenderShiftedBox.performLayout 以使其子节点的 baseline(通过 RenderBox.getDistanceToBaseline)与其顶部边缘(RenderBaseline.baseline)的偏移量对齐。然后,它会自行调整大小,使其底部与子节点的 baseline 重合(这可能会截断子节点的顶部)。

Render boxes 如何处理布局?

  • Boxes 使用标准的 RenderObject 布局协议来把传入的 BoxConstraints映射到一个具体的 Size(存储在 RenderBox.size 中)。布局过程同时确定子节点相对于父节点的偏移量(RenderBox.parentData.offset)。子节点在 layout 过程中不应该有权访问这些信息。
  • Boxes 增加了对固有尺寸(理想尺寸,在标准布局协议之外计算)和 baselines(用于垂直对齐的线,通常在布置文本时使用)的支持。 RenderBox 实例跟踪对这些值的变更以及父级是否已查询了这些值;如果查询过,则当该 box 被标记为需要布局时,父节点也将被标记。
    • 每当通过 RenderBox._computeIntrinsicDimension 方法计算固有尺寸时,都会对其进行缓存(RenderBox._cachedIntrinsicDimensions)。缓存在调试期间被禁用。
    • 每当计算 baseline 时(通过 RenderBox.getDistanceToActualBaseline ),都会缓存 baseline(RenderBox._cachedBaselines)。为字母和表意文字计算出来的 baseline 是不同的。
  • 如果修改了盒子的固有尺寸或 baseline 缓存,则 RenderBox.markNeedsLayout 将父节点标记为需要布局(即,这意味着父节点已访问了 box 的 "out-of-band" 几何数据)。如果是这样,则将两者都清除,以便在下一次布局通过之后计算新值。
  • 默认情况下,由其父节点调整大小的 boxes 采用满足给定约束的最小尺寸(通过RenderBox.performResize方法)。
  • Boxes 可以有非 box 子节点。在这种情况下,需要将提供给子节点的约束条件从 BoxConstraints 修改为适当的类型。

Render boxes 如何处理 painting?

  • Boxes 使用标准的 RenderObject 绘制协议将自己绘制到给定的画布上。画布的原点不一定与 box 的原点重合。提供给 RenderBox.paint 的偏移量描述了 box 的原点在画布上的位置。画布和框将始终保持轴对齐。
  • RenderBox.paintBounds 描述了 box 将要绘制的区域。这确定了用于绘制的缓冲区的大小,并以局部坐标表示。它不需要匹配 RenderBox.size
  • 如果 render box 在绘制时应用了 transform(例如,在与给定的偏移量不同的地方绘制),则 RenderBox.applyPaintTransform 必须将相同的 transformation 应用于给定的矩阵。
    • RenderBox.globalToLocalRenderBox.localToGlobal 依靠这个 transformation 将全局坐标系映射到 box 的坐标系,反之亦然。
    • 默认情况下,RenderBox.applyPaintTransform 将子节点的偏移量(child.parentData.offset)应用为一个 translation。

Render boxes 如何处理 hit testing?

  • Box 支持 hit testing,这是一种用于委派和处理事件的类似订阅的机制。 Box 协议实现自定义流程,而不是继承 HitTestable 接口(尽管 hit testing 入口 RendererBinding 确实实现了此接口)。
    • Mixin 链通过 GestureBinding._handlePointerEvent 调用 RendererBinding.hitTest。Binding 委托给 RenderView.hitTest 通过 RenderBox.hitTest 来测试它的子节点。
    • RenderBox.hitTest 确定一个偏移量(在局部坐标系中)是否在其范围内。如果是,则在 box 通过RenderBox.hitTestSelf对其自身进行测试之前,将通过 RenderBox.hitTestChildren 依次测试每个子项。默认情况下,这两种方法均返回 false(即,boxes 不处理事件或将事件转发给其子节点)。
    • Boxes 通过将其添加到 BoxHitTestResult 来订阅相关的事件流(即,接收 RenderBox.handleEvent 调用以响应与交互相关的事件)。越早添加的 boxes 优先级越高。
    • 通过 RenderBox.handleEvent 按照添加顺序通知 BoxHitTestResult 中的所有 boxes 。

什么是固有尺寸?

  • 从概念上讲,box 的固有尺寸是其自然尺寸(即“想要”成为的尺寸)。精确的定义取决于 box 的实现方式和语义(即 box 所代表的含义)。
    • 固有尺寸通常是根据子元素的固有尺寸定义的,因此计算起来很昂贵(通常需要遍历整个子树)。
  • 固有尺寸通常与布局产生的尺寸不同(除了使用会尝试用自己的固有尺寸布局子节点的 IntrinsicHeightIntrinsicWidth widget 时)
    • 除非使用这些小部件中的一个(或另一个明确地将固有尺寸合并到其自己的布局中的小部件,例如 RenderTable),否则通常会忽略固有尺寸。
  • Box 通过 RenderBox.computeMinIntrinsicHeightRenderBox.computeMaxIntrinsicHeight 等方法以宽度和高度的最小值和最大值来描述固有尺寸。两者都接收相反维度的值(如果是无限的,则另一个维度不受限制);这对于根据另一维度(例如文本)定义一个固有维度的 boxes 很有用。
    • 最小固有宽度是 box 在不裁剪的情况下,无法正确绘制之前的最小宽度。
      • 直觉:把 box 变窄会裁剪它的内容。
      • 如果宽度是根据 box 的语义由高度确定的,则应使用传入的高度(可以是无限的,即不受限制)。否则,忽略高度。
    • 最小固有高度与高度的概念相同。
    • 最大固有宽度是最小宽度,因此进一步扩展不会减小最小固有高度。
      • 使 box 变宽不会有助于容纳更多内容。
      • 如果宽度是根据 box 的语义由高度确定的,则应使用传入的高度(可以是无限的,即不受限制)。否则,忽略高度。
  • 固有尺寸的具体含义取决于 box 的语义。
    • Text is width-in -height-out.
      • 最大固有宽度:没有换行符的字符串的宽度(增加宽度不会缩小首选高度)。
      • 最小固有宽度:最宽的字的宽度(减小该宽度会截断该词或导致无效的中断)。
      • 固有高度是通过以提供的宽度布置文本来计算的。
    • Viewports 会忽略传入的约束并聚合子节点的尺寸而不会进行裁剪(即,理想情况下,视口可以呈现其所有子级而不会裁剪)。
    • Aspect ratio boxes 使用输入的尺寸来计算查询的尺寸(即,宽度确定高度,反之亦然)。如果传入尺寸是无界的,则使用子节点的固有尺寸。
    • 如果无法计算固有尺寸或过于昂贵,则返回 0。

什么是 baselines?

  • Baselines 是从文本渲染中借来的概念,用于描述放置所有字形的行。字形的部分通常在此线下方延伸(例如,下降线),因为它主要用于以视觉上令人愉悦的方式垂直对齐一系列字形。通过定位文本的每个范围,使基线是共线的,可以从视觉上对齐来自不同字体的字符。
    • 定义视觉基线的 boxes 也可以这种方式对齐。
  • Boxes 可以通过实现 RenderBox.computeDistanceToActualBaseline 来指定基线。返回的值表示距框顶部的垂直偏移量。值将被缓存,直到该框被标记为需要布局为止。
    • Boxes 常返回 null(即,它们未定义逻辑基线),其所代表的内容的固有值(即,文本范围的基线),委托给单个子节点或使用 RenderBoxContainerDefaultsMixin 从一组子节点中生成基线。
      • RenderBoxContainerDefaultsMixin.defaultComputeDistanceToFirstActualBaseline 返回由该组子节点报告的第一个有效基线,根据子节点的偏移量进行调整。
      • RenderBoxContainerDefaultsMixin.defaultComputeDistanceToHighestActualBaseline 返回所有子项中的最小基线(即垂直偏移),根据子节点的偏移量进行调整。
  • 如果没有可用的实际基线(即 RenderBox.computeDistanceToActualBaseline 返回 null),则 RenderBox.getDistanceToBaseline 返回 boxes 底边的偏移量(RenderBox.size.height)。
    • 基线只能由 box 的父节点查询,并且只能在 box 布局后(通常在父节点 layout 或 paint 过程中)查询。