在 Flutter 中, Stack 是一種特殊的布局小部件,它允許開發者在單一平面上重疊多個子小部件。這類似於傳統的堆疊概念,其中元素可以放置在其他元素上面,從而創建出層次豐富的視覺效果。

Stack 定義在 src/widgets/basic.dart。 Stack 的 child 分成兩種,分別是 positioned 跟沒有 positionedpositioned 的 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 的幾個關鍵用途:

  1. 儲存位置信息: 對於使用 Positioned 小部件的子元素,StackParentData 會儲存其位置信息,如左、上、右、下邊距,這些信息用於計算子元素在 Stack 中的最終位置。
  2. 處理非定位子元素: 對於沒有使用 Positioned 包裹的子元素,Stack 會將它們根據 alignment 屬性的指定,放置在 Stack 中的相應位置。StackParentData 在這個過程中用於輔助計算這些子元素的位置。
  3. 維護子元素的層次結構: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

相依的類別相當複雜,但若你之前已經很了解 ParentDataRenderBox 的作用,那還需要深入瞭解的只有 ChildLayoutHelper

ChildLayoutHelper

ChildLayoutHelper 是一個 collection 類別,裡面有兩個 static helper 函式,分別為 dryLayoutChildlayoutChild 。用途是計算 child layout 後的 Size

performLayout

如果 child 不是 positioned , 要計算 offset,不然就丟到 layoutPositionedChild 處理。

計算 child constraint,計算有沒有 visualoverflow_visualoverFlowRenderStack.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

  1. 首先計算 childConstraints ,然後用 RenderBox.layout 進行佈局。
  2. 然後計算有沒有 visual overflow,有的話 hasVisualOverflow 設為 true。
  3. 最後回傳 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。 該類是 TParentData 類型參數。