您好,登錄后才能下訂單哦!
前言
我們都清楚,Toast顯示時長有兩個選擇,長顯示是3.5秒,端顯示是2秒。那如果想要做到長時間顯示,該怎么做呢?有個歷史遺留的app通過開一個線程,不斷調(diào)用show方法進行實現(xiàn),這些年也沒出過問題,直到系統(tǒng)版本更新到了Android9.0。
實現(xiàn)方式大概如下:
mToast = new Toast(context); mToast.setDuration(Toast.LENGTH_LONG); mToast.setView(layout); ... mToast.show(); //在線程里不斷調(diào)用show方法,達到長時間顯示的目的
在Android9.0上,Toast閃現(xiàn)了一下就不見了,并沒有如預期那樣,長時間顯示。為什么呢?
概述
這里我們先來大概了解下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實現(xiàn)
先看看Toast的實現(xiàn):
//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的構造函數(shù)很簡單,主要就是mTN這個成員,后續(xù)對Toast的操作都在這里進行。緊接著就是設置Toast顯示時長和顯示內(nèi)容:
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方法簡單,最終是調(diào)用了通知服務的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中,首先會判斷應用是否為系統(tǒng)應用,如果是系統(tǒng)應用,則通過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; }
判斷的依據(jù)是包名和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); //調(diào)用TN類的show方法 scheduleDurationReachedLocked(record); //時間到就隱藏Toast return; } catch (RemoteException e) { ... } } }
該方法也簡單,就是回調(diào)TN類的show方法,上文提過,TN類對外提供show,hide, cancel等方法,在這些方法中,再通過內(nèi)部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 */ } } }
調(diào)用show方法,最終會調(diào)用到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); //已經(jīng)顯示完畢的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(); } }
該方法調(diào)用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的顯示和隱藏已經(jīng)講完。下面說說多次show為什么會導致Toast消失。
Toast的消失
想象一個場景,如果一個全局Toast(此次出問題的app中就是一個全局Toast),我們不斷的去調(diào)用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(); //如果存在已經(jīng)顯示的Toast,這里會先進行hide } catch (RemoteException e) { } record.update(callback); } }
hide的流程我們已經(jīng)清楚,會將資源釋放,將mNextView和mView置為Null。執(zhí)行到這里會導致第一個Toast消失,之后調(diào)用showNextToastLocked()方法顯示第二個Toast,最終調(diào)用到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,不會執(zhí)行mWM.addView(),Toast也就不會顯示。
解決方法
在Android9.0中如果想要一直顯示某個Toast,怎么做?使用局部Toast,不要使用全局Toast。
但有一點比較奇怪的是,查看了Android10.0代碼,發(fā)現(xiàn)Android10.0將這個機制回滾了。即Android10.0上又可以一直顯示Toast:
//這里就不執(zhí)行hide的操作了 if (index >= 0) { record = mToastQueue.get(index); record.update(duration); }
結語
Android多個系統(tǒng)版本中,唯獨Android9.0做了這個特殊處理,無非就是禁用應用長時間顯示Toast。但10.0版本又取消了這個處理,難道是發(fā)現(xiàn)這樣處理并不合適?
到此這篇關于Android9.0上針對Toast的特殊處理的文章就介紹到這了,更多相關Android9.0對Toast的特殊處理內(nèi)容請搜索億速云以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持億速云!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。