說說Flutter中的Overlay

Overlay是什麼

​ Flutter中的Overlay是什麼?先看看官方的文檔註釋吧:web

/// A [Stack] of entries that can be managed independently.
///
/// Overlays let independent child widgets "float" visual elements on top of
/// other widgets by inserting them into the overlay's [Stack]. The overlay lets
/// each of these widgets manage their participation in the overlay using
/// [OverlayEntry] objects.
複製代碼

​ 簡單總結一下,Overlay是一個能夠管理的堆棧。咱們能夠經過將一個Widget插入這個堆棧中,這樣就可讓此Widget浮在其餘的Widget之上,從而實現懸浮窗效果。咱們能夠經過OverlayEntry對象的配置來管理Overlay的層級關係。markdown

Overlay建立流程

​ 經過追蹤構造方法,咱們能夠發現它被Navigator所建立,而Navigator又被WidgetsApp所建立。一般咱們runApp啓動的page裏會建立的CupertinoApp或者MaterialApp都會建立WidgetsApp,所以咱們在項目中能夠直接使用這個建立好的Overlayapp

簡單使用

@override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback(
      (timeStamp) {
        OverlayEntry entry = OverlayEntry(
          builder: (_) {
            return IgnorePointer(
              child: Center(
                child: Text('OverlayEntry'),
              ),
            );
          },
        );
        Overlay.of(context).insert(entry);
      },
    );
  }
複製代碼

Overlay的使用很簡單,經過Overlay.of(context)方法向上查找到OverlayState,而後經過insert或者insertAll方法插入一個或多個懸浮Widget。若是想要移除此懸浮Widget,咱們能夠經過OverlayEntryremove()方法實現。ide

Overlay原理源碼解析

爲何Overlay插入的OverlayEntry會在頁面上面懸浮呢?先來看看insert方法作了什麼:佈局

void insert(OverlayEntry entry, { OverlayEntry below, OverlayEntry above }) {
  ...
  entry._overlay = this;
  setState(() {
    _entries.insert(_insertionIndex(below, above), entry);
  });
}
複製代碼

​ 能夠看到OverlayEntry被插入了_entries中的指定位置,再看_insertionIndex的實現:post

int _insertionIndex(OverlayEntry below, OverlayEntry above) {
  assert(above == null || below == null);
  if (below != null)
    return _entries.indexOf(below);
  if (above != null)
    return _entries.indexOf(above) + 1;
  return _entries.length;
}
複製代碼

​ 咱們沒有指定可選參數的時候,默認會將OverlayEntry插入到_entries的末端。再看看OverlayStatebuild方法:ui

@override
  Widget build(BuildContext context) {
    // This list is filled backwards and then reversed below before
    // it is added to the tree.
    final List<Widget> 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,
      children: children.reversed.toList(growable: false),
    );
  }
複製代碼

build中會倒序遍歷_entries,而後生成一個個_OverlayEntryWidget,最後這個列表又會被倒序放入_Theatre這個控件中。那這個_Theatre是什麼呢?this

/// Special version of a [Stack], that doesn't layout and render the first
/// [skipCount] children.
///
/// The first [skipCount] children are considered "offstage".
class _Theatre extends MultiChildRenderObjectWidget 複製代碼

​ 仍是得看源碼,能夠看出_Theatre繼承本身MultiChildRenderObjectWidget,這就好辦了,結合我以前寫的深刻研究Flutter佈局原理,我只用關心paint方法就能夠知道這些children是如何繪製的了,來看paint方法:url

@protected
  void paintStack(PaintingContext context, Offset offset) {
    RenderBox child = _firstOnstageChild;
    while (child != null) {
      final StackParentData childParentData = child.parentData as StackParentData;
      context.paintChild(child, childParentData.offset + offset);
      child = childParentData.nextSibling;
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (_hasVisualOverflow) {
      context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintStack);
    } else {
      paintStack(context, offset);
    }
  }
複製代碼

paint很簡單,他會從第一個child開始循環繪製,也就是說children會被一層一層的繪製在屏幕上,那麼咱們最後插入的OverlayEntry天然是在最上了。spa

​ 可能到這裏仍是會有疑惑,插入的OverlayEntry只是在衆多的OverlayEntry當中的最上,爲啥會在各個頁面之上呢?其實到這裏也能夠大概纔出緣由了:

/// Although you can create an [Overlay] directly, it's most common to use the
/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
/// navigator uses its overlay to manage the visual appearance of its routes.
複製代碼

Overlay的註釋中還有這麼一段:The navigator uses its overlay to manage the visual appearance of its routes.,因此,咱們的各個頁面其實也是一個個OverlayEntryNavigator的源碼仍是比較繞的,追蹤源碼看看:

@optionalTypeArgs
Future<T> push<T extends Object>(Route<T> route) {
  ...
  _history.add(_RouteEntry(route, initialState: _RouteLifecycle.push));
  _flushHistoryUpdates();
  ...
  _afterNavigation(route);
  return route.popped;
}
複製代碼

​ 當咱們執行Navigatorpush方法後,會先放_history中增長一條路由記錄,而後執行 _flushHistoryUpdates 方法:

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  int index = _history.length - 1;
  ...
  final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];
  while (index >= 0) {
    switch (entry.currentState) {
     ...
      case _RouteLifecycle.push:
      case _RouteLifecycle.pushReplace:
      case _RouteLifecycle.replace:
        assert(rearrangeOverlay);
        entry.handlePush(
          navigator: this,
          previous: previous?.route,
          previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
          isNewFirst: next == null,
        );
        assert(entry.currentState != _RouteLifecycle.push);
        assert(entry.currentState != _RouteLifecycle.pushReplace);
        assert(entry.currentState != _RouteLifecycle.replace);
        if (entry.currentState == _RouteLifecycle.idle) {
          continue;
        }
        break;
     ...
      case _RouteLifecycle.disposed:
      case _RouteLifecycle.staging:
        assert(false);
        break;
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  ...
  if (rearrangeOverlay)
    overlay?.rearrange(_allRouteOverlayEntries);
}
複製代碼

_flushHistoryUpdates方法很長,這裏看要點,push方法執行後會加入的_RouteEntry,會執行handlePush方法,handlePush方法會執行Routeinstall方法,看看咱們經常使用的MaterialPageRoute的繼承關係:

image-20201210201648659

藏得真的深,咱們能夠發現MaterialPageRoute有一個父類OverlayRoute,看看OverlayRoute中的install方法:

@override
void install() {
  assert(_overlayEntries.isEmpty);
  _overlayEntries.addAll(createOverlayEntries());
  super.install();
}
複製代碼

install方法會執行createOverlayEntries方法生成Iterable<OverlayEntry>並加入列表。到這裏咱們也大概能夠搞清楚了,push一個頁面本質上仍是push了一個OverlayEntry_flushHistoryUpdates方法最終會調用OverlayStaterearrange方法處理列表:

void rearrange(Iterable<OverlayEntry> newEntries, { OverlayEntry below, OverlayEntry above }) {
  final List<OverlayEntry> newEntriesList = newEntries is List<OverlayEntry> ? newEntries : newEntries.toList(growable: false);
  ...
  if (newEntriesList.isEmpty)
    return;
  if (listEquals(_entries, newEntriesList))
    return;
  final LinkedHashSet<OverlayEntry> old = LinkedHashSet<OverlayEntry>.from(_entries);
  for (final OverlayEntry entry in newEntriesList) {
    entry._overlay ??= this;
  }
  setState(() {
    _entries.clear();
    _entries.addAll(newEntriesList);
    old.removeAll(newEntriesList);
    _entries.insertAll(_insertionIndex(below, above), old);
  });
}
複製代碼

rearrange方法會先複製一遍舊的_entries列表,而後清空_entries列表,加入最新的路由列表數據,再從舊列表中去重路由列表,這樣剩下的基本都是用戶本身insert的OverlayEntry,最後再將舊列表的數據插入新的列表,這樣用戶的OverlayEntry仍是會在各個頁面的上層。

相關文章
相關標籤/搜索