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的效果是一樣的,空指標安全,很舒服。
更多幹貨移步我的個人部落格 www.nightfarmer.top/