前言

我们都清楚,Toast显示时长有两个选择,长显示是3.5秒,端显示是2秒。那如果想要做到长时间显示,该怎么做呢?有个历史遗留的app通过开一个线程,不断调用show方法进行实现,这些年也没出过问题,直到系统版本更新到了Android9.0。

实现方式大概如下:

mToast = new Toast(context);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.setView(layout);
...
mToast.show(); //在线程里不断调用show方法,达到长时间显示的目的

在Android9.0上,Toast闪现了一下就不见了,并没有如预期那样,长时间显示。为什么呢?

概述

这里我们先来大概了解下Toast的显示流程。

Toast使用

一般使用Toast的时候,比较简单的就是如下方式:

Toast.makeText(mContext, "hello world", duration).show();

这样就可以显示一个toast。还有一种是自定义view的:

mToast = new Toast(context);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.setView(layout);
mToast.show(); 

原理都一样,先new 一个Toast,然后设置显示时长,设置toast中要显示的view(text也是view),然后就可以show出来。

Toast原理

Toast实现

先看看Toast的实现:

//frameworks/base/core/java/android/widget/Toast.java
public Toast(@NonNull Context context, @Nullable Looper looper) {
 mContext = context;
 mTN = new TN(context.getPackageName(), looper);
 mTN.mY = context.getResources().getDimensionPixelSize(
  com.android.internal.R.dimen.toast_y_offset);
 mTN.mGravity = context.getResources().getInteger(
  com.android.internal.R.integer.config_toastDefaultGravity);
}

Toast的构造函数很简单,主要就是mTN这个成员,后续对Toast的操作都在这里进行。紧接着就是设置Toast显示时长和显示内容:

public void setView(View view) {
 mNextView = view;
}

public void setDuration(@Duration int duration) {
 mDuration = duration;
 mTN.mDuration = duration;
}

Toast显示

public void show() {
 if (mNextView == null) {
  throw new RuntimeException("setView must have been called");
 }

 INotificationManager service = getService(); //这里是一个通知服务
 String pkg = mContext.getOpPackageName();
 TN tn = mTN;
 tn.mNextView = mNextView;

 try {
  service.enqueueToast(pkg, tn, mDuration);
 } catch (RemoteException e) {
  // Empty
 }
}

show方法简单,最终是调用了通知服务的enqueueToast方法:

frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

public void enqueueToast(String pkg, ITransientNotification callback, int duration)
 {
  ...
  final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));

  ...
   synchronized (mToastQueue) {
   int callingPid = Binder.getCallingPid();
   long callingId = Binder.clearCallingIdentity();
   try {
    ToastRecord record;
    int index;
    // All packages aside from the android package can enqueue one toast at a time
    if (!isSystemToast) {
     index = indexOfToastPackageLocked(pkg);
    } else {
     index = indexOfToastLocked(pkg, callback);
    }

    // If the package already has a toast, we update its toast
    // in the queue, we don't move it to the end of the queue.
    if (index >= 0) {
     record = mToastQueue.get(index);
     record.update(duration);
     try {
      record.callback.hide();
     } catch (RemoteException e) {
     }
     record.update(callback);
    } else {
     Binder token = new Binder();
     mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
     record = new ToastRecord(callingPid, pkg, callback, duration, token);
     mToastQueue.add(record);
     index = mToastQueue.size() - 1;
    }
    keepProcessAliveIfNeededLocked(callingPid);
    // If it's at index 0, it's the current toast. It doesn't matter if it's
    // new or just been updated. Call back and tell it to show itself.
    // If the callback fails, this will remove it from the list, so don't
    // assume that it's valid after this.
    if (index == 0) {
     showNextToastLocked();
    }
   } finally {
    Binder.restoreCallingIdentity(callingId);
   }
  }
 }

Toast的管理是通过ToastRecord类型列表集中管理的,NotificationManagerService会将每一个Toast封装为ToastRecord对象,并添加到mToastQueue中,mToastQueue的类型是ArrayList。在enqueueToast中,首先会判断应用是否为系统应用,如果是系统应用,则通过indexOfToastLocked来寻找是否有满足条件的Toast存在:

int indexOfToastLocked(String pkg, ITransientNotification callback)
{
 IBinder cbak = callback.asBinder();
 ArrayList<ToastRecord> list = mToastQueue;
 int len = list.size();
 for (int i=0; i<len; i  ) {
  ToastRecord r = list.get(i);
  if (r.pkg.equals(pkg) && r.callback.asBinder().equals(cbak)) {
   return i;
  }
 }
 return -1;
}

判断的依据是包名和callback,这里的callback其实就是上文说到的TN类,这是一个Binder类型,继承自ITransientNotification.Stub。如果条件符合,则返回对应索引,否则返回-1。首次show Toast的时候,肯定返回-1,则此时会new一个ToastRecord对象,并且加入到mToastQueue中,此时的index则为0:

record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;

那么就会走到如下分支了:

if (index == 0) {
 showNextToastLocked(); //显示Toast
}

void showNextToastLocked() {
 ToastRecord record = mToastQueue.get(0);
 while (record != null) {
  if (DBG) Slog.d(TAG, "Show pkg="   record.pkg   " callback="   record.callback);
  try {
   record.callback.show(record.token); //调用TN类的show方法
   scheduleDurationReachedLocked(record); //时间到就隐藏Toast
   return;
  } catch (RemoteException e) {
   ...
  }
 }
}

该方法也简单,就是回调TN类的show方法,上文提过,TN类对外提供show,hide, cancel等方法,在这些方法中,再通过内部handler进行处理:

//frameworks/base/core/java/android/widget/Toast.java
public void show(IBinder windowToken) {
  if (localLOGV) Log.v(TAG, "SHOW: "   this);
  mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

//贴出部分handleMessage方法
case SHOW: {
 IBinder token = (IBinder) msg.obj;
 handleShow(token);
 break;
}

public void handleShow(IBinder windowToken) {

 ...
 if (mView != mNextView) {
  // remove the old view if necessary
  handleHide();
  mView = mNextView;
  ...
  mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
  ...
  try {
   mWM.addView(mView, mParams); //交给WMS进行下一步的操作,最终显示出我们的view
   trySendAccessibilityEvent();
  } catch (WindowManager.BadTokenException e) {
   /* ignore */
  }
 }

}

调用show方法,最终会调用到handleshow方法,在该方法中使用WMS服务将view显示出来。

Toast隐藏

显示说完了,什么时候隐藏消失?在scheduleDurationReachedLocked方法中:

//frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
private void scheduleDurationReachedLocked(ToastRecord r)
{
  mHandler.removeCallbacksAndMessages(r);
  Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
  long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
  mHandler.sendMessageDelayed(m, delay);
}

这里也是使用了一个handler来进行处理,delay的时长取决于我们之前设置的Toast显示时长。长时间为3.5秒,短时间为2秒。

MESSAGE_DURATION_REACHED消息处理如下:

case MESSAGE_DURATION_REACHED:
  handleDurationReached((ToastRecord)msg.obj);
  break;

private void handleDurationReached(ToastRecord record)
{
  if (DBG) Slog.d(TAG, "Timeout pkg="   record.pkg   " callback="   record.callback);
  synchronized (mToastQueue) {
    int index = indexOfToastLocked(record.pkg, record.callback);
    if (index >= 0) {
      cancelToastLocked(index);
    }
  }
}

void cancelToastLocked(int index) {

  ToastRecord record = mToastQueue.get(index);
  try {
    record.callback.hide(); //隐藏掉该Toast
  } catch (RemoteException e) {
    ...
  }

  ToastRecord lastToast = mToastQueue.remove(index); //已经显示完毕的Toast,从列表中移除掉
  ...
  if (mToastQueue.size() > 0) { //如果还有待显示Toast
    // Show the next one. If the callback fails, this will remove
    // it from the list, so don't assume that the list hasn't changed
    // after this point.
    showNextToastLocked();
  }
}

该方法调用TN的hide方法隐藏掉Toast,然后再将Toast从列表中移除。看看隐藏的过程:

case HIDE: {
  handleHide();
  // Don't do this in handleHide() because it is also invoked by
  // handleShow()
  mNextView = null; //这里会把view清掉
  break;
}

public void handleHide() {
    if (localLOGV) Log.v(TAG, "HANDLE HIDE: "   this   " mView="   mView);
    if (mView != null) {
      ...
      mWM.removeViewImmediate(mView);
      ...
      mView = null;
    }
}

隐藏的过程,其实也简单,将view从窗口中移除,然后将mNextView和mView置Null。

到此Toast的显示和隐藏已经讲完。下面说说多次show为什么会导致Toast消失。

Toast的消失

想象一个场景,如果一个全局Toast(此次出问题的app中就是一个全局Toast),我们不断的去调用Toast的show方法,那么就意味着上文说的mToastQueue列表不为空,存在Toast,就会走到如下分支:

if (!isSystemToast) {
    index = indexOfToastPackageLocked(pkg);
  } else {
    index = indexOfToastLocked(pkg, callback);
  }

  // If the package already has a toast, we update its toast
  // in the queue, we don't move it to the end of the queue.
  if (index >= 0) {
    record = mToastQueue.get(index);
    record.update(duration);
    try {
      record.callback.hide(); //如果存在已经显示的Toast,这里会先进行hide
    } catch (RemoteException e) {
    }
    record.update(callback);
  }
}

hide的流程我们已经清楚,会将资源释放,将mNextView和mView置为Null。执行到这里会导致第一个Toast消失,之后调用showNextToastLocked()方法显示第二个Toast,最终调用到TN的handleShow方法:

public void handleShow(IBinder windowToken) {
  // ...
  if (mView != mNextView) {
    // ...
    mView = mNextView;
    // ...
    mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    // ...
    mWM.addView(mView, mParams);
    // ...
  }
}

由于所有的Toast都对应一个TN对象,因此此时mView和mNextView均为null,不会执行mWM.addView(),Toast也就不会显示。

解决方法

在Android9.0中如果想要一直显示某个Toast,怎么做?使用局部Toast,不要使用全局Toast。

但有一点比较奇怪的是,查看了Android10.0代码,发现Android10.0将这个机制回滚了。即Android10.0上又可以一直显示Toast:

//这里就不执行hide的操作了
if (index >= 0) {
  record = mToastQueue.get(index);
  record.update(duration);
}

结语

Android多个系统版本中,唯独Android9.0做了这个特殊处理,无非就是禁用应用长时间显示Toast。但10.0版本又取消了这个处理,难道是发现这样处理并不合适?

到此这篇关于Android9.0上针对Toast的特殊处理的文章就介绍到这了,更多相关Android9.0对Toast的特殊处理内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

Android9.0上针对Toast的特殊处理图文详解的更多相关文章

  1. android – 如何在特定号码的应用程序中共享文本

    使用此代码只打开特殊号码的聊天但文本不是共享.我该怎么做?解决方法由于您尝试将其实现为“smsto:”,因此“text/plain”类型将帮助您.如果没有帮助,请尝试额外的“sms_body”.

  2. android – JobIntentService被破坏,当app被销毁时

    从Android开发人员JobIntentService开始在AndroidO或更高版本上运行时,工作将通过JobScheduler.enqueue作为作业分派.在较旧版本的平台上运行时,它将使用Context.startService.在我的情况下,我正在学习JobIntentService,在我的情况下,我有一个计时器,每秒运行一次并显示当前的日期和时间,但当我的应用程序被销毁时,JobInt

  3. android – Toast与Dialog框:何时使用?

    谢谢.解决方法Toast主要用于告知用户一些不重要且不需要交互的东西,所以我会使用Toastforthethat.此外,Toast不会阻止用户使用设备/应用程序,您仍然可以激活,例如显示Toast时的基础图标.对话框通常要求用户做出选择,或者显示不需要交互的进度但是将使用户在此期间不做其他事情,这可能是重要的,例如,一旦用户在完成参数之前更改参数,您进行的计算将失败.

  4. 在Android中收听ENTER键

    不是那样的.您需要覆盖dispatchKeyEvent.一个例子:

  5. android – ProgressDialog.dismiss()不起作用

    请检查以下示例代码.显示Toast消息但从不隐藏progressdialog.为什么?五秒后显示“DONE”消息,但progressdialog没有被解雇,即使我把pd.dismiss()放在thr下面pd.show()我也不会忽略progressdialog,我不知道为什么会这样.这让我发疯了!)行旁边显示一个简洁的小警告标志.你在做什么>使用新的ProgressDialog()创建进度对话框>使用pd.Show()创建带有所需文本的另一个进度对话框,而不存储对它的引用.>关闭第一个对话框.的对话框仍然

  6. android – 将侦听器添加到数字选择器小部件

    我正在努力将数字选择器集成到我的应用程序中.活动显示每个带有数字选择器的项目列表.用户可以使用数字选择器增加或减少数量.当他们这样做时,我想更新显示价格的TextView.我试图实现这一目标时遇到了困难.我做了一个简单的项目,并试图在用户点击小部件时尝试显示一个Toast消息,但无济于事.我的猜测是,数字窗口小部件不被视为按钮,因此点击监听器不起作用?

  7. Android中的EACCESS权限被拒绝

    在外部SD卡中写入文件时,我收到错误EACCESS权限被拒绝.我已经设置了权限但是当我读取文件时,我成功地能够读取它但无法写入文件.我用于在SD卡中写入文件的代码是:外部存储卡的路径是mnt/extsd/.这就是为什么我无法使用Environment.

  8. android – 如何立即用第二个toast替换当前的toast而不等待当前的toast完成?

    参见英文答案>HowtopreventMultipleToastOverlaps7个我有很多按钮.点击其中的每一个我都会显示一个Toast.但是当一个toast加载并在视图中显示时,单击另一个按钮并且在显示的那个完成之前不会显示toast.所以,我想找出一种方法来检测当前上下文中是否显示吐司.有没有办法知道是否正在显示吐司,以便我可以取消它并显示一个新的.解决方法您可以在Activity的变量中缓

  9. Spinner在android中不起作用

    Spinner无法正常工作.这是我的代码:解决方法你犯的唯一错误是你设置了不同的事件监听器..ListenerOnItemClickListener适用于ListView..对于Spinner,你必须设置OnItemSelectedListener..尝试将OnItemClickListener替换为OnItemSelectedListener,如:

  10. android – BroadcastReceiver没有接收广播

    我正在尝试使用以下代码扩展活动来广播Toast消息.但是广播没有被另一个活动收到,烤面包没有显示.有人能解决我的错误吗?

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部