最普通不过的Toast我们进场使用,有没有想过这样的问题,可不可以在显示的时候取消Toast的显示,Toast到底是被添加到哪个Window的,WindowManager是如何添加Toast的,在添加Toast的时候总共有几个WindowManager参与,参与Toast显示的WindowManager是如何产生的……
好吧,想到这么多的时候,我感觉头绪有点乱,还是从最简单的开始吧,先看下Toast的显示吧。
最常用的方式如下:
Toast.makeText(context, "test", Toast.LENGTH_SHORT).show();
跟踪下代码:
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
Toast的对象和普通的对象没有什么区别,先新建一个对象,然后给Toast对象设置一个view,再设置Toast的显示时常,然后makeText
就直接返回,接下来就需要调用show
方法,看下show
方法
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
方法是一个跨进程调用,最终会调用NotificationManagerService
的enqueueToast
方法,这里有一个TN
类型的tn
对象,这个对象的作用是接收来自NotificationManagerService
的回调,后面会对这个对象进行解释,接下来进入NotificationManagerService
,看下它是如何处理Toast
。
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
......
synchronized (mToastQueue) {
......
try {
ToastRecord record;
int index;
if (!isSystemToast) {
index = indexOfToastPackageLocked(pkg);
} else {
......
}
// 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);
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;
}
......
// 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 {
......
}
}
}
最终函数走进了showNextToast
的调用。
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);
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to show notification " + record.callback
+ " in package " + record.pkg);
// remove it from the list and let the process die
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
showNextToast
方法再mToastQueue上进行循环,如果找到待显示的Toast则调用record.callback.show
函数进行显示,并执行scheduleTimeoutLocked
函数。
record.callback.show
函数后面会重点说明,先理解一下scheduleTimeoutLocked
函数
scheduleTimeoutLocked
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
返现scheduleTimeoutLocked
函数还是很简单的,就是根据显示时的时长去触发一个message。这个地方有一个Toast使用经常会遇到的问题,直接使用Toast.makeText
是无法控制Toast的显示时长的,因为系统处理的时候仅仅设置了LONG_DELAY
和SHORT_DELAY
,究竟是不是这个原因,再往下看。
代码段1
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MESSAGE_TIMEOUT:
handleTimeout((ToastRecord)msg.obj);
break;
......
}
}
代码段2
private void handleTimeout(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);
}
}
}
代码段3
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide();
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to hide notification " + record.callback
+ " in package " + record.pkg);
// don't worry about this, we're about to remove it from
// the list anyway
}
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
// 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();
}
}
代码段2是MESSAGE_TIMEOUT
的具体处理逻辑,可以看到,系统执行cancelToastLocked
方法取消了Toast的显示,如果mToastQueue
中还存在未显示的Toast,则继续执行显示。看来直接使用Toast.makeText
无法控制Toast的显示时长的原因就是这儿了。
record.callback.show
这个方法是Toast显示的核心,还记得record的callback指的是什么吗,不记得的同学往前面看即可。callback指的是toast的内部类的TN
类型的对象,因此show
方法调用的也是TN
的show
函数。
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
show
函数很好理解,但是windowToken
从哪来的呢?可以看下前面enqueueToast
函数的实现,为了实现NotificationServiceManager
的跨进程调用,在封装ToastRecord对象时新建了一个binder对象作为windowToken,继续…
public void handleShow(IBinder windowToken) {
.....
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// 删除之前显示的Toast
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
代码看的比较长,实际上逻辑比较简单,就是将Toast的view添加到Windowmanager。具体的流程:判断当前还有没有正在显示的Toast,如果有则先取消,然后给准备显示的Toast设置显示位置,最后将Toast显示出来。