【Flutter 專題】89 圖解基本 Overlay 懸浮新手引導

阿策小和尚發表於2021-07-20

      隨著業務的擴充套件和延伸,需要的功能也是多種多樣,而同一種效果可以有多種實現方案;小菜今天學習一下通過 Overlay 實現基本的懸浮引導效果;

      Overlay 以浮層的方式管理單獨的 item 儲存在棧中(後進先出);Overlay 其原始碼也是採用的 Stack 浮層,將 OverEntry 逐個加入到 Overlay 中進行展示,OverEntry 可以使用 PositionedAnimatedPositionedOverlay 中定義自身的位置;

      當建立 MaterialApp 時,它會自動建立一個 Navigator,之後建立一個 Overlay,然後利用這個 Navigator 來管理路由中的介面;

原始碼分析

const Overlay({
    Key key,
    this.initialEntries = const <OverlayEntry>[],
})

class OverlayEntry 
  OverlayEntry({
    @required this.builder,
    bool opaque = false,
    bool maintainState = false,
  })
}
複製程式碼

      分析原始碼可知,Overlay 主要是由 OverlayEntry 浮層元素組成的,並以棧的方式儲存;opaque 為當前浮層元素是否遮蓋整個 Overlay 浮層;maintainState 一般與 opaque 共同使用,是否將不透明的浮層元素新增到 Widget Tree 中;

案例嘗試

      Overlay 作為浮層的應用效果很廣泛,網上很多老師都通過 Overlay 實現自定義 Toast / Dialog / PopupMenu / List item 等,但小菜嘗試通過 Overlay 實現升級過程中的新手引導;       Overlay 主要是通過 insert / insertAll 方式加入 OverEntry 浮層元素,通過 remove 移除浮層元素;

insert One OverEnrty

      如果僅需展示一個 OverEntry 浮層元素,可以通過 insert 加入到 Overlay 中,也可以通過 insertAll 加入僅有一個 OverEntry 的陣列;最終通過 remove 關閉浮層元素,注意陣列中的元素要全部 remove

// insert
overlayEntry = OverlayEntry(builder: (context) {
  return Stack(children: <Widget>[Positioned(
        top: (height - 200) * 0.5, left: (width - 200) * 0.5,
        child: GestureDetector(
            onTap: () => overlayEntry.remove(),
            child: _itemContainer(Colors.blue.withOpacity(0.6))))
  ]);
});
Overlay.of(context).insert(overlayEntry);
複製程式碼

// insertAll
overlayEntry = OverlayEntry(builder: (context) {
  return Stack(children: <Widget>[Positioned(
        top: (height - 200) * 0.5, left: (width - 200) * 0.5,
        child: GestureDetector(
            onTap: () { overlayEntry.remove(); overlayEntryList.clear(); },
            child: _itemContainer(Colors.brown.withOpacity(0.6))))
  ]);
});
overlayEntryList.add(overlayEntry);
Overlay.of(context).insertAll(overlayEntryList);
複製程式碼

insert Three OverEntrys

      如果需要展示多個 OverEntry 浮層元素時,只能用 insertAll 新增到 Overlay 中,其中預設是以棧方式加入的;其中 insertAll 會一次性的把所有 OverEntry 均加入到 Overlay 中;

overlayEntryList.add(OverlayEntry(builder: (context) {
  return Stack(children: <Widget>[
    Positioned(top: (height - 200) * 0.5 - 50, left: (width - 200) * 0.5 - 50,
        child: GestureDetector(
            onTap: () {
              --overIndex;
              overlayEntryList[overIndex].remove();
              overlayEntryList.removeLast();
            },
            child: _itemContainer(Colors.red.withOpacity(0.6))))
  ]);
}));
overlayEntryList.add(OverlayEntry(builder: (context) {
  return Stack(children: <Widget>[
    Positioned(top: (height - 200) * 0.5, left: (width - 200) * 0.5,
        child: GestureDetector(
            onTap: () {
              --overIndex;
              overlayEntryList[overIndex].remove();
              overlayEntryList.removeLast();
            },
            child: _itemContainer(Colors.orange.withOpacity(0.6))))
  ]);
}));
overlayEntryList.add(OverlayEntry(builder: (context) {
  return Stack(children: <Widget>[
    Positioned(top: (height - 200) * 0.5 + 50, left: (width - 200) * 0.5 + 50,
        child: GestureDetector(
            onTap: () {
              --overIndex;
              overlayEntryList[overIndex].remove();
              overlayEntryList.removeLast();
            },
            child: _itemContainer(Colors.blue.withOpacity(0.6))))
  ]);
}));
overIndex = overlayEntryList.length;
Overlay.of(context).insertAll(overlayEntryList);
複製程式碼

      若需要逐次展示多個 OverlayEntry 可以在點選事件中單獨加入新的 OverlayEntry

overlayEntry = OverlayEntry(builder: (context) {
  return Stack(children: <Widget>[
    Positioned(top: (height - 200) * 0.5 - 50, left: (width - 200) * 0.5 - 50,
        child: GestureDetector(
            onTap: () {
              overlayEntry.remove();
              Overlay.of(this.context).insert(overlayEntry2);
            },
            child: _itemContainer(Colors.red.withOpacity(0.6))))
  ]);
});
overlayEntry2 = OverlayEntry(builder: (context) {
  return Stack(children: <Widget>[
    Positioned(top: (height - 200) * 0.5, left: (width - 200) * 0.5,
        child: GestureDetector(
            onTap: () {
              overlayEntry2.remove();
              Overlay.of(this.context).insert(overlayEntry3);
            },
            child: _itemContainer(Colors.orange.withOpacity(0.6))))
  ]);
});
overlayEntry3 = OverlayEntry(builder: (context) {
  return Stack(children: <Widget>[
    Positioned(top: (height - 200) * 0.5 + 50, left: (width - 200) * 0.5 + 50,
        child: GestureDetector(onTap: () => overlayEntry3.remove(), child: _itemContainer(Colors.blue.withOpacity(0.6))))
  ]);
});
Overlay.of(context).insert(overlayEntry);
複製程式碼

注意事項

1. Overlay 為全域性覆蓋,並非當前 Page,需要重新定義返回按鍵等;若沒有 remove 則返回上一個頁面依然展示浮層元素;若 remove 其他未加入浮層的元素會返回失敗;
return WillPopScope(
    onWillPop: () async {
      if (overListIndex == 6) {
        for (int i = overlayEntryList.length; i > 0; i--) {
          overlayEntryList[i - 1].remove();
        }
        overlayEntryList.clear();
        overIndex = 0;
      } else if (overListIndex == 7) {
        overlayEntry.remove();
      } else if (overListIndex == 8) {
        overlayEntry2.remove();
      } else if (overListIndex == 9) {
        overlayEntry3.remove();
      }
      if (overIndex == 4) {
        overlayEntry.remove();
        overlayEntry0.remove();
      } else if (overIndex == 3) {
        overlayEntry2.remove();
        overlayEntry0.remove();
      } else if (overIndex == 2) {
        overlayEntry3.remove();
        overlayEntry0.remove();
      } else if (overIndex == 5) {
        overlayEntry.remove();
      }
      overIndex = 0;
      return true;
    },
    child: Container(...)
);
複製程式碼

2. 使用 insertAll 新增浮層元素時,不要同時加入多次同一個 OverlayEntry;
overlayEntry = OverlayEntry(builder: (context) {
  return Stack(children: <Widget>[
    Positioned(top: (height - 200) * 0.5 - 50, left: (width - 200) * 0.5 - 50,
        child: GestureDetector(
            onTap: () => overlayEntry.remove(),
            child: _itemContainer(Colors.red.withOpacity(0.6))))
  ]);
});
// 錯誤寫法,加入多次同一個 OverlayEntry
overlayEntryList.add(overlayEntry);
overlayEntryList.add(overlayEntry);
overlayEntryList.add(overlayEntry);

Overlay.of(this.context).insertAll(overlayEntryList);
複製程式碼

3. opaque = true 時會完全覆蓋之前的浮層元素,為不透明的,且不可透過當前浮層點選下一個浮層元素;maintainState 為在上層元素 opaque = true,即不透明的完全覆蓋下層元素時,被覆蓋的這個元素設定的 maintainState 是否提前構建;
overlayEntryList.add(OverlayEntry(builder: (context) {
  return Stack(children: <Widget>[
    Positioned( top: (height - 200) * 0.5 - 50, left: (width - 200) * 0.5 - 50,
        child: GestureDetector(
            onTap: () { --overIndex; overlayEntryList[overIndex].remove(); },
            child: _itemContainer(Colors.red.withOpacity(0.6))))
  ]);
}));
overlayEntryList.add(OverlayEntry(
    opaque: true, maintainState: true,
    builder: (context) {
      return Material(
          color: Colors.amber.withOpacity(0.4),
          child: Stack(children: <Widget>[
            Positioned( top: (height - 200) * 0.5, left: (width - 200) * 0.5,
                child: GestureDetector(
                    onTap: () { --overIndex; overlayEntryList[overIndex].remove(); },
                    child: _itemContainer(Colors.orange.withOpacity(0.6))))
          ]));
    }));
overlayEntryList.add(OverlayEntry(
    opaque: true, maintainState: false,
    builder: (context) {
      return Material(
          color: Colors.lightBlueAccent.withOpacity(0.4),
          child: Stack(children: <Widget>[
            Positioned( top: (height - 200) * 0.5 + 50, left: (width - 200) * 0.5 + 50,
                child: GestureDetector(
                    onTap: () { --overIndex; overlayEntryList[overIndex].remove(); },
                    child: _itemContainer(Colors.blue.withOpacity(0.6))))
          ]));
    }));
overIndex = overlayEntryList.length;
Overlay.of(context).insertAll(overlayEntryList);
複製程式碼


      Overlay 案例原始碼


      小菜對 Overlay 的嘗試還比較基礎,使用場景也比較小,如有錯誤,請多多指導!

阿策小和尚

相關文章