之前写了一篇Flutter 利用OverlayEntry实现Toast的文章,由于只是个初创,还有很多不足的地方,这几天利用闲暇时间重新完善了一下,添加了成功、失败、消息、等待这几个状态并新增加了暗黑模式的判断:
首先我们还是先把需要实现的一些功能列一下:
- 利用OverlayEntry实现(关于OverlayEntry这个Widget如果有不清楚的可以自己百度一下)
- 可以设置Toast显示时间,比如显示3秒后自动移除Toast(Timer)
-
可以设置Toast显示位置,比如在顶部、中间、底部显示(Column)
在Flutter 利用OverlayEntry实现Toast中我们是使用 Positioned 达 到这个效果的,但在实际使用中我发现当居中和在底部显示的时候会有点问题,所以这里改为了 Column 实现。// Toast 显示位置 enum ToastPosition { top, center, bottom, }
- 可以设置Toast背景颜色
- 可以设置Toast显示的文本、文本的颜色和大小
-
各类型Toast给外部调用的显示方法
// Toast 类型 enum _ToastType { text, loading, success, error, info, custom }
-
暗黑模式判断
// 判断是否是暗黑模式 bool isDarkMode() { bool isDarkMode; final ThemeMode themeMode = ThemeMode.system; if (themeMode == ThemeMode.light || themeMode == ThemeMode.dark) { isDarkMode = themeMode == ThemeMode.dark; } else { isDarkMode = WidgetsBinding.instance.window.platformBrightness == Brightness.dark; } return isDarkMode; }
接下来定义一下我们需要开放给用户的参数:
class ToastStyle extends Diagnosticable {
const ToastStyle({
this.position = ToastPosition.bottom,
this.textSize = 14.0,
this.margin = const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
this.padding = const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
this.color,
this.textColor,
this.duration,
});
final Color color; // 背景颜色
final Color textColor; // 文本颜色
final double textSize; // 文字大小
final EdgeInsetsGeometry padding;// 内边距
final EdgeInsetsGeometry margin; // 外边距
final ToastPosition position; // 显示位置
final Duration duration; // 显示时间
}
定义各类型Toast暴露给用户调用的方法
class Toast {
// 等待
static void loading(BuildContext context, {String message}) => Toast.showLoading(context, message: message);
static void showLoading(BuildContext context, {String message, ToastStyle style}) =>
_showToast(
context,
message: message,
type: _ToastType.loading,
style: style == null ? ToastStyle(position: ToastPosition.center) : style
);
// 自定义
static void custom(BuildContext context, Widget customChild) => Toast.showCustom(context, customChild);
static void showCustom(BuildContext context, Widget customChild, {ToastStyle style}) =>
_showToast(
context,
customChild: customChild,
type: _ToastType.custom,
style: style == null ? ToastStyle() : style
);
// 文本
static void text(BuildContext context, String message) => Toast.showText(context, message);
static void showText(BuildContext context, String message, {ToastStyle style}) =>
_showToast(
context,
message: message,
type: _ToastType.text,
style: style == null ? ToastStyle() : style
);
// 成功
static void success(BuildContext context, String message) => Toast.showSuccess(context, message);
static void showSuccess(BuildContext context, String message, {ToastStyle style}) =>
_showToast(
context,
message: message,
type: _ToastType.success,
style: style == null ? ToastStyle(position: ToastPosition.center) : style,
);
// 失败
static void error(BuildContext context, String message) => Toast.showError(context, message);
static void showError(BuildContext context, String message, {ToastStyle style}) =>
_showToast(
context,
message: message,
type: _ToastType.error,
style: style == null ? ToastStyle(position: ToastPosition.center) : style,
);
// 消息
static void info(BuildContext context, String message) => Toast.showInfo(context, message);
static void showInfo(BuildContext context, String message, {ToastStyle style}) =>
_showToast(
context,
message: message,
type: _ToastType.info,
style: style == null ? ToastStyle(position: ToastPosition.center) : style,
);
}
提取的 _showToast 方法和 Toast 自动消失设置:
class Toast {
static OverlayEntry _overlayEntry; // UI浮层
static Timer _timer; // 计时,如果计时大于 _seconds,则移除Toast
// 显示
static void _showToast(
BuildContext context,
{
String message,
Widget customChild,
_ToastType type,
ToastStyle style
}
) async {
// 显示之前先把之前的浮层清空
_cancelToast();
_overlayEntry = OverlayEntry(
builder: (BuildContext context) => SafeArea(
// 当类型为 _ToastType.loading 时,
// 我希望页面布局的事件不被执行,在 _containerWidget 外面包裹一层 Material 并且设置颜色,
// 其它类型直接加载 _containerWidget
child: type == _ToastType.loading ? Material(
// 在 _containerWidget 外面包裹一层 Material 并且设置颜色之后,事件就不能穿透了
color: Colors.transparent,
child: _containerWidget(context,
message: message,
customChild: customChild,
type: type,
style: style),
) : _containerWidget(
context,
message: message,
customChild: customChild,
type: type,
style: style),
),
);
// 当 Toast 类型不为 _ToastType.custom 和 _ToastType.loading,
// 并且 style.duration == null 时,
// 我们给它一个默认值 Duration(seconds: 3)
_startTimer((type != _ToastType.custom
&& style.duration == null
&& type != _ToastType.loading) ? Duration(seconds: 3) : style.duration);
Overlay.of(context).insert(_overlayEntry);
}
// 开启倒计时
static void _startTimer(Duration duration) {
// 当 duration == null 时,Toast 长存不自动消失
if (duration == null) return;
_timer = Timer(duration, (){
_cancelToast();
});
}
// 取消倒计时
static _cancelTimer() async {
_timer?.cancel();
_timer = null;
}
// 移除Toast
static _cancelToast() async {
_cancelTimer();
_overlayEntry?.remove();
_overlayEntry = null;
}
// 移除Toast,当 Toast 类型为 _ToastType.custom 和 _ToastType.loading
// 并且 style.duration == null 时 Toast 不会自动消失,
// 需要暴露一个方法给用户主动调用
static cancel() async {
_cancelToast();
}
}
_containerWidget
Widget _containerWidget(
BuildContext context,
{
String message,
Widget customChild,
_ToastType type,
ToastStyle style,
}
) {
MainAxisAlignment mainAxisAlignment;
if (style.position == ToastPosition.top) {
mainAxisAlignment = MainAxisAlignment.start;
} else if (style.position == ToastPosition.center) {
mainAxisAlignment = MainAxisAlignment.center;
} else {
mainAxisAlignment = MainAxisAlignment.end;
}
return Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: mainAxisAlignment,//设置 Toast 显示的位置
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: Padding(
padding: style.margin,
child: Card(
// 设置Toast背景色
color: style.color != null ? style.color : isDarkMode() ? const Color(0xFFE0E0E0) : Colors.black87,
child: Padding(
padding: style.padding,
// 当 Toast 的类型是 _ToastType.custom 时,使用加载自定义 Widget,否则加载内置的各类型 Widget
child: type == _ToastType.custom ? customChild : _typeWidget(type, message, style),
),
),
),
),
],
);
}
上面设置Toast背景色时,我们使用了判断暗黑模式的代码,逻辑是:
- 用户设置了自定义背景色时,使用用户自定义的颜色作为背景色;
- 用户没有设置自定义背景色时,如果当前是暗黑模式,使用颜色 0xFFE0E0E0,否则使用颜色 Colors.black87;
另外,当Toast 的类型是 _ToastType.custom 时,加载用户自定义的 Widget 做Toast的内容Widget,否则加载内置的各类型 Widget。
_typeWidget
// 显示内容的Widget
Widget _typeWidget(_ToastType type, String message, ToastStyle style) {
if (type == _ToastType.custom) {
return null;
}
if (type == _ToastType.text) {
return Text(
message,
style: TextStyle(
fontSize: style.textSize,
// 文本颜色
color: style.textColor != null ? style.textColor : isDarkMode() ? const Color(0xFF2F2F2F) : Colors.white,
),
);
}
final List _widgets = [];
if (type == _ToastType.loading) {
_widgets.add(CupertinoActivityIndicator(radius: 24,),);
} else if (type == _ToastType.success) {
_widgets.add(Icon(
Icons.check_circle_outline,
size: 36,
color: Colors.green,
));
} else if (type == _ToastType.error) {
_widgets.add(Icon(
Icons.highlight_off,
size: 36,
color: Colors.red,
));
} else if (type == _ToastType.info) {
_widgets.add(Icon(
Icons.info_outline,
size: 36,
color: Colors.blue,
));
}
// 文本与 Icon 的距离
double _textTop = 8;
if (message.isEmpty) {
_textTop = 0;
}
_widgets.add(
Flexible(child: Padding(
padding: EdgeInsets.only(top: _textTop),
child: Text(message,
style: TextStyle(
fontSize: style.textSize,
// 文本颜色
color: style.textColor != null ? style.textColor : isDarkMode() ? const Color(0xFF2F2F2F) : Colors.white,
),
),
))
);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: _widgets,
);
}
上面代码设置文本颜色时,也判断了暗黑模式:
- 用户设置了自定义文本颜色时,使用用户自定义的颜色作为背景色;
- 用户没有设置自定义文本颜色时,如果当前是暗黑模式,使用颜色 0xFF2F2F2F,否则使用颜色Colors.white;
使用
// _ToastType.text
Toast.showText(context, "这是一个文本Toast(bottom)", style: ToastStyle(
position: ToastPosition.bottom,
));
Toast.showText(context, "这是一个文本Toast(center)", style: ToastStyle(
position: ToastPosition.center,
));
Toast.showText(context, "这是一个文本Toast(top)", style: ToastStyle(
position: ToastPosition.top,
));
// _ToastType.loading
Toast.loading(context, message: "这是一个加载中Toast");
Future.delayed(Duration(seconds: 3), () {
Toast.cancel();
});
// _ToastType.success
Toast.success(context, "这是一个成功Toast");
// _ToastType.error
Toast.error(context, "这是一个失败Toast");
// _ToastType.info
Toast.info(context, "这是一个消息Toast");
// _ToastType.custom
Toast.custom(
context,
customToastContent(
Icon(
Icons.info_outline,
color: Colors.blue,
size: 25,
),
"这是一个自定义Toast"
),
);
Future.delayed(Duration(seconds: 3), () {
Toast.cancel();
});
感觉文字表达的很是苍白,还是上一份完整的代码吧
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class ToastStyle extends Diagnosticable {
const ToastStyle({
this.position = ToastPosition.bottom,
this.textSize = 14.0,
this.margin = const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
this.padding = const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
this.color,
this.textColor,
this.duration,
});
final Color color; // 背景颜色
final Color textColor; // 文本颜色
final double textSize; // 文字大小
final EdgeInsetsGeometry padding;// 内边距
final EdgeInsetsGeometry margin; // 外边距
final ToastPosition position; // 显示位置
final Duration duration; // 显示时间
}
enum ToastPosition { top, center, bottom, }
enum _ToastType { text, loading, success, error, info, custom }
class Toast {
static OverlayEntry _overlayEntry; // UI浮层
static Timer _timer; // 计时,如果计时大于 _seconds,则移除Toast
// 加载中
static void loading(BuildContext context, {String message}) => Toast.showLoading(context, message: message);
static void showLoading(BuildContext context, {String message, ToastStyle style}) =>
_showToast(
context,
message: message,
type: _ToastType.loading,
style: style == null ? ToastStyle(position: ToastPosition.center) : style
);
// 自定义
static void custom(BuildContext context, Widget customChild) => Toast.showCustom(context, customChild);
static void showCustom(BuildContext context, Widget customChild, {ToastStyle style}) =>
_showToast(
context,
customChild: customChild,
type: _ToastType.custom,
style: style == null ? ToastStyle() : style
);
// 文本
static void text(BuildContext context, String message) => Toast.showText(context, message);
static void showText(BuildContext context, String message, {ToastStyle style}) =>
_showToast(
context,
message: message,
type: _ToastType.text,
style: style == null ? ToastStyle() : style
);
// 成功
static void success(BuildContext context, String message) => Toast.showSuccess(context, message);
static void showSuccess(BuildContext context, String message, {ToastStyle style}) =>
_showToast(
context,
message: message,
type: _ToastType.success,
style: style == null ? ToastStyle(position: ToastPosition.center) : style,
);
// 失败
static void error(BuildContext context, String message) => Toast.showError(context, message);
static void showError(BuildContext context, String message, {ToastStyle style}) =>
_showToast(
context,
message: message,
type: _ToastType.error,
style: style == null ? ToastStyle(position: ToastPosition.center) : style,
);
// 消息
static void info(BuildContext context, String message) => Toast.showInfo(context, message);
static void showInfo(BuildContext context, String message, {ToastStyle style}) =>
_showToast(
context,
message: message,
type: _ToastType.info,
style: style == null ? ToastStyle(position: ToastPosition.center) : style,
);
// 显示
static void _showToast(
BuildContext context,
{
String message,
Widget customChild,
_ToastType type,
ToastStyle style
}
) async {
// 显示之前先把之前的浮层清空
_cancelToast();
_overlayEntry = OverlayEntry(
builder: (BuildContext context) => SafeArea(
// 当类型为 _ToastType.loading 时,
// 我希望页面布局的事件不被执行,在 _containerWidget 外面包裹一层 Material 并且设置颜色,
// 其它类型直接加载 _containerWidget
child: type == _ToastType.loading ? Material(
// 在 _containerWidget 外面包裹一层 Material 并且设置颜色之后,事件就不能穿透了
color: Colors.transparent,
child: _containerWidget(context,
message: message,
customChild: customChild,
type: type,
style: style),
) : _containerWidget(
context,
message: message,
customChild: customChild,
type: type,
style: style),
),
);
// 当 Toast 类型不为 _ToastType.custom 和 _ToastType.loading,
// 并且 style.duration == null 时,
// 我们给它一个默认值 Duration(seconds: 3)
_startTimer((type != _ToastType.custom
&& style.duration == null
&& type != _ToastType.loading) ? Duration(seconds: 3) : style.duration);
Overlay.of(context).insert(_overlayEntry);
}
// 开启倒计时
static void _startTimer(Duration duration) {
// 当 duration == null 时,Toast 长存不自动消失
if (duration == null) return;
_timer = Timer(duration, (){
_cancelToast();
});
}
// 取消倒计时
static _cancelTimer() async {
_timer?.cancel();
_timer = null;
}
// 移除Toast
static _cancelToast() async {
_cancelTimer();
_overlayEntry?.remove();
_overlayEntry = null;
}
// 移除Toast,当 Toast 类型为 _ToastType.custom 和 _ToastType.loading
// 并且 style.duration == null 时 Toast 不会自动消失,
// 需要暴露一个方法给用户主动调用
static cancel() async {
_cancelToast();
}
}
// Toast内容容器
Widget _containerWidget(
BuildContext context,
{
String message,
Widget customChild,
_ToastType type,
ToastStyle style,
}
) {
MainAxisAlignment mainAxisAlignment;
if (style.position == ToastPosition.top) {
mainAxisAlignment = MainAxisAlignment.start;
} else if (style.position == ToastPosition.center) {
mainAxisAlignment = MainAxisAlignment.center;
} else {
mainAxisAlignment = MainAxisAlignment.end;
}
return Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: mainAxisAlignment,// 设置 Toast 显示的位置
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: Padding(
padding: style.margin,
child: Card(
// 设置Toast背景色
color: style.color != null ? style.color : isDarkMode() ? const Color(0xFFE0E0E0) : Colors.black87,
child: Padding(
padding: style.padding,
// 当 Toast 的类型是 _ToastType.custom 时,加载自定义 Widget,否则加载内置的各类型 Widget
child: type == _ToastType.custom ? customChild : _typeWidget(type, message, style),
),
),
),
),
],
);
}
// 显示内容的Widget
Widget _typeWidget(_ToastType type, String message, ToastStyle style) {
if (type == _ToastType.custom) {
return null;
}
if (type == _ToastType.text) {
return Text(
message,
style: TextStyle(
fontSize: style.textSize,
// 文本颜色
color: style.textColor != null ? style.textColor : isDarkMode() ? const Color(0xFF2F2F2F) : Colors.white,
),
);
}
final List _widgets = [];
if (type == _ToastType.loading) {
_widgets.add(CupertinoActivityIndicator(radius: 24,),);
} else if (type == _ToastType.success) {
_widgets.add(Icon(
Icons.check_circle_outline,
size: 36,
color: Colors.green,
));
} else if (type == _ToastType.error) {
_widgets.add(Icon(
Icons.highlight_off,
size: 36,
color: Colors.red,
));
} else if (type == _ToastType.info) {
_widgets.add(Icon(
Icons.info_outline,
size: 36,
color: Colors.blue,
));
}
// 文本与 Icon 的距离
double _textTop = 8;
if (message.isEmpty) {
_textTop = 0;
}
_widgets.add(
Flexible(child: Padding(
padding: EdgeInsets.only(top: _textTop),
child: Text(message,
style: TextStyle(
fontSize: style.textSize,
// 文本颜色
color: style.textColor != null ? style.textColor : isDarkMode() ? const Color(0xFF2F2F2F) : Colors.white,
),
),
))
);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: _widgets,
);
}
// 判断是否是暗黑模式
bool isDarkMode() {
bool isDarkMode;
final ThemeMode themeMode = ThemeMode.system;
if (themeMode == ThemeMode.light || themeMode == ThemeMode.dark) {
isDarkMode = themeMode == ThemeMode.dark;
} else {
isDarkMode =
WidgetsBinding.instance.window.platformBrightness == Brightness.dark;
}
return isDarkMode;
}