Flutter 中 Overlay 是一個特殊的 Stack Widget。官方文件指出雖然開發者可以直接建立 Overlay,但通用的用法,是使用由在 WidgetsApp 或 MAterialApp 的 Navigator 去管理路由的視覺化顯示。Overlay 實作一個特殊的 Stack widget,如果沒有特殊需求,開發者可直接使用 Stack。會用到 Overlay 通常是需要對浮動元件的渲染做控制。

撰寫時所使用的 Flutter 環境如下:

  • Flutter 3.1.0 • channel beta • https://github.com/flutter/flutter.git
  • Framework • revision bcea432bce (9 天前) • 2022-05-26 09:05:34 -0700
  • Engine • revision 44e5b38ee8
  • Tools • Dart 2.18.0 (build 2.18.0-44.1.beta) • DevTools 2.12.2

classDiagram
  class ChangeNotifier {
  }
  class OverlayEntry {
  }
  class OverlayState {
  }
  class _Theatre {
  }
  class _OverlayEntryWidget {
  }
  class _OverlyEntryState {
  }

  ChangeNotifier <|-- OverlayEntry
  OverlayEntry --> OverlayState
  Overlay --> OverlayState
  OverlayState -- _Theatre
  OverlayState -- _OverlayEntryWidget
  _OverlayEntryWidget -- _OverlyEntryState

OverlayEntry

該類別繼承 ChangeNotifierbuildermarkNeedsBuild 決定了 _OverlayEntryWidget 怎麼重建, remove 用來從 Overlay 移除該實例 。

OverlayEntry.builder 是一個型別為 Widget Function(dynamic context) 的 callback , 該 callback 定義要建立的 Widgets。 當 OverlayEntry.markNeedsBuild 被呼叫後, Overlay.builder 會在 build phase 被呼叫,並且導致 _OverlayEntryWidget 會被重建。

OverlayEntry.markNeedsBuild 是透過 GlobalKey 去拿到 _OverlayEntryWidget 當前的狀態達成,源碼如下:

  /// Cause this entry to rebuild during the next pipeline flush.
  ///
  /// You need to call this function if the output of [builder] has changed.
  void markNeedsBuild() {
    _key.currentState?._markNeedsBuild();
  }

_markNeedsBuild 的作用很單純,只是透過呼叫 setState 來觸發 _OverlayEntryWidget 重建而已,源碼如下:

  void _markNeedsBuild() {
    setState(() { /* the state that changed is in the builder */ });
  }

到這裡為止,我們已經了解 OverlayEntry。 那怎麼讓 _OverlayEntryWidget 重建,接下來來看 _OverlayEntryWidget

_OverlayEntryWidget


classDiagram
 OverlayState "1" *-- "many" _OverlayEntryWidget : manages
 _OverlayEntryWidget "1" -- "1" _OverlayEntryWidgetState : has
 _OverlayEntryWidgetState "1" --> "1" OverlayEntry : references
 _OverlayEntryWidget --> "1" TickerMode : uses

_OverlayEntryWidget 是一個 StatefulWidget, 構造函數包含

final OverlayEntry entry;
final bool tickerEnabled;

主要的作用是建立 _OverlayEntryWidgetState, 用途是控制要不要顯示透明的 OverlayEntry

_OverlayEntryWidgetState

OverlayEntry.maintainState 被設為 true 時, ticker 是 disabled, 但 maintainState 通常是 false,build 建立 TickerMode, initState 將 entry 標記為 mounted, dispose 標記為 unmounted.

  @override
  void initState() {
    super.initState();
    widget.entry._updateMounted(true);
  }

  @override
  void dispose() {
    widget.entry._updateMounted(false);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TickerMode(
      enabled: widget.tickerEnabled,
      child: widget.entry.builder(context),
    );
  }

TickerMode

TickerMode 用來禁用或啟用子元件樹的 ticker,也就是說它控制底動畫的生效,或是無效。透過這個元件,我們不需要改變子元件任何屬性進行重建,就可以將動畫關閉或打開。

OverlayState

Overlay.build 負責把 entry 轉成 Widget,並且放進 _Theatre_Theatre 是一個特殊的類 Stack 元件,這裡先有個印象即可,後面再詳述。

OverlayEntry 被設為透明,則該 entry 不會被放到 widget tree。以下是相關的原始碼,邏輯很清楚,就不解釋了。

  @override
  Widget build(BuildContext context) {
    // This list is filled backwards and then reversed below before
    // it is added to the tree.
    final List<> children = <Widget>[];
    bool onstage = true;
    int onstageCount = 0;
    for (int i = _entries.length - 1; i >= 0; i -= 1) {
      final OverlayEntry entry = _entries[i];
      if (onstage) {
        onstageCount += 1;
        children.add(_OverlayEntryWidget(
          key: entry._key,
          entry: entry,
        ));
        if (entry.opaque)
          onstage = false;
      } else if (entry.maintainState) {
        children.add(_OverlayEntryWidget(
          key: entry._key,
          entry: entry,
          tickerEnabled: false,
        ));
      }
    }
    return _Theatre(
      skipCount: children.length - onstageCount,
      clipBehavior: widget.clipBehavior,
      children: children.reversed.toList(growable: false),
    );
  }

_Theatre

_Theatre 是特殊的 Stack Widget,skipCount 參數表示前面幾個子元件不顯示。該元件透過自訂佈局 Widget 實現。

Overlay 的 children 會把透明的元件排序到最前面,並在這裡指定不做顯示。


classDiagram

  class Theatre {
  }

  class _TheatreElement {

  }

  class _RenderTheatre {
  }

  class MultiChildRenderObjectWidget {
  }

  class MultiChildRenderObjectElement {
  }

  class RenderBox {
  }

  class ContainerRenderObjectMixin {
  }

  Theatre <|-- MultiChildRenderObjectWidget
  _TheatreElement <|-- MultiChildRenderObjectElement
  _RenderTheatre <|-- RenderBox
  ContainerRenderObjectMixin --* _RenderTheatre
  ContainerRenderObjectMixin --> RenderBox
  ContainerRenderObjectMixin --> StackParentData
  Theatre --> _theatreElement
  Theatre --> _renderTheatre
  MultiChildRenderObjectWidget --> _renderTheatre
  MultiChildRenderObjectElement --> _theatreElement
  RenderBox --> _renderTheatre

_TheatreElement

繼承 MultiChildRenderObjectElement,只在 debug 增加資訊,不需細看。

RenderTheatre

  • 利用 RenderStack 這個 class 處理 layout。
  • RenderTheatre 的寬高是第一個 onstage 的寬(width)跟高(height)。

關於 IntrinsicWidth/Height,如果重寫了 performLayout 方法,則進而需要重寫以下四個方法:

  • computeMaxIntrinsicWidth(double height):用於計算一個最小寬度(沒錯,是最小寬度),在最終 size.width 超過該寬度時,也不會減少 size.height(例如,對文字排版,將文字排成一行需要的最小寬度就是這裡的 MaxIntrinsicWidth,因為再增加寬度也不會減少文字的高度)。
  • computeMinIntrinsicWidth(double height):排版需要的最小寬度,若小於這個寬度內容就會被裁剪。
  • computeMinIntrinsicHeight:與 computeMinIntrinsicWidth 類似,用於計算一個最小高度,在最終 size.height 超過該高度時,也不會減少 size.width
  • computeMaxIntrinsicHeight:與 computeMaxIntrinsicWidth 類似,用於計算一個最大高度,在最終 size.height 小於該高度時,也不會減少 size.width

重寫這些方法的原因:

  • performLayout 方法用於安排子元素的位置。如果重寫了 performLayout 方法,則需要確保它能夠正確地計算子元素的尺寸。
  • computeMaxIntrinsicWidthcomputeMinIntrinsicWidthcomputeMinIntrinsicHeightcomputeMaxIntrinsicHeight 方法用於計算 widget 的內在尺寸。如果重寫了 performLayout 方法,則需要確保這些方法能夠正確地計算 widget 的內在尺寸,以便 performLayout 方法能夠正確地安排子元素的位置。

重寫這些方法時需要注意的事項:

  • 需要確保這些方法能夠正確地計算 widget 的內在尺寸。
  • 需要確保這些方法與 performLayout 方法一起使用時能夠正常工作。

paint

Overlay 小部件的 paint 方法會遍歷 Overlay 的子 widget 列表,並調用每個子 widget 的 paint 方法。每個子 widget 的 paint 方法負責繪製該子 widget。

如果任一 child 有 visual overflow,則 _hasVisualOverflow 為 true, 在 paint 階段要畫面裁減。

_hasVisualOverflow = RenderStack.layoutPositionedChild(child, childParentData, size, _resolvedAlignment!) || _hasVisualOverflow;

如果有 overflow,透過 context.pushClipRect 對畫面進行裁減。

StackParentData

StackParentData 是 Stack 小部件使用的 ParentData 子類,它存儲有關 Stack 中子 widget 的位置和對齊信息。

StackParentData 具有以下屬性:

  • offset:此屬性指定子小部件在 Stack 中的偏移量。默認情況下,offset 為 (0.0, 0.0),這意味著子小部件位於 Stack 的左上角。
  • alignment:此屬性指定子小部件在 Stack 中的對齊方式。默認情況下,alignment 為 Alignment.center,這意味著子小部件在 Stack 中居中對齊。

在這裡的用法是用 isPositioned 判斷是否有把 Positioned 元件包住。