背景
Toast是Android平台较常用的基础提示控件,使用简单易用;但是,Toast是系统层面提供的,不依赖于前台页面,存在滥用的风险。为了规避这些风险,Google在Android系统版本的迭代过程中,不断进行了优化和限制。这些限制不可避免的影响到了正常的业务逻辑,在迭代过程中,我们遇到过以下几个问题:
- 设置中关闭某个App的【显示通知】开关,Toast不再弹出,极大的影响了用户体验。
- Toast在Android 7.1.2(API25)以下会发生
BadTokenException
异常,导致App崩溃。 - 自定义
TYPE_TOAST
类型的Window,在Android 7.1.1、7.1.2发生token null is not valid
异常,导致App崩溃。
当然了,上面这些问题,多少有一些替代方案,比如以下这些方式:
经过对比和参考美团实践方案,最终采用Snackbar对Toast进行替换。
使用Snackbar存在的一些问题
- Snackbar弹出的时候,被Dialog,PopupWindow等控件遮住。
- Snackbar无法进行跨页面展示,这是Snackbar实现原理决定的。
- Snackbar无法自定义布局、动画等
解决方案
问题一:
针对Snackbar弹出的时候,被Dialog,PopupWindow等控件遮住的问题,原因在于Snackbar依赖于View,当把Activity布局的View传给Snackbar做为Snackbar展示依赖的父View时,后面再弹Dialog,PopupWindow等控件,Snackbar就会被控件遮挡。正确的做法是直接把PopupWindow和Dialog所依赖的View传给Snackbar。那么我们定制化的Snackbar不仅支持传递这个View,也支持直接传递PopupWindow和Dialog的实例
问题二:
跨页面存在两种情况:
- Snackbar#show → startActivity
- Snackbar#show → finish
这两种情况都是在弹出Snackbar之后所依赖的Activity不可见或者关闭导致无法正常显示。所以将消息缓存起来,后置到下一个可见Activity进行处理,通过 application.registerActivityLifecycleCallbacks 进行页面onStart监听实现
问题三:
系统的Snackbar不支持自定义扩展,所以参考Snackbar的源码,进行了按需定制。
如何使用
#正常单页面使用
SnackbarUtils#showToast
#针对自定义View容器(会相对于View容器大小居中对齐,最好使用FrameLayout)
SnackbarUtils#showToastForView
#针对跨页使用
SnackbarUtils#showToastForJump
#针对Dialog使用
SnackbarUtils#showToastForDialog
简而言之:请在合适的时候、合适的场景、使用合适的API
当前版本兼容
- toast()#showToast ==>底层替换成Snackbar的显示
- 对于传入context是Application的调用,还是使用Toast进行展示
- ToastUtils#showToast的使用全部替换成SnackbarUtils#showToast
- 对于跨页面的Toast替换成SnackbarUtils#showToastForJump
注意:如继续使用ToastUtils中相关API,还是会老的Toast进行展示
不支持的情况(请使用Toast)
- 子线程异步执行→跳页面→子线程执行完毕弹Toast (有此种情况也是写的有问题)
- 拿不到Activity的Context
- PopupWindow不支持使用(暂未发现使用场景)
- 获取的父容器是getContentView(),在低版本中如不是FrameLayout,会无法找到合适添加Snackbar的容器,从而导致NPE
- 如contentView是FrameLayout,Snackbar的显示极度受contentView的大小限制