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

Stack 定義在 src/widgets/basic.dart。 Stack 的 child 分成兩種,分別是 positioned 跟沒有 positionedpositioned 的 child 會用 Positioned 包住,設定上、下、左、右的 offset,做位置的指定。

Stack

入口點,類圖整理如下:

use

use

use

create

MultiChildRenderObjectWidget

Stack

AlignmentGeometry

StackFit

Clip

RenderStack

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

AlignmentGeometry

Alignment

AlignmentDirectional

FractionalOffset

StackParentData

StackParentData 用來記錄 child 的 positioned 狀態跟位置、長寬。

類圖整理如下:

StackParentData

ContainerBoxParentData

RenderBox

以下是 StackParentData 的幾個關鍵用途:

  1. 儲存位置信息: 對於使用 Positioned 小部件的子元素,StackParentData 會儲存其位置信息,如左、上、右、下邊距,這些信息用於計算子元素在 Stack 中的最終位置。
  2. 處理非定位子元素: 對於沒有使用 Positioned 包裹的子元素,Stack 會將它們根據 alignment 屬性的指定,放置在 Stack 中的相應位置。StackParentData 在這個過程中用於輔助計算這些子元素的位置。
  3. 維護子元素的層次結構:Stack 中,子元素是按照它們在子列表中的順序進行繪製的,StackParentData 負責記錄這一信息,確保子元素能夠按正確的順序重疊顯示。

RenderStack

RenderStack 是一個負責渲染 Stack 布局的底層渲染對象(Render Object)。當你使用 Stack 小部件來創建重疊的布局時,背後實際上是 RenderStack 在工作,它負責按照指定的方式繪製和定位每個子元素。

類圖整理如下:

mixin

mixin

use

use

use

use

use

RenderBox

RenderStack

ContainerRenderObjectMixin

RenderBoxContainerDefaultsMixin

StackParentData

ChildLayoutHelper

相依的類別相當複雜,但若你之前已經很了解 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。

ParentDataWidget

Positioned

ParentDataWidget 用於為擁有多個子元素的 RenderObjectWidgets 提供每個子元素的配置。例如,Stack 使用 Positioned 這個父數據小部件來定位每個子元素。ParentDataWidget 特定於特定種類的 ParentData。 該類是 TParentData 類型參數。