背景
在 Android 11 上谷歌对 Toast 进行了变更,导致 Toast#getView() 返回 null,具体可以查看文档 Android 11 中消息框的更新,对此,1.30.0 版本日志,其中就包括重构 ToastUtils,其重构后的 APIs 如下所示:
吐司相关 -> [ToastUtils.java][toast.java] -> [Demo][toast.demo]
make : 制作吐司
make.setMode : 设置模式
make.setGravity : 设置位置
make.setBgColor : 设置背景颜色
make.setBgResource : 设置背景资源
make.setTextColor : 设置字体颜色
make.setTextSize : 设置字体大小
make.setDurationIsLong : 设置是否长时间显示
make.setLeftIcon : 设置左侧图标
make.setTopIcon : 设置顶部图标
make.setRightIcon : 设置右侧图标
make.setBottomIcon : 设置底部图标
make.setNotUseSystemToast: 设置不使用系统吐司
make.show : 显示吐司
getDefaultMaker : 获取默认制作实例(控制 showShort、showLong 样式)1.30.1 新增
showShort : 显示短时吐司
showLong : 显示长时吐司
cancel : 取消吐司显示
使用
作为工具类,基本是看完 APIs 就知道该如何使用了,我这里就做下简单的介绍,想玩 Demo 的话可以在 1.30.0 版本日志 中下载。
// 短时间显示 toast
ToastUtils.showShort(xxx)
// 长时间显示 toast
ToastUtils.showLong(xxx)
// 长时间显示带左侧 icon 的暗黑模式 toast
ToastUtils.make().setLeftIcon(icon).setDurationIsLong(true).setMode(ToastUtils.MODE.DARK).show(xxx)
// 自定义 toast
private fun show(text: CharSequence, isLong: Boolean) {
val textView = ViewUtils.layoutId2View(R.layout.toast_custom) as TextView
textView.text = text
ToastUtils.make().setDurationIsLong(isLong).show(textView)
}
源码分析
系统 Toast 展示是需要有通知权限的(某些修改了 ROM 手机除外),所以如果不进行处理的话,关闭了通知权限就会导致 Toast 不显示的 BUG,针对不同版本及权限我们有不同的处理方案,下面来看下工具类中的相关代码:
// 最终 show 都会走到这个函数来处理
private static void show(@Nullable final View view, final CharSequence text, final int duration, final ToastUtils utils) {
UtilsBridge.runOnUiThread(new Runnable() {
@Override
public void run() {
cancel();
iToast = newToast(utils);
if (view != null) {
iToast.setToastView(view);
} else {
iToast.setToastView(text);
}
iToast.show(duration);
}
});
}
private static IToast newToast(ToastUtils toastUtils) {
if (!toastUtils.isNotUseSystemToast) {
if (NotificationManagerCompat.from(Utils.getApp()).areNotificationsEnabled()) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return new SystemToast(toastUtils);
}
if (!UtilsBridge.isGrantedDrawOverlays()) {
return new SystemToast(toastUtils);
}
}
}
// not use system or notification disable
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return new WindowManagerToast(toastUtils, WindowManager.LayoutParams.TYPE_TOAST);
} else if (UtilsBridge.isGrantedDrawOverlays()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
new WindowManagerToast(toastUtils, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
} else {
new WindowManagerToast(toastUtils, WindowManager.LayoutParams.TYPE_PHONE);
}
}
return new ActivityToast(toastUtils);
}
可以看到我们有 SystemToast、WindowManagerToast 和 ActivityToast 三种类型的 Toast,它们都继承自 AbsToast,其相关源码如下所示:
static abstract class AbsToast implements IToast {
protected Toast mToast;
protected ToastUtils mToastUtils;
protected View mToastView;
AbsToast(ToastUtils toastUtils) {
mToast = new Toast(Utils.getApp());
mToastUtils = toastUtils;
if (mToastUtils.mGravity != -1 || mToastUtils.mXOffset != -1 || mToastUtils.mYOffset != -1) {
mToast.setGravity(mToastUtils.mGravity, mToastUtils.mXOffset, mToastUtils.mYOffset);
}
}
@Override
public void setToastView(View view) {
mToastView = view;
mToast.setView(mToastView);
}
@Override
public void setToastView(CharSequence text) {
// 当设置了 mode 或者 设置了 icon 则会使用 utils_toast_view 布局
View utilsToastView = mToastUtils.tryApplyUtilsToastView(text);
if (utilsToastView != null) {
setToastView(utilsToastView);
return;
}
// API30 getView() 为空,则使用 utils_toast_view 布局来兼容
mToastView = mToast.getView();
if (mToastView == null || mToastView.findViewById(android.R.id.message) == null) {
setToastView(UtilsBridge.layoutId2View(R.layout.utils_toast_view));
}
TextView messageTv = mToastView.findViewById(android.R.id.message);
messageTv.setText(text);
if (mToastUtils.mTextColor != COLOR_DEFAULT) {
messageTv.setTextColor(mToastUtils.mTextColor);
}
if (mToastUtils.mTextSize != -1) {
messageTv.setTextSize(mToastUtils.mTextSize);
}
setBg(messageTv);
}
protected void setBg(final TextView msgTv) {
if (mToastUtils.mBgResource != -1) {
mToastView.setBackgroundResource(mToastUtils.mBgResource);
msgTv.setBackgroundColor(Color.TRANSPARENT);
} else if (mToastUtils.mBgColor != COLOR_DEFAULT) {
Drawable toastBg = mToastView.getBackground();
Drawable msgBg = msgTv.getBackground();
if (toastBg != null && msgBg != null) {
toastBg.mutate().setColorFilter(new PorterDuffColorFilter(mToastUtils.mBgColor, PorterDuff.Mode.SRC_IN));
msgTv.setBackgroundColor(Color.TRANSPARENT);
} else if (toastBg != null) {
toastBg.mutate().setColorFilter(new PorterDuffColorFilter(mToastUtils.mBgColor, PorterDuff.Mode.SRC_IN));
} else if (msgBg != null) {
msgBg.mutate().setColorFilter(new PorterDuffColorFilter(mToastUtils.mBgColor, PorterDuff.Mode.SRC_IN));
} else {
mToastView.setBackgroundColor(mToastUtils.mBgColor);
}
}
}
@Override
@CallSuper
public void cancel() {
if (mToast != null) {
mToast.cancel();
}
mToast = null;
mToastView = null;
}
}
interface IToast {
void setToastView(View view);
void setToastView(CharSequence text);
void show(int duration);
void cancel();
}
这部分代码比较简单,很多都是做兼容处理,我就不做解释了,下面我们依次来看 SystemToast、WindowManagerToast 和 ActivityToast。
```java
static final class SystemToast extends AbsToast {
SystemToast(ToastUtils toastUtils) {
super(toastUtils);
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
try {
//noinspection JavaReflectionMemberAccess
Field mTNField = Toast.class.getDeclaredField("mTN");
mTNField.setAccessible(true);
Object mTN = mTNField.get(mToast);
Field mTNmHandlerField = mTNField.getType().getDeclaredField("mHandler");
mTNmHandlerField.setAccessible(true);
Handler tnHandler = (Handler) mTNmHandlerField.get(mTN);
mTNmHandlerField.set(mTN, new SafeHandler(tnHandler));
} catch (Exception ignored) {}
}
}
@Override
public void show(int duration) {
if (mToast == null) return;
mToast.setDuration(duration);
mToast.show();
}
static class SafeHandler extends Handler {
private Handler impl;
SafeHandler(Handler impl) {
this.impl = impl;
}
@Override
public void handleMessage(@NonNull Message msg) {
impl.handleMessage(msg);
}
@Override
public void dispatchMessage(@NonNull Message msg) {