我们最常使用Toast的方法是 Toast.makeText().show();
在源码中找到 Toast.java 文件的路径
frameworks/base/core/java/android/widget/Toast.java
先看 makeText() 方法
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;
}
第一个参数 context : 上下文,这个不用多讲
第二个参数 looper : handler 的那个 looper,这个也不需要多说,@Nullable 可以为 null
第三个参数 text : 要显示的字符串
第四个参数 duration : toast显示的时间,@Duration 注解代码看下面,这个注解规定了只能有两种显示时长 LENGTH_SHORT = 0(4秒),LENGTH_LONG = 1(7秒)
/** @hide */
@IntDef(prefix = { "LENGTH_" }, value = {
LENGTH_SHORT,
LENGTH_LONG
})
@Retention(RetentionPolicy.SOURCE)
public @interface Duration {}
static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;
mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
从 makeText() 方法体里面看到其实这个方法很简单
第一步:new 一个 Toast 对象
第二步:inflate 布局文件成一个 view
第三步:把要显示的 text 内容设置到布局文件中的 message(TextView) 中
第四步:把 view 和 duration 设置到 toast 对象中
第五步:返回这个 toast 对象,方便链式编程
然后再看一下这个构造函数
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);
}
第一步:new TN 对象,TN 是 TransientNotification 短暂的通知的缩写,这个后面会详细介绍
第二步:设置 y 轴偏默认移量 toast_y_offset = 24dp,设置默认 gravity = 0x00000051 (Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM)
frameworks/base/core/res/res/values/config.xml
<dimen name="toast_y_offset">24dp</dimen>
<!-- Default Gravity setting for the system Toast view. Equivalent to: Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM -->
<integer name="config_toastDefaultGravity">0x00000051</integer>
接下来看 TN 的构造函数
TN(String packageName, @Nullable Looper looper) {
// XXX This should be changed to use a Dialog, with a Theme.Toast
// defined that sets up the layout params appropriately.
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;
}
}
}
};
}
这个构造函数就做了两件事情
1.初始化 WindowManager.LayoutParams,窗口类型设置为 TYPE_TOAST 这样就可以显示在比较顶层
2.初始化 handler 用于处理 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;
final int displayId = mContext.getDisplayId();
try {
if (localLOGV) Log.v(TAG, "Toast SHOW: " + this + " view = " + mNextView
+ " caller: " + android.os.Debug.getCallers(1));
service.enqueueToast(pkg, tn, mDuration, displayId);
} catch (RemoteException e) {
// Empty
}
}
这个方法中调用了一个 service.enqueueToast() 方法,这个 service 是 INotificationManager 的实例对象,就是用 AIDL binder 机制调用 NotificationManagerService 中的方法
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration,
int displayId)
{
if (DBG) {
Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
+ " duration=" + duration + " displayId=" + displayId);
}
if (pkg == null || callback == null) {
Slog.e(TAG, "Not enqueuing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
final int callingUid = Binder.getCallingUid();
final boolean isSystemToast = isCallerSystemOrPhone()
|| PackageManagerService.PLATFORM_PACKAGE_NAME.equals(pkg);
final boolean isPackageSuspended = isPackageSuspendedForUser(pkg, callingUid);
final boolean notificationsDisabledForPackage = !areNotificationsEnabledForPackage(pkg,
callingUid);
long callingIdentity = Binder.clearCallingIdentity();
try {
final boolean appIsForeground = mActivityManager.getUidImportance(callingUid)
== IMPORTANCE_FOREGROUND;
if (ENABLE_BLOCKED_TOASTS && !isSystemToast && ((notificationsDisabledForPackage
&& !appIsForeground) || isPackageSuspended)) {
Slog.e(TAG, "Suppressing toast from package " + pkg
+ (isPackageSuspended ? " due to package suspended."
: " by user request."));
return;
}
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// Limit the number of toasts that any given package except the android
// package can enqueue. Prevents DOS attacks and deals with leaks.
if (!isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i<N; i++) {
final ToastRecord r = mToastQueue.get(i);
if (r.pkg.equals(pkg)) {
count++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, displayId);
record = new ToastRecord(callingPid, pkg, callback, duration, token,
displayId);
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 添加到一个 mToastQueue 队列中,然后调用 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);
scheduleDurationReachedLocked(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;
}
}
}
}
这个方法会从 mToastQueue 队列中依次取出第一个 toast 来显示
至此我们对 Toast 的整个流程已经有了一个清晰的认识,那么还有几个问题需要思考一下:
1.从代码中我们看到 toast 的显示时间 duration 只有两种情况,是否能控制 toast 的显示时间?使用反射能否实现?
2.能否通过命令显示一个 toast?