Flutter Stack Widget Class 源碼解析
在 Flutter 中, Stack 是一種特殊的布局小部件,它允許開發者在單一平面上重疊多個子小部件。這類似於傳統的堆疊概念,其中元素可以放置在其他元素上面,從而創建出層次豐富的視覺效果。
Stack
定義在 src/widgets/basic.dart。 Stack
的 child 分成兩種,分別是 positioned
跟沒有 positioned
。positioned
的 child 會用 Positioned
包住,設定上、下、左、右的 offset,做位置的指定。
Stack
入口點,類圖整理如下:
classDiagram MultiChildRenderObjectWidget <|-- Stack abstract AlignmentGeometry enum StackFit enum Clip Stack --> AlignmentGeometry : use Stack --> StackFit : use Stack --> Clip : use Stack --> RenderStack : create
AlignmentGeometry
可以做加減乘除運算的方向性單位,主要用來處理文字方向考量的 resolution。下面的註解解釋得很清楚。
/// Base class for [Alignment] that allows for text-direction aware
/// resolution.
///
/// A property or argument of this type accepts classes created either with [
/// Alignment] and its variants, or [AlignmentDirectional.new].
///
/// To convert an [AlignmentGeometry] object of indeterminate type into an
/// [Alignment] object, call the [resolve] method.
@immutable
abstract class AlignmentGeometry
classDiagram class AlignmentGeometry AlignmentGeometry <|-- Alignment AlignmentGeometry <|-- AlignmentDirectional AlignmentGeometry <|-- FractionalOffset
StackParentData
StackParentData
用來記錄 child 的 positioned 狀態跟位置、長寬。
類圖整理如下:
classDiagram class StackParentData ContainerBoxParentData <|-- StackParentData ContainerBoxParentData --> RenderBox
以下是 StackParentData
的幾個關鍵用途:
- 儲存位置信息: 對於使用
Positioned
小部件的子元素,StackParentData
會儲存其位置信息,如左、上、右、下邊距,這些信息用於計算子元素在Stack
中的最終位置。 - 處理非定位子元素: 對於沒有使用
Positioned
包裹的子元素,Stack
會將它們根據alignment
屬性的指定,放置在Stack
中的相應位置。StackParentData
在這個過程中用於輔助計算這些子元素的位置。 - 維護子元素的層次結構: 在
Stack
中,子元素是按照它們在子列表中的順序進行繪製的,StackParentData
負責記錄這一信息,確保子元素能夠按正確的順序重疊顯示。
RenderStack
RenderStack
是一個負責渲染 Stack
布局的底層渲染對象(Render Object)。當你使用 Stack
小部件來創建重疊的布局時,背後實際上是 RenderStack
在工作,它負責按照指定的方式繪製和定位每個子元素。
類圖整理如下:
classDiagram RenderBox <|-- RenderStack ContainerRenderObjectMixin <|-- RenderStack : mixin RenderBoxContainerDefaultsMixin <|-- RenderStack : mixin ContainerRenderObjectMixin --> RenderBox : use ContainerRenderObjectMixin --> StackParentData : use RenderBoxContainerDefaultsMixin --> RenderBox : use RenderBoxContainerDefaultsMixin --> StackParentData : use RenderStack --> ChildLayoutHelper : use
相依的類別相當複雜,但若你之前已經很了解 ParentData
跟 RenderBox
的作用,那還需要深入瞭解的只有 ChildLayoutHelper
。
ChildLayoutHelper
ChildLayoutHelper
是一個 collection 類別,裡面有兩個 static helper 函式,分別為 dryLayoutChild
跟 layoutChild
。用途是計算 child layout 後的 Size
。
performLayout
如果 child 不是 positioned , 要計算 offset,不然就丟到 layoutPositionedChild
處理。
計算 child constraint,計算有沒有 visualoverflow
,_visualoverFlow
會 RenderStack.paint
用到。
_computeSize
計算最大的寬跟高。
RenderBox? child = firstChild;
while (child != null) {
final StackParentData childParentData = child.parentData! as StackParentData;
if (!childParentData.isPositioned) {
hasNonPositionedChildren = true;
final Size childSize = layoutChild(child, nonPositionedConstraints);
width = math.max(width, childSize.width);
height = math.max(height, childSize.height);
}
child = childParentData.nextSibling;
}
如果有 positioned child,就用最大的 constrains,不然就是用 ChildLayoutHelper.laytoutChild
計算出來的寬跟高產生 size。
ChildLayoutHelper.layoutChild
child layout 語法糖,定義在 src/rendering/layout_helper.dart
。
static Size layoutChild(RenderBox child, BoxConstraints constraints) {
child.layout(constraints, parentUsesSize: true);
return child.size;
}
layoutPositionedChild
- 首先計算
childConstraints
,然後用RenderBox.layout
進行佈局。 - 然後計算有沒有 visual overflow,有的話
hasVisualOverflow
設為 true。 - 最後回傳
hasVisualOverflow
, 這個會改變RenderStack._hasVisualOverflow
, 從而影響要不要做剪裁 。
paint
_visualoverFlow
會在這裡用到,有 visual overflow 會使用 CLip
做剪裁。
@override
void paint(PaintingContext context, Offset offset) {
if (clipBehavior != Clip.none && _hasVisualOverflow) {
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
paintStack,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
paintStack(context, offset);
}
主要的繪製邏輯在 RenderStack.paintStack
。
paintStack
這個 function 目的是讓子類別 override。
/// Override in subclasses to customize how the stack paints.
///
/// By default, the stack uses [defaultPaint]. This function is called by
/// [paint] after potentially applying a clip to contain visual overflow.
@protected
void paintStack(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
其實就是執行 RenderBoxContainerDefaultsMixin.defaultPaint
。
RenderBoxContainerDefaultsMixin.defaultPaint
defaultPaint codeBody 如下:
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;
}
}
邏輯是把 child 畫在 對應的 offset (stack 中的位移 + stack 在父元件的位移)
Positioned
改變數值會通知 parent rebuild。
classDiagram ParentDataWidget <|-- Positioned
ParentDataWidget
用於為擁有多個子元素的 RenderObjectWidgets
提供每個子元素的配置。例如,Stack
使用 Positioned
這個父數據小部件來定位每個子元素。ParentDataWidget
特定於特定種類的 ParentData
。 該類是 T
,ParentData
類型參數。