Flutter之使用Overlay創(chuàng)建全局Toast并靜態(tài)調(diào)用

Toast在Android上是最常用的提示組件了沦辙,它的優(yōu)勢(shì)在于靜態(tài)調(diào)用判族、全局顯示,可以在任意你想要的地方調(diào)用他而絲毫不影響界面的布局吝岭,調(diào)用簡(jiǎn)單程度與Logger的調(diào)用不相上下肋联。
然而在Flutter中并沒(méi)有給我們提供Toast的接口威蕉,想要實(shí)現(xiàn)Toast的效果有兩種途徑,一種是接Android/iOS原生工程牺蹄,第二種是不依托于使用Flutter來(lái)實(shí)現(xiàn)忘伞。
本篇選用第二種方案來(lái)實(shí)現(xiàn)薄翅,接原生代碼一方面要求雙端開(kāi)發(fā)工作量和門(mén)檻都較大沙兰,而且不利于以后的樣式擴(kuò)展,二是純Flutter實(shí)現(xiàn)的Toast確實(shí)效果非常好翘魄,自定義樣式也非常的方便鼎天。使用Flutter相對(duì)于RN來(lái)說(shuō),F(xiàn)lutter的渲染引擎是非常強(qiáng)大的暑竟,基本上能用Flutter實(shí)現(xiàn)的效果都不建議接原生斋射,而RN則沒(méi)有自己的渲染引擎育勺,性能的限制造成RN需要頻繁的接入原生模塊,這也是我傾心Flutter的原因罗岖。

效果圖

本篇要用的核心組件是Overlay涧至,這個(gè)組件提供了動(dòng)態(tài)的在Flutter的渲染樹(shù)上插入布局的特性,從而讓我們有了在包括路由在內(nèi)的所有組件的上層插入toast的可能性桑包。

創(chuàng)建Flutter工程

本品系列的Flutter博客都會(huì)以創(chuàng)建純凈的Flutter工程開(kāi)篇南蓬,創(chuàng)建工程后,放一個(gè)Button在布局中哑了,便于觸發(fā)Toast調(diào)用赘方。
代碼:略。

使用Overlay插入Toast布局

因?yàn)槲覀円獙?shí)現(xiàn)全局的靜態(tài)調(diào)用弱左,所以這里先創(chuàng)建一個(gè)工具類(lèi)窄陡,并在這個(gè)類(lèi)中創(chuàng)建靜態(tài)方法show:

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

}

這是一種很常見(jiàn)的靜態(tài)調(diào)用方式,是需要在你的某個(gè)回調(diào)中調(diào)用Toast.show(context, "你的消息提示");即可完成toast的顯示拆火,而不用考慮布局嵌套問(wèn)題跳夭。

下面我們就在show方法中向布局中插入一個(gè)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插入了一個(gè)OverlayEntry,而OverlayEntry負(fù)責(zé)構(gòu)建布局们镜,buildToastLayout方法這是一個(gè)正常的布局構(gòu)建方法优妙,通過(guò)這個(gè)方法我們構(gòu)建了一個(gè)Toast樣式的ToastView,并通過(guò)OverlayEntry插入到了整個(gè)布局的最上層憎账。
這時(shí)候通過(guò)調(diào)用Toast.show方法就能在界面上看到一個(gè)Toast樣式的提示了套硼。
但是,這個(gè)ToastView是不會(huì)消失的胞皱,它會(huì)一直呆在界面上邪意,這顯然不是我們想要的。

讓Toast自動(dòng)消失

我們繼續(xù)改造這個(gè)Toast反砌,讓它能夠自動(dòng)消失雾鬼。
創(chuàng)建一個(gè)叫做ToastView的類(lèi),便于控制每次插入的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的顯示和消失的控制封裝起來(lái)了策菜。然后在Toast的show方法中對(duì)他進(jìn)行調(diào)用

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();
  }
  ...
}

通過(guò)上面的方法,已經(jīng)實(shí)現(xiàn)了Toast的全局靜態(tài)調(diào)用酒贬,并插入全局布局又憨,并在顯示3.5秒后自動(dòng)消失的Toast,但是這個(gè)toast好像怪怪的锭吨,沒(méi)錯(cuò)蠢莺,他沒(méi)有動(dòng)畫(huà),下面來(lái)給這個(gè)toast增加動(dòng)畫(huà)零如。

給Toast增加動(dòng)畫(huà)

這個(gè)Toast的動(dòng)畫(huà)算是Flutter的高級(jí)應(yīng)用了躏将,它涉及到了縮放锄弱,位移,自定義差值器祸憋,AnimatedBuilder等特性会宪,本篇的核心在介紹Overlay的使用和ToastView的封裝,關(guān)于動(dòng)畫(huà)的使用如果在這里講就發(fā)散的太多了蚯窥,篇幅限制以后單獨(dú)來(lái)講動(dòng)畫(huà)吧狈谊,這里以你對(duì)動(dòng)畫(huà)系統(tǒng)了解的前提來(lái)講解。

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;
  }
}

這是段非常長(zhǎng)的代碼沟沙,本來(lái)是不想往上面貼這么多代碼的河劝,但是動(dòng)畫(huà)這塊兒講的話篇幅又太長(zhǎng),不貼代碼的話講起來(lái)又太空洞矛紫,只能貼了赎瞎,大概說(shuō)一下。
上面代碼分為四段:
第一段颊咬,在show方法中創(chuàng)建3個(gè)動(dòng)畫(huà)务甥,Toast顯示的位移和漸顯動(dòng)畫(huà),Toast消失的漸隱動(dòng)畫(huà)喳篇,然后把這三個(gè)動(dòng)畫(huà)的controller交給ToastView來(lái)控制動(dòng)畫(huà)播放敞临。
第二段,在ToastView中接收三個(gè)動(dòng)畫(huà)controller麸澜,并在show和dismiss方法中控制動(dòng)畫(huà)的播放挺尿。
第三段,創(chuàng)建一個(gè)自定義Widget炊邦,并使用三個(gè)AnimatedBuilder來(lái)實(shí)現(xiàn)動(dòng)畫(huà)编矾,并在show方法中把Toast的布局包裹起來(lái)。
第四段馁害,定義了一個(gè)動(dòng)畫(huà)差值器窄俏,F(xiàn)lutter中提供了很多動(dòng)畫(huà)差值器,但是并沒(méi)有我們需要的碘菜,所以這里定義一個(gè)彈跳一次后回彈的動(dòng)畫(huà)差值器用來(lái)控制ToastView的偏移動(dòng)畫(huà)效果凹蜈。

到目前為止,這個(gè)Toast已經(jīng)滿足了最基本的樣式忍啸,全局調(diào)用仰坦,動(dòng)畫(huà)彈出,延遲3.5秒后自動(dòng)漸隱消失吊骤。

防止連續(xù)調(diào)用造成toast堆疊

但是還存在一個(gè)問(wèn)題缎岗,因?yàn)門(mén)oast的樣式的半透明的黑色,如果連續(xù)調(diào)用多次的話白粉,會(huì)有多個(gè)Toast同時(shí)彈出传泊,并堆疊在一起,會(huì)顯得非常的黑鸭巴。

下面再做一個(gè)處理眷细,在show之前,判斷是否已經(jīng)有一個(gè)Toast在顯示了鹃祖,如果有溪椎,即刻把它dismiss了。

  static ToastView preToast;

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

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


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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末歉秫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子养铸,更是在濱河造成了極大的恐慌雁芙,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钞螟,死亡現(xiàn)場(chǎng)離奇詭異兔甘,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)鳞滨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)洞焙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人拯啦,你說(shuō)我怎么就攤上這事闽晦。” “怎么了提岔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵仙蛉,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我碱蒙,道長(zhǎng)荠瘪,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任赛惩,我火速辦了婚禮哀墓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘喷兼。我一直安慰自己篮绰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布季惯。 她就那樣靜靜地躺著吠各,像睡著了一般臀突。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贾漏,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天候学,我揣著相機(jī)與錄音,去河邊找鬼纵散。 笑死梳码,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的伍掀。 我是一名探鬼主播掰茶,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蜜笤!你這毒婦竟也來(lái)了濒蒋?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瘩例,失蹤者是張志新(化名)和其女友劉穎啊胶,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體垛贤,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡焰坪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了聘惦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片某饰。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖善绎,靈堂內(nèi)的尸體忽然破棺而出黔漂,到底是詐尸還是另有隱情,我是刑警寧澤禀酱,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布炬守,位于F島的核電站,受9級(jí)特大地震影響剂跟,放射性物質(zhì)發(fā)生泄漏减途。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一曹洽、第九天 我趴在偏房一處隱蔽的房頂上張望鳍置。 院中可真熱鬧,春花似錦送淆、人聲如沸税产。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)辟拷。三九已至撞羽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間梧兼,已是汗流浹背放吩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工智听, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留羽杰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓到推,卻偏偏與公主長(zhǎng)得像考赛,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子莉测,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容