1. 前言
有時候,RenderObject需要在其子節點中儲存一些資料,比如用於佈局的一些引數,或者和其他子節點之間的關係。為此,Flutter提供了ParentData,用於儲存父節點的一些資訊。每個RenderObject都有這個成員變數,該成員在setupParentData方法中初始化。子類如果需要ParentData的某個子類,需要重寫該方法,並在該方法中對ParentData進行初始化。
2. ParentData分類
ParentData可以分為三大類:BoxParentData,SliverLogicalParentData,以及SliverPhysicalParentData。其中SliverLogicalParentData和SliverPhysicalParentData用於sliver,對應滑動檢視場景,此文不進行展開。BoxParentData則用於RenderBox,對應普通檢視場景,是本文講解的重點。
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的節點佈局。
其中,ContainerBoxParentData使用頻率很高,基本上所有父節點ParentData都混入了該類,該類需要與ContainerRenderObjectMixin共同使用,主要解決了對child的管理,它用雙連結串列儲存了所有子節點並提供了方便的介面去獲取他們。對於開發者,一般來說只用到ContainerRenderObjectMixin中的firstChild、lastChild、childCount,用來獲取首末child,child的個數,配合使用ContainerParentDataMixin中的previousSibling、nextSibling就可以對child進行遍歷了。
這些ParentData的基類解決了child的佈局位置資訊的儲存和child的管理以及引用的獲取,再往下的子類就是與各佈局的功能相關的類了,如 FlexParentData,儲存了flex和fit的值,分別表示該child的flex比重和佈局的fit策略。
3. 關鍵流程
對於BoxParentData的常用屬性offset,通常情況下,其在子RenderObject節點的setupParentData函式中對BoxParentData進行初始化;在performLayout中,對offset進行賦值;在paint函式中,使用offset確認子節點的繪製位置,在hitTestChildren中,使用offset輔助判斷是否在點選區域內。
接下來,將使用一個示例,來分析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對應的ParentData是StackParentData,其被儲存在Stack的Child RenderObject中。StackParentData初始化的時序圖如下圖:
由該時序圖可以看出,StackParentData的初始化函式的呼叫時機是Element被載入時,即mount函式中,其對應的setupParentData程式碼如下。
@override
void setupParentData(RenderBox child) {
if (child.parentData is! StackParentData)
child.parentData = StackParentData();
}
複製程式碼
- 賦值
StackParentData被初始化後,其賦值是在performLayout中,其流程圖如下所示。
如流程圖所示,在performLayout中,對子節點是否是isPositioned,會分別進行處理,但最終都會對offset進行賦值。
- 使用
offset的使用場景主要有兩處,第一個是在繪製子RenderObject節點的時候用於確認子RenderObject節點的位置,對應的函式是paint;第二個是在判斷點選事件的時候,用於判斷是否會觸發子RenderObject節點的點選事件,對應的函式是hitTestChildren。 其在paint函式中使用的流程圖如下:
最後會呼叫到RenderBoxContainerDefaultsMixin的defaultPaint函式,其程式碼如下:
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;
}
}
複製程式碼
offset在hitTestChildren函式中使用的流程圖如下:
最後會呼叫到RenderBoxContainerDefaultsMixin的defaultHitTestChildren函式,其程式碼如下:
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;
}
複製程式碼
除了offset,StackParentData還有自己特有的屬性top等,這些屬性儲存了子RenderObject在RenderStack中的位置資訊,其賦值是在applyParentData中,這個函式也是Flutter Framework開放出來對ParentData進行賦值的介面。其流程圖如下:
此處,Positioned是Stack的子Widget,RenderObjectElement是Positioned的子Widget對應的Element。 值得注意的是applyParentData只在父Element是ParentDataElement,且其有更新的時候才會被呼叫,包括但不限於本Element attachRenderObject和父ParentDataElement對應的Widget被重建。
4. 常用使用場景
ParentData的常用使用場景是在自定義RenderObject中,自定義一種ParentData,例如CustomParentData,儲存其特有的佈局資訊。然後在setupParentData中對其進行初始化,在applyParentData中對其進行賦值,然後在paint和hitTestChildren中對其進行使用。具體使用示例會在自定義RenderObject章節中進行詳述。
5. 小結
本文主要介紹了ParentData的作用、分類和關鍵流程,並通過一個示例分析了ParentData的關鍵流程。其重點如下:
- RenderObject通常使用ParentData在其子節點中儲存一些資料,比如用於佈局的一些引數。
- ParentData一般在setupParentData中進行初始化,在performLayout中進行賦值,在paint和hitTestChildren中進行使用。
- 如果是自定義的ParentData,通常需要在applyParentData中對其進行賦值。