深入分享一下android.widget.Toast

匿名 (未验证) 提交于 2019-12-03 00:26:01

Toast.LENGTH_SHORTToast.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()方法,而showhide将分别调用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流程分析了一遍。。。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!