Flutter之使用Overlay創建全局Toast并靜態調用

Toast在Android上是最常用的提示組件了,它的優勢在于靜態調用、全局顯示,可以在任意你想要的地方調用他而絲毫不影響界面的布局,調用簡單程度與Logger的調用不相上下。
然而在Flutter中并沒有給我們提供Toast的接口,想要實現Toast的效果有兩種途徑,一種是接Android/iOS原生工程,第二種是不依托于使用Flutter來實現。
本篇選用第二種方案來實現,接原生代碼一方面要求雙端開發工作量和門檻都較大,而且不利于以后的樣式擴展,二是純Flutter實現的Toast確實效果非常好,自定義樣式也非常的方便。使用Flutter相對于RN來說,Flutter的渲染引擎是非常強大的,基本上能用Flutter實現的效果都不建議接原生,而RN則沒有自己的渲染引擎,性能的限制造成RN需要頻繁的接入原生模塊,這也是我傾心Flutter的原因。

效果圖

本篇要用的核心組件是Overlay,這個組件提供了動態的在Flutter的渲染樹上插入布局的特性,從而讓我們有了在包括路由在內的所有組件的上層插入toast的可能性。

創建Flutter工程

本品系列的Flutter博客都會以創建純凈的Flutter工程開篇,創建工程后,放一個Button在布局中,便于觸發Toast調用。
代碼:略。

使用Overlay插入Toast布局

因為我們要實現全局的靜態調用,所以這里先創建一個工具類,并在這個類中創建靜態方法show:

class Toast {
    
    static show(BuildContext context, String msg) {
        //這里實現toast的彈出邏輯
    }

}

這是一種很常見的靜態調用方式,是需要在你的某個回調中調用Toast.show(context, "你的消息提示");即可完成toast的顯示,而不用考慮布局嵌套問題。

下面我們就在show方法中向布局中插入一個toast:

class Toast {
  static show(BuildContext context, String msg) {
    var overlayState = Overlay.of(context);
    OverlayEntry overlayEntry;
    overlayEntry = new OverlayEntry(builder: (context) {
      return buildToastLayout(msg);
    });
    overlayState.insert(overlayEntry);
  }

  static LayoutBuilder buildToastLayout(String msg) {
    return LayoutBuilder(builder: (context, constraints) {
      return IgnorePointer(
        ignoring: true,
        child: Container(
          child: Material(
            color: Colors.white.withOpacity(0),
            child: Container(
              child: Container(
                child: Text(
                  "${msg}",
                  style: TextStyle(color: Colors.white),
                ),
                decoration: BoxDecoration(
                  color: Colors.black.withOpacity(0.6),
                  borderRadius: BorderRadius.all(
                    Radius.circular(5),
                  ),
                ),
                padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
              ),
              margin: EdgeInsets.only(
                bottom: constraints.biggest.height * 0.15,
                left: constraints.biggest.width * 0.2,
                right: constraints.biggest.width * 0.2,
              ),
            ),
          ),
          alignment: Alignment.bottomCenter,
        ),
      );
    });
  }
}

在show方法中使用Overlay插入了一個OverlayEntry,而OverlayEntry負責構建布局,buildToastLayout方法這是一個正常的布局構建方法,通過這個方法我們構建了一個Toast樣式的ToastView,并通過OverlayEntry插入到了整個布局的最上層。
這時候通過調用Toast.show方法就能在界面上看到一個Toast樣式的提示了。
但是,這個ToastView是不會消失的,它會一直呆在界面上,這顯然不是我們想要的。

讓Toast自動消失

我們繼續改造這個Toast,讓它能夠自動消失。
創建一個叫做ToastView的類,便于控制每次插入的ToastView:

class ToastView {
  OverlayEntry overlayEntry;
  OverlayState overlayState;
  bool dismissed = false;

  _show() async {
    overlayState.insert(overlayEntry);
    await Future.delayed(Duration(milliseconds: 3500));
    this.dismiss();
  }

  dismiss() async {
    if (dismissed) {
      return;
    }
    this.dismissed = true;
    overlayEntry?.remove();
  }
}

這樣,就把ToastView的顯示和消失的控制封裝起來了。然后在Toast的show方法中對他進行調用

class Toast {
  static show(BuildContext context, String msg) {
    var overlayState = Overlay.of(context);
    OverlayEntry overlayEntry;
    overlayEntry = new OverlayEntry(builder: (context) {
      return buildToastLayout(msg);
    });
    var toastView = ToastView();
    toastView.overlayState = overlayState;
    toastView.overlayEntry = overlayEntry;
    toastView._show();
  }
  ...
}

通過上面的方法,已經實現了Toast的全局靜態調用,并插入全局布局,并在顯示3.5秒后自動消失的Toast,但是這個toast好像怪怪的,沒錯,他沒有動畫,下面來給這個toast增加動畫。

給Toast增加動畫

這個Toast的動畫算是Flutter的高級應用了,它涉及到了縮放,位移,自定義差值器,AnimatedBuilder等特性,本篇的核心在介紹Overlay的使用和ToastView的封裝,關于動畫的使用如果在這里講就發散的太多了,篇幅限制以后單獨來講動畫吧,這里以你對動畫系統了解的前提來講解。

class Toast {
  static show(BuildContext context, String msg) {
    var overlayState = Overlay.of(context);
    var controllerShowAnim = new AnimationController(
      vsync: overlayState,
      duration: Duration(milliseconds: 250),
    );
    var controllerShowOffset = new AnimationController(
      vsync: overlayState,
      duration: Duration(milliseconds: 350),
    );
    var controllerHide = new AnimationController(
      vsync: overlayState,
      duration: Duration(milliseconds: 250),
    );
    var opacityAnim1 =
        new Tween(begin: 0.0, end: 1.0).animate(controllerShowAnim);
    var controllerCurvedShowOffset = new CurvedAnimation(
        parent: controllerShowOffset, curve: _BounceOutCurve._());
    var offsetAnim =
        new Tween(begin: 30.0, end: 0.0).animate(controllerCurvedShowOffset);
    var opacityAnim2 = new Tween(begin: 1.0, end: 0.0).animate(controllerHide);

    OverlayEntry overlayEntry;
    overlayEntry = new OverlayEntry(builder: (context) {
      return ToastWidget(
        opacityAnim1: opacityAnim1,
        opacityAnim2: opacityAnim2,
        offsetAnim: offsetAnim,
        child: buildToastLayout(msg),
      );
    });
    var toastView = ToastView();
    toastView.overlayEntry = overlayEntry;
    toastView.controllerShowAnim = controllerShowAnim;
    toastView.controllerShowOffset = controllerShowOffset;
    toastView.controllerHide = controllerHide;
    toastView.overlayState = overlayState;
    preToast = toastView;
    toastView._show();
  }
  ...
}

class ToastView {
  OverlayEntry overlayEntry;
  AnimationController controllerShowAnim;
  AnimationController controllerShowOffset;
  AnimationController controllerHide;
  OverlayState overlayState;
  bool dismissed = false;

  _show() async {
    overlayState.insert(overlayEntry);
    controllerShowAnim.forward();
    controllerShowOffset.forward();
    await Future.delayed(Duration(milliseconds: 3500));
    this.dismiss();
  }

  dismiss() async {
    if (dismissed) {
      return;
    }
    this.dismissed = true;
    controllerHide.forward();
    await Future.delayed(Duration(milliseconds: 250));
    overlayEntry?.remove();
  }
}

class ToastWidget extends StatelessWidget {
  final Widget child;
  final Animation<double> opacityAnim1;
  final Animation<double> opacityAnim2;
  final Animation<double> offsetAnim;

  ToastWidget(
      {this.child, this.offsetAnim, this.opacityAnim1, this.opacityAnim2});

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: opacityAnim1,
      child: child,
      builder: (context, child_to_build) {
        return Opacity(
          opacity: opacityAnim1.value,
          child: AnimatedBuilder(
            animation: offsetAnim,
            builder: (context, _) {
              return Transform.translate(
                offset: Offset(0, offsetAnim.value),
                child: AnimatedBuilder(
                  animation: opacityAnim2,
                  builder: (context, _) {
                    return Opacity(
                      opacity: opacityAnim2.value,
                      child: child_to_build,
                    );
                  },
                ),
              );
            },
          ),
        );
      },
    );
  }
}

class _BounceOutCurve extends Curve {
  const _BounceOutCurve._();

  @override
  double transform(double t) {
    t -= 1.0;
    return t * t * ((2 + 1) * t + 2) + 1.0;
  }
}

這是段非常長的代碼,本來是不想往上面貼這么多代碼的,但是動畫這塊兒講的話篇幅又太長,不貼代碼的話講起來又太空洞,只能貼了,大概說一下。
上面代碼分為四段:
第一段,在show方法中創建3個動畫,Toast顯示的位移和漸顯動畫,Toast消失的漸隱動畫,然后把這三個動畫的controller交給ToastView來控制動畫播放。
第二段,在ToastView中接收三個動畫controller,并在show和dismiss方法中控制動畫的播放。
第三段,創建一個自定義Widget,并使用三個AnimatedBuilder來實現動畫,并在show方法中把Toast的布局包裹起來。
第四段,定義了一個動畫差值器,Flutter中提供了很多動畫差值器,但是并沒有我們需要的,所以這里定義一個彈跳一次后回彈的動畫差值器用來控制ToastView的偏移動畫效果。

到目前為止,這個Toast已經滿足了最基本的樣式,全局調用,動畫彈出,延遲3.5秒后自動漸隱消失。

防止連續調用造成toast堆疊

但是還存在一個問題,因為Toast的樣式的半透明的黑色,如果連續調用多次的話,會有多個Toast同時彈出,并堆疊在一起,會顯得非常的黑。

下面再做一個處理,在show之前,判斷是否已經有一個Toast在顯示了,如果有,即刻把它dismiss了。

  static ToastView preToast;

  static show(BuildContext context, String msg) {
    preToast?.dismiss();
    preToast = null;
    ...
    preToast = toastView;
    toastView._show();
  }
  ...
}

這樣就可以了,?.操作符和kotlin的效果是一樣的,空指針安全,很舒服。


更多干貨移步我的個人博客 https://www.nightfarmer.top/

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容