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

相關文章
相關標籤/搜索