Toast的作用主要是快速的展示相關(guān)信息給用戶,同時(shí)又不占據(jù)太多屏幕位置褐奴,也不奪取焦點(diǎn)按脚,更重要的時(shí)其調(diào)用非常簡(jiǎn)單,一行代碼就可以實(shí)現(xiàn)敦冬。所以學(xué)習(xí)一下Toast的源碼還是很有必要的辅搬。
Toast的源碼在這個(gè)位置:
frameworks\base\core\java\android\widget\Toast.java
先看我們常用的調(diào)用方式
Toast.makeText(context,"Hello ",Toast.LENGTH_SHORT).show();
一段非常簡(jiǎn)潔的鏈?zhǔn)秸{(diào)用代碼,一個(gè)方法就是設(shè)置內(nèi)容和時(shí)長(zhǎng)脖旱,第二個(gè)方法就是顯示堪遂。
我們先從第一個(gè)方法入手介蛉。用過(guò)的都知道m(xù)akeText有一個(gè)重載方法,主要就是所傳內(nèi)容的參數(shù)類型不同溶褪,一個(gè)可以直接傳入字符串币旧,一個(gè)則可以傳入資源ID,如下:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
...
}
public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
throws Resources.NotFoundException {
...
}
但是從8.0開(kāi)始猿妈,又多了一個(gè)重載方法吹菱,如下:
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
}
可以看到多了一個(gè)Looper參數(shù),用過(guò)的朋友都知道Toast是不可以直接在子線程調(diào)用的彭则,否則會(huì)有如下錯(cuò)誤:
Can't toast on a thread that has not called Looper.prepare()
具體原因后面在分析鳍刷,根據(jù)其提示信息,我們可以分別調(diào)用 Looper.prepare()和 Looper.loop()來(lái)實(shí)現(xiàn)在子線程使用Toast贰剥。但這樣比較麻煩倾剿,所以8.0中又多了一個(gè)這樣的方法,可以直接傳入一個(gè)主線程的looper蚌成,然后在子線程調(diào)用Toast前痘。但是這個(gè)方法目前是hide狀態(tài)的,我們無(wú)法直接調(diào)用担忧,為了試試這個(gè)方法芹缔,我們可以通過(guò)反射調(diào)用一下,看看效果瓶盛。
final Class clazz = Class.forName("android.widget.Toast");
final Method method = clazz.getMethod("makeText", Context.class, Looper.class,CharSequence.class,int.class);
new Thread(){
@Override
public void run() {
super.run();
try {
Toast toast = (Toast) method.invoke(null,context,Looper.getMainLooper(),"Hello",0);
toast.show();
} catch (Exception e) {
Log.d("MTC",e.getMessage());
}
}
}.start();
結(jié)果證明是可以正常調(diào)用的最欠,至于為什么要隱藏這個(gè)方法,目前還不清楚惩猫。
現(xiàn)在我們開(kāi)始看這個(gè)方法的具體實(shí)現(xiàn):
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
throws Resources.NotFoundException {
return makeText(context, context.getResources().getText(resId), duration);
}
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;
}
可以看到前兩個(gè)makeText方法都在間接調(diào)用第三個(gè)重載芝硬,只是默認(rèn)將looper方法設(shè)為null而已。在第三個(gè)重載中轧房,主要做了這幾件事拌阴,1.構(gòu)造一個(gè)Toast對(duì)象。2.引入一個(gè)布局并且給Textview設(shè)置內(nèi)容奶镶。3.設(shè)置顯示時(shí)間迟赃。
相應(yīng)的在Toast中也提供了一些get,set方法來(lái)獲取和設(shè)置布局和文字厂镇。如:setView纤壁,getView,setText捺信。注意沒(méi)有g(shù)etText方法酌媒。其中有setDuration方法,但是并不是想象中那樣自定義事件,而是只能設(shè)置為L(zhǎng)ENGTH_SHORT和LENGTH_LONG兩種馍佑。對(duì)應(yīng)的也有g(shù)etDuration斋否。
我們可以簡(jiǎn)單看一下這個(gè)布局文件,布局很簡(jiǎn)單,就是一個(gè)TextView
frameworks\base\core\res\res\layout\transient_notification.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/toastFrameBackground">
<TextView
android:id="@android:id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="15dp"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Toast"
android:textColor="@color/primary_text_default_material_light"
/>
</LinearLayout>
接下來(lái)看一下Toast的構(gòu)造方法:
public Toast(Context context) {
this(context, null);
}
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);
}
同樣拭荤,相對(duì)于之前版本的代碼茵臭,8.0中也多出了一個(gè)帶Looper參數(shù)的構(gòu)造函數(shù),同樣這個(gè)構(gòu)造函數(shù)也是隱藏的舅世。第一個(gè)構(gòu)造也是調(diào)用第二個(gè)構(gòu)造旦委。在構(gòu)造函數(shù)中,初始化了Context變量雏亚,構(gòu)造了一個(gè)TN對(duì)象缨硝,并設(shè)置TN對(duì)象中的一些參數(shù)。我們?yōu)g覽Toast一些布局設(shè)置的方法時(shí)發(fā)現(xiàn)罢低,比如setGravity查辩,setMargin等,都是間接的設(shè)置給了TN网持,說(shuō)明TN是用來(lái)控制Toast的宜岛。我們就來(lái)看一下這個(gè)類。
它是Toast里的一個(gè)靜態(tài)內(nèi)部類功舀,父類是ITransientNotification萍倡,構(gòu)造函數(shù)如下:
TN(String packageName, @Nullable Looper looper) {
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
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;
mPackageName = packageName;
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
在構(gòu)造中,先是設(shè)置許多布局屬性辟汰,在這里我們看到在設(shè)置flag時(shí)設(shè)置為不可觸摸和不可獲得焦點(diǎn)列敲。然后設(shè)置looper變量,如果使用不帶Looper參數(shù)的makeText方法帖汞,這里的looper會(huì)用Looper.myLooper()方法初始化戴而,這也就是在子線程中,myLooper()返回為空翩蘸,導(dǎo)致報(bào)錯(cuò)的原因填硕。這個(gè)looper使用在Handler中的,所以不能為空鹿鳖。之后實(shí)例化了一個(gè)Handler對(duì)象,用于處理show壮莹,hide和cancel動(dòng)作翅帜。
在TN里面也定義了顯示的時(shí)長(zhǎng):
static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;
我們也看到TN里的show,hide命满,cancel方法也都是通過(guò)mHandler傳遞消息涝滴,在Handler對(duì)象調(diào)用對(duì)應(yīng)方法實(shí)現(xiàn)的。
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
public void cancel() {
if (localLOGV) Log.v(TAG, "CANCEL: " + this);
mHandler.obtainMessage(CANCEL).sendToTarget();
}
我們首先看一下handleShow方法:
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
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);
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);
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
可以看出還是很簡(jiǎn)單的,主要是設(shè)置窗口參數(shù)歼疮,然后通過(guò)WindowManager添加視圖即可杂抽。由此可以想到hide就是通過(guò)WindowManager移除視圖,具體看代碼:
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
這里總結(jié)一下韩脏,Toast的makeText方法只是實(shí)例化出來(lái)一個(gè)Toast對(duì)象和TN對(duì)象缩麸,其中Toast只是用來(lái)提供接口讓我們?cè)O(shè)置各種參數(shù),TN則是實(shí)際上用來(lái)控制Toast的顯示隱藏及布局等操作赡矢。
最后看一下Toast的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
}
}
這個(gè)就很清楚了杭朱,首先獲得INotificationManager對(duì)象,然后將顯示toast的請(qǐng)求加入隊(duì)列吹散,等待顯示弧械。
INotificationManager的源碼在以下位置:
frameworks\base\services\core\java\com\android\server\notification\NotificationManagerService.java
由于Toast的顯示和隱藏是由INotificationManager管理的,所以我們具體看一下相關(guān)的幾個(gè)方法空民。
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
final boolean isPackageSuspended =
isPackageSuspendedForUser(pkg, Binder.getCallingUid());
if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
(!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
|| isPackageSuspended)) {
return;
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index;
if (!isSystemToast) {
index = indexOfToastPackageLocked(pkg);
} else {
index = indexOfToastLocked(pkg, callback);
}
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;
}
keepProcessAliveIfNeededLocked(callingPid);
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
這就是進(jìn)隊(duì)列的方法刃唐,其中mToastQueue就是一個(gè)ArrayList,里面保存著一個(gè)個(gè)ToastRecord界轩,ToastRecord也是一個(gè)靜態(tài)內(nèi)部類画饥,負(fù)責(zé)保存進(jìn)程ID,ITransientNotification實(shí)例耸棒,時(shí)長(zhǎng)等信息荒澡。基本流程就是先判斷是否存在隊(duì)列中(這個(gè)判斷主要是基于包名的与殃,詳見(jiàn)indexOfToastPackageLocked方法)单山,若存在則更新時(shí)長(zhǎng)和TN信息(用于更新內(nèi)容等),否則加入隊(duì)列末尾幅疼。若當(dāng)前是隊(duì)列第一個(gè)米奸,則調(diào)用showNextToastLocked()來(lái)顯示,方法如下:
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
record.callback.show(record.token);
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
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;
}
}
}
}
基本流程就是不斷從隊(duì)列中取第一個(gè)ToastRecord爽篷,然后調(diào)用TN實(shí)例中的show方法顯示悴晰。也就回到之前我們看的handleShow方法中,利用WindowManager添加視圖進(jìn)行顯示逐工。之后調(diào)用了scheduleTimeoutLocked铡溪,主要用于移除Toast
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);
}
在這里我們看到了Toast顯示時(shí)長(zhǎng)的實(shí)現(xiàn),就是發(fā)送一個(gè)延遲消息泪喊,延遲期間就是顯示的時(shí)機(jī)棕硫。當(dāng)Handler收到MESSAGE_TIMEOUT消息時(shí),執(zhí)行下面方法:
private void handleTimeout(ToastRecord record)
{
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
又調(diào)用了cancelToastLocked方法袒啼,附帶該Toast在隊(duì)列的位置:
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide();
} catch (RemoteException e) {
}
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
showNextToastLocked();
}
}
可見(jiàn)顯示調(diào)用了TN中的hide方法哈扮,然后將ToastRecord移出隊(duì)列纬纪,然后循環(huán)去顯示下一個(gè)Toast。