Android内有一个很好用的Toast提示,不抢焦点不妨碍操作,用来作一些短暂的提示是再好不过了。但再好的东西也有其弊端,就是这货只受系统消息控制,显示的长度只放出两个选择LENGTH_SHORT(2000ms),LENGTH_LONG(3500ms),对于有奇葩需求的产品来说这简直是不可理喻,于是我就简单地对Toast进行了一点分析。
需要分析toast的内在原理,先看一个最简单的toast使用方式:
1 | Toast.makeText(this,"Message",Toast.LENGTH_LONG).show(); |
其中,this为context大家懂的。一般我们调用完这句toast都能“马上”就显示出来,但当多次调用toast的show方法时就不是了,toast会一个接一个地显示出来,很容易想到这是放队列里实现的,而事实上,每调用一次show,其实都是把toast的对象放到了toast的队列中而已,当消息调度调出时toast才能真正显示出来。其显示与关闭都是受系统消息调度控制的,这就是我们无法自由控制其显隐的原因。
通过读源码可知,要获取一个Toast对象很简单,通过Toast的makeText方法就可以生成一个了,makeText方法如下:
1 |
|
makeText的作用是加载一个内置的布局com.android.internal.R.layout.transient_notification,并设置好文字,显示时长等 ,并将其封装到一个Toast对象中,于是容易想到,平时我们看到的Toast其实就是一个view,而Toast对象就是一个包含控制View显示信息的对象。
获取到Toast对象后,就可以调用show()方法将Toast对象放进队列了,源码中是这么定义的:
1 | public void show() { |
先是使用getService()方法获取INotificationManager对象,其内部实现是通过AIDL获取services的;TN是Toast类的一个内部类,在Toast的构造方法中会创建一个新的对象,其是ITransientNotification.Stub的一个子类,用于AIDL回调通信的,最后再将调用enqueueToast将其压入到系统的队列中。
AIDL通信的对象是com.android.server.NotificationManagerService类,这个类承接着显示消息提示的调度作用,调度Toast是其一个功能,调用enqueueToast后,会生成一个ToastRecord对象,其包含调用的pid,回调对象,时长及包名,然后放入mToastQueue队列中,这里android使用了synchronized来保证消息同步。
接着系统会自动调用showNextToastLocked()方法去获取位于toast队列最top的一个ToastRecord对象,成功获取后的第一件事就是去使用ITransientNotification对象回调(这里就是Toast里的TN对象)。TN的show通过handler调用handleShow方法,其做的就是把view添加到系统默认的WindowManager上;第二件事就是设置一个定时,决定何时去关闭toast,这里系统用了一个比较抵死的方法:
1 | private void scheduleTimeoutLocked(ToastRecord r, boolean immediate) { |
这就是API有提供设置显示时长的方法但是设了也没用的原因……
隐藏的方法也比较简单,不再赘述。
整个Toast显示的流程可以表示为:
知道了原理就可以动手修改了,因为有很多方法都是private的,又到了反射的时候了 =_= 从以上过程可以知道,Toast其实并不包含显示的实际动作,都在其内部类TN中,于是我们重点反射其TN类,代码如下:
1 | try { |
好吧,如果已经成功了就不用再看了,不成功不想看了上面那些算我扯蛋好了……现实是上面说的那些只能在3.0以下能用,3.0以上不work……
又得重新分析,看回TN的HandlerShow方法,当mView与mNextView不相等时才会执行addView操作,mView是正在显示toast的view,而mNextView是下一个将要显示的view,修改Toast的源码把LOG的开关打开后重新编绎刷进手机,然后发现mView与mNextView都是null。大概这就是原因了,对比两个版本的Toast源码,发现4.0.4版本的内部类TN里跟Toast用的并不是同一个mView与mNextView(事实上3.0以下的两个定义都是定义在Toast的成员变量里,3.0以上是Toast与TN各有一个mNextView,而mView只有TN有,无伤大雅知道就好)。于是考虑从此处入手。
从handlerShow的方法可以看到mNextView会先赋值给mView,然后再添加到WindowManager中,但现在就是mNextView也为空那就只能没辙……吗?又看回Toast,发现了一个方法
1 | public View getView() { |
这是从Toast对象中获取Toast的View的方法,于是便想系统没有按我所想的把mNextView设置上,那我可以主动地把我想要显示的View设置给TN对象然后再显示出来吗?答案当然是可以的,要不我说那么多干嘛 =_=| 完整的代码如下:
1 | try { |
写完了