Flutter框架分析- Parent Data

1. 前言

有时候,RenderObject须要在其子节点中存储一些数据,好比用于布局的一些参数,或者和其余子节点之间的关系。为此,Flutter提供了ParentData,用于存储父节点的一些信息。每一个RenderObject都有这个成员变量,该成员在setupParentData方法中初始化。子类若是须要ParentData的某个子类,须要重写该方法,并在该方法中对ParentData进行初始化。node

2. ParentData分类

image.png ParentData能够分为三大类:BoxParentDataSliverLogicalParentData,以及SliverPhysicalParentData。其中SliverLogicalParentDataSliverPhysicalParentData用于sliver,对应滑动视图场景,此文不进行展开。BoxParentData则用于RenderBox,对应普通视图场景,是本文讲解的重点。markdown

BoxParentData中主要属性是offset,用于描述子节点在父节点中的坐标偏移,主要用于子节点的布局,其源码以下:架构

class BoxParentData extends ParentData {
  /// The offset at which to paint the child in the parent's coordinate system.
  Offset offset = Offset.zero;

  @override
  String toString() => 'offset=$offset';
}
复制代码

BoxParentData的子类TableCellParentData主要用于表格布局,_ToolbarParentData主要用于iOS风格的工具栏布局,ContainerBoxParentData主要用于须要ContainerRenderObjectMixin的节点布局。app

其中,ContainerBoxParentData使用频率很高,基本上全部父节点ParentData都混入了该类,该类须要与ContainerRenderObjectMixin共同使用,主要解决了对child的管理,它用双链表存储了全部子节点并提供了方便的接口去获取他们。对于开发者,通常来讲只用到ContainerRenderObjectMixin中的firstChildlastChildchildCount,用来获取首末childchild的个数,配合使用ContainerParentDataMixin中的previousSiblingnextSibling就能够对child进行遍历了。框架

这些ParentData的基类解决了child的布局位置信息的存储和child的管理以及引用的获取,再往下的子类就是与各布局的功能相关的类了,如 FlexParentData,存储了flexfit的值,分别表示该childflex比重和布局的fit策略。less

3. 关键流程

对于BoxParentData的经常使用属性offset,一般状况下,其在子RenderObject节点的setupParentData函数中对BoxParentData进行初始化;在performLayout中,对offset进行赋值;在paint函数中,使用offset确认子节点的绘制位置,在hitTestChildren中,使用offset辅助判断是否在点击区域内。ide

接下来,将使用一个示例,来分析BoxParentData中的各个流程。示例代码以下:函数

class StackTestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints.expand(),
      child: Stack(
        alignment:Alignment.center , //指定未定位或部分定位widget的对齐方式
        children: <Widget>[
          // Positioned(
          // top: 80.0,
          // child: RichText(text: TextSpan(text: "first text"),
          // textDirection: TextDirection.ltr,),
          // ),
          Container(
            child: RichText(
                text: TextSpan(text: "first text"),
                textDirection: TextDirection.ltr),
            color: Colors.red,
          ),
          Positioned(
            top: 180.0,
            left: 100,
            child: RichText(text: TextSpan(text: "second"),
              textDirection: TextDirection.ltr,),
          )
        ],
      ),
    );
  }
}
复制代码
  • 初始化

其中,Stack对应的ParentDataStackParentData,其被存储在StackChild RenderObject中。StackParentData初始化的时序图以下图:工具

image.png

由该时序图能够看出,StackParentData的初始化函数的调用时机是Element被加载时,即mount函数中,其对应的setupParentData代码以下。布局

@override
void setupParentData(RenderBox child) {
  if (child.parentData is! StackParentData)
    child.parentData = StackParentData();
}
复制代码
  • 赋值

StackParentData被初始化后,其赋值是在performLayout中,其流程图以下所示。

image.png

如流程图所示,在performLayout中,对子节点是不是isPositioned,会分别进行处理,但最终都会对offset进行赋值。

  • 使用

offset的使用场景主要有两处,第一个是在绘制子RenderObject节点的时候用于确认子RenderObject节点的位置,对应的函数是paint;第二个是在判断点击事件的时候,用于判断是否会触发子RenderObject节点的点击事件,对应的函数是hitTestChildren。 其在paint函数中使用的流程图以下:

image.png

最后会调用到RenderBoxContainerDefaultsMixindefaultPaint函数,其代码以下:

void defaultPaint(PaintingContext context, Offset offset) {
  ChildType child = firstChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData as ParentDataType;
    context.paintChild(child, childParentData.offset + offset);
    child = childParentData.nextSibling;
  }
}
复制代码

offsethitTestChildren函数中使用的流程图以下:

image.png

最后会调用到RenderBoxContainerDefaultsMixindefaultHitTestChildren函数,其代码以下:

bool defaultHitTestChildren(BoxHitTestResult result, { Offset position }) {
  // The x, y parameters have the top left of the node's box as the origin.
  ChildType child = lastChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData as ParentDataType;
    final bool isHit = result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      hitTest: (BoxHitTestResult result, Offset transformed) {
        assert(transformed == position - childParentData.offset);
        return child.hitTest(result, position: transformed);
      },
    );
    if (isHit)
      return true;
    child = childParentData.previousSibling;
  }
  return false;
}
复制代码

除了offsetStackParentData还有本身特有的属性top等,这些属性保存了子RenderObjectRenderStack中的位置信息,其赋值是在applyParentData中,这个函数也是Flutter Framework开放出来对ParentData进行赋值的接口。其流程图以下:

image.png

此处,PositionedStack的子WidgetRenderObjectElementPositioned的子Widget对应的Element。 值得注意的是applyParentData只在父ElementParentDataElement,且其有更新的时候才会被调用,包括但不限于本Element attachRenderObject和父ParentDataElement对应的Widget被重建。

4. 经常使用使用场景

ParentData的经常使用使用场景是在自定义RenderObject中,自定义一种ParentData,例如CustomParentData,存储其特有的布局信息。而后在setupParentData中对其进行初始化,在applyParentData中对其进行赋值,而后在painthitTestChildren中对其进行使用。具体使用示例会在自定义RenderObject章节中进行详述。

5. 小结

本文主要介绍了ParentData的做用、分类和关键流程,并经过一个示例分析了ParentData的关键流程。其重点以下:

  • RenderObject一般使用ParentData在其子节点中存储一些数据,好比用于布局的一些参数。
  • ParentData通常在setupParentData中进行初始化,在performLayout中进行赋值,在painthitTestChildren中进行使用。
  • 若是是自定义的ParentData,一般须要在applyParentData中对其进行赋值。

6. 相关文章

Flutter框架分析(一)--架构总览
Flutter框架分析(二)-- Widget
Flutter框架分析(三)-- Element
Flutter框架分析(四)-RenderObject
Flutter框架分析(五)-Widget,Element,RenderObject树
Flutter框架分析(六)-Constraint
Flutter框架分析(七)-relayoutBoundary
Flutter框架分析(八)-Platform Channel
Flutter框架分析 -InheritedWidget

相关文章
相关标签/搜索