简介
在平时开发中,为了提高用户体验,我们经常会使用Toast来提示一些重要信息,例如“输入正确的用户名”、“密码不能为空”等,但大家都遇到过如下问题:多次点击按钮,界面会匀速弹出Toast,即使切换到其他页面也会弹出,用户体验很不好,这也是本人最近根据测试反馈在项目中优化Toast显示的一点心得记录,下面也针对这种问题,给出解决方案。
分析
根据问题很自然的会想到我们在显示toast之前判断是否已经显示了,没有显示的时候才可以显示。关键点:找到toast的显示状态是什么?
通过源码分析会发现有如下一段
TN(String packageName, @Nullable Looper looper) {
....
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
在手动取消或者隐藏的时候,或者显示时间结束的时候,系统会通过handler发送消息到TN的mHandler中。我们继续分析handleHide()方法
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}
// Now that we've removed the view it's safe for the server to release
// the resources.
try {
getService().finishToken(mPackageName, this);
} catch (RemoteException e) {
}
mView = null;
}
}
这个方法会将mView视图释放掉,我们的突破口就是这里。在显示之前判断toast的mView是否为null。
解决方法
直接看代码吧
private var toast:Toast?=null
private fun doToast(message: String? = "", duration: Int = Toast.LENGTH_SHORT, context: Context? = BaseApplication.app) {
if (toast!=null) {
if (getTnView(toast!!)!=null) {//正在显示
return
}else toast=null
}
context?.let {
toast = Toast.makeText(context, "", duration).apply {
hook(this)
setText(message)
setGravity(Gravity.CENTER, 0, 0)
show()
}
}
}
private var sField_TN: Field? = null
private var sField_TN_Handler: Field? = null
init {
try {
sField_TN = Toast::class.java.getDeclaredField("mTN")
sField_TN?.isAccessible = true
sField_TN_Handler = sField_TN?.type?.getDeclaredField("mHandler")
sField_TN_Handler?.isAccessible = true
} catch (e: Exception) {
}
}
private fun getTnView(toast: Toast):View?{
val viewField = sField_TN?.type?.getDeclaredField("mView").also { it?.isAccessible=true }
val tn = sField_TN?.get(toast)
return viewField?.get(tn) as? View
}
private fun hook(toast: Toast) {
try {
val tn = sField_TN?.get(toast)
val preHandler = sField_TN_Handler?.get(tn) as Handler
sField_TN_Handler?.set(tn, SafelyHandlerWrapper(preHandler))
} catch (e: Exception) {
}
}
private class SafelyHandlerWrapper(private val impl: Handler) : Handler() {
override fun dispatchMessage(msg: Message) {
try {
super.dispatchMessage(msg)
} catch (e: Exception) {
}
}
override fun handleMessage(msg: Message) {
impl.handleMessage(msg)//需要委托给原Handler执行
}
}
getTnView方法通过反射获取当前toast的mView是否是null,不是null表示当前toast正在显示,否则当前toast隐藏或者取消,就可以显示新的toast。