Flutter Overlay Widget Class 源碼解析
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
該類別繼承 ChangeNotifier
, builder
跟 markNeedsBuild
決定了 _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
方法,則需要確保它能夠正確地計算子元素的尺寸。computeMaxIntrinsicWidth
、computeMinIntrinsicWidth
、computeMinIntrinsicHeight
、computeMaxIntrinsicHeight
方法用於計算 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
元件包住。