Toast.LENGTH_SHORT
和Toast.LENGTH_LONG
两种,前者是2s,后者是显示3.5s。嗯,有些页面的确显示有问题,short太短,long太短,搞得比较尴尬,所以需要分析一下,这Toast到底是怎么运行的,我能不能修改这个时间,让它要多长有多长呢? 我希望你读完这篇文章之后,能了解深层次的Toast运行原理。
Toast.makeText(activity, "hello world", Toast.LENGTH_SHORT).show(); //or Toast.makeText(activity, "hello world", Toast.LENGTH_LONG).show();
通过源码分析,makeText()方法中,主要是获取LayoutInflater获取一个TextView,这个TextView就是我们的Toast需要显示的View,这个很简单,就不说了,主要来看一下show()方法:
/** * 显示特定时长的View */ public void show() { //代码省略 INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }
通过上面可以看出,首先Toast获取了一个INotificationManager,因为后面叫service,看上去是一个服务,那么我们看一下getService()方法:
static private INotificationManager getService() { if (sService != null) { return sService; } sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; }
一看INotificationManager.Stub.asInterface,就知道使用了aidl
方法,这可有点扯淡了,一个简单的Toast,也要用上aidl
,我不太熟悉,怎么办?既然不熟悉,那么我们先放一步说话吧,先看看
service.enqueueToast(pkg, tn, mDuration);
方法,但是这个方法也是标红的:

以前,每次看到这里,都有些失望,都不知道接下来该怎么分析源码,因为这些源码都在framework层,也不好找,怎么办呢?别急,现在已经有办法了,现在有这么一个网站,专门介绍Android#framwork层的,叫http://androidxref.com/,大家可以有事没事上去看看。说到这里,我脑袋发热,直接搜了一下enqueueToast
方法,不搜不知道,一搜吓一跳:

我们居然搜到了INotificationManager.aidl
,Toast
还有NotificationManagerService.java
三个文件,想都不用想,真正的实现类一定是NotificationManagerService.java
,我们进去看看:

我稍微把代码整理一下,当然我建议你先不看这一大段代码,说实话没什么卵用,边看解释边回来看效果更好:
@Override public void enqueueToast(String pkg, ITransientNotification callback, int duration) { //无关紧要的代码ignore final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg)); final boolean isPackageSuspended = isPackageSuspendedForUser(pkg, Binder.getCallingUid()); synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); try { ToastRecord record; //获取 int index = indexOfToastLocked(pkg, callback); if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } Binder token = new Binder(); mWindowManagerInternal.addWindowToken(token,WindowManager.LayoutParams.TYPE_TOAST); record = new ToastRecord(callingPid, pkg, callback, duration, token); mToastQueue.add(record); index = mToastQueue.size() - 1; } if (index == 0) { showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); } }
代码还是很长,不过我们慢慢分析,我们先看一下enqueueToast(String pkg, ITransientNotification callback, int duration)
这个方法三个参数分别是:pkg,ITransientNotification接口的callback和duration,其实我现在还是最关心的就是这个duration,因为这个duration传入的就是决定显示的时间,见Toast.show()方法:

不过,更重要的是需要知道这个TN,即ITransientNotification这个callback,TN是Toast的一个静态内部类,继承了ITransientNotification.Stub (这是是aidl方法产生的,此时也实现了ITransientNotification接口,这篇不是扯aidl的,所以我就当大家都明白), 大家可以先不管这个TN到底是个什么玩意,先不扯,等用到了咋们在慢慢回来看。
对于上面的源码,我们就从最简单的方式分析吧,那么我们就直接分析这个方法:
if (index == 0) { showNextToastLocked(); }
我们就认为现在mToastQueue
就一个ToastRecord
,在分析这个方法之前,我们先看一下ToastRecord
是怎么生成的:
record = new ToastRecord(callingPid, pkg, callback, duration, token);
可见,此时的callback和duration是我们在传入enqueueToast
方法的参数,此时记住就行,等会要用到,现在我们需要分析showNextToastLocked()
方法了:
方法不长,那我就直接截图了:

此时很明朗,其实是直接调用了record.callback.show(record.token)
通过刚才的分析,这个callback就是我们传入ITransientNotification
,当然实现者大家肯定都知道,就是Toast内部静态类TN,那么是时候看看TN的show()方法了:
@Override public void show(IBinder windowToken) { mHandler.obtainMessage(0, windowToken).sendToTarget(); }
这个一看就明白,直接mHandler看源码了:

这个简单,直接看handleShow(token)方法了:
@Override public void handleShow(IBinder windowToken) { if (mView != mNextView) { WindowManager mWn = ... ; //代码省 mWM.addView(mView, mParams); } }
哦,终于知道Toast也是通过WindowManager.addView
添加上去的,这个就叼了,最起码我们分析了一遍Toast的生成过程,还是比较曲折的。 好了,既然挂上去了,那怎么消失掉呢?消失的源码该怎么分析呢?别急,咋们忘回看:showNextToastLocked()
中还有一个scheduleTimeoutLocked(record)
方法,我们进去看看:

很简单,如果使用的事Toast.LENGTH_LONG,就延迟3.5秒,否则就延迟2秒,那么我们去看一下mHandler.MESSAGE_TIMEOUT:

那我们就去看一下handleTimeOut
方法:

这次执行的方法是cancelToastLocked(index)
方法,那么此时这个方法在做什么呢?
我们来看一下代码:
void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); //调用record.callback //即是刚刚认识的TN对象 try { record.callback.hide(); } catch (RemoteException ignore) { //... } ToastRecord lastToast = mToastQueue.remove(index); mWindowManagerInternal.removeWindowToken(lastToast.token, true); keepProcessAliveIfNeededLocked(record.pid); if (mToastQueue.size() > 0) { showNextToastLocked(); } }
同样道理,我们看到了record.callback.hide()
方法,此时我们知道callback还是Toast中的TN对象,那么它的hide
方法为:
@Override public void hide() { mHandler.post(mHide); }
对于mHander.post(mHide)
,其中的mHide
为一个Runnable对象:
final Runnable mHide = new Runnable() { @Override public void run() { handleHide(); mNextView = null; } };
直接去看handleHide()
方法:
public void handleHide() { if (mView != null) { if (mView.getParent() != null) { //最终还是WindowManager移除了该View mWM.removeViewImmediate(mView); } mView = null; } }
最终还是熟悉的WindowManager移除了mView,那么此时我们的Toast就会消失在屏幕之上了。
当然了,事情还没有结束,在cancelToastLocked(index)
方法中还有一个方法值得我们看一下:
if (mToastQueue.size() > 0) { showNextToastLocked(); }
如果ToastQueue不为空,那么将继续循环将ToastQueue每一个ToastRecord执行show()和hide()方法,直到所有Toast都显示掉。当然,如果你不适用特殊的手段,按照Toast的执行意向,你将不会同时看到两个Toast在一个屏幕上,因为前一个Toast没有show()完成,不会去调用后面ToastRecord的方法的。
上个图,把整个过程描述一下,如果有错误,请及时提出:

Toast.show()
其实远程调用了NotificationManageService的enqueueToast
方法,在该方法中,存在一个Handler遍历ToastQueue,ToastQueue中每一个ToastRecord将会调用callback.show()
和callback.hide()
方法,此方法最终将调用Toast的内部类TN对象的show()
和hide()
方法,而show
和hide
将分别调用WindowManager.addView()
和windowManager.removeView()
方法,直至将Toast显示或者移除在屏幕上。
回到开头,到了这里,我们是否可以自己控制Toast的显示时长呢?如果还是要NotificationManagerService参与,那就没啥希望了,因为它内部的Handler.postDelay()
只有两种选择,要么是2s,要么是3.5s。那么我们不用NotificationManagerService
这这尊大佛,直接调用Toast.TN.show()
和Toast.TN.Hide()
是否可行呢?我的想法也是这样的,也实现过了,代码如下,很简单,只使用了简单的反射:
public class AllTimeShowToast { private Toast mToast ; private Object TN ; private Method show ; private Method hide ; private TextView mLongTextView ; public AllTimeShowToast(Context context) { mToast = new Toast(context ); initTextView(context); initTN(); } private void initTextView(Context context) { mLongTextView = new TextView(context); mLongTextView.setText("all time show "); } private void initTN() { try { Field tnObj = mToast.getClass().getDeclaredField("mTN"); tnObj.setAccessible(true); TN = tnObj.get(mToast); show = TN.getClass().getMethod("show"); hide = TN.getClass().getMethod("hide"); Field tnParamsField = TN.getClass().getDeclaredField("mParams"); tnParamsField.setAccessible(true); WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(TN); params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; Field nextViewField = TN.getClass().getDeclaredField("mNextView"); nextViewField.setAccessible(true); nextViewField.set(TN, mLongTextView); }catch (Exception e) { e.printStackTrace(); } } public void show() { try { show.invoke(TN); } catch (Exception e) { e.printStackTrace(); } } public void hide() { try { hide.invoke(TN) ; }catch (Exception e) { e.printStackTrace(); } } }
调用方式为:
var showToast = AllTimeShowToast(this); //show方法展示 showToast!!.show() //hide方法展示 showToast!!.hide()
结果如下:

当然了,这里Toast也只是一次性的,show()
一次,hide()
一次之后,就相当于废了。如果要想重新show
,那么需要重新new Object
了。
好了,这篇文章好长,也差不多写完了,基本上把Toast流程分析了一遍。。。