Android Toast基础与原理
2017-07-08 10:58
375 查看
一、Toast的使用方式
Toast.makeText(context,text,duration)public Toast(Context context)
在Android系统中,给我们提供了两种方式来创建一个Toast对象。第一种是通过makeText方法快速构建Toast对象。第二种是通过Toast的构造方法进行创造一个空的(不含View)的Toast对象。注意,通过构造方法创建的Toast对象,在show()前需要我们调用setView(View view)方法来设置显示的View。
这里我们简要查看下makeText方法和构造方法的源码。
构造方法
/** * Construct an empty Toast object. You must call {@link #setView} before you * can call {@link #show}. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. */ public Toast(Context context) { mContext = context; mTN = new TN(); 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); }
在上面的源码中,我们可以看到构造方法中核心操作是创建一个TN对象,并给它设置默认的y轴方向上的偏移和默认的对齐方式Gravity。
makeText方法
/** * Make a standard toast that just contains a text view. */ public static Toast makeText(Context context, CharSequence text, @Duration int duration) { Toast result = new Toast(context); 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; }
makeText方法会构建一个只含有TextView的视图来展示。源码中第一步还是通过构造函数构建一个空的Toast对象,然后通过LayoutInflater解析一个View对象,绑定到Toast对象上进行展示。
通过上面对makeToast的源码分析,我们比葫芦画瓢的总结下自定义视图Toast的实现过程:
通过构造方法创建一个空的Toast对象;
自定义视图layout,通过LayoutInflate解析成View对象,通过Toast.setView方法设置。
调用Toast.show()方法进行展示。
二、Toast常用方法
1、setDuration(@Duration
int duration)
设置Toast的显示时长,有两个值:LENGTH_SHORT, LENGTH_LONG。
2、setGravity(int
gravity, int xOffset, int yOffset)
设置Toast显示的位置,这里有三个参数。
gravity:toast显示的位置,CENTER_VERTICAL(垂直居中)、CENTER_HORIZONTAL(水平居中)、TOP(顶部);
xOffset:Toast在水平方向(x轴)的偏移量,偏移量单位为,大于0向右偏移,小于0向左偏移;
yOffset:Toast在垂直方向(y轴)的偏移量,大于0向下偏移,小于0向上偏移;
3、setMargin(float
horizontalMargin, float verticalMargin)
4、setText(CharSequence
s)
设置Toast的显示文字。
5、setView(View
view)
设置Toast展示的View样式。可用于我们的自定义Toast显示样式。
6、show()
Toast的显示调用的方法。
三、Toast的展示与管理
通过上面的介绍,我们知道通过Toast.show()方法完成Toast的显示。/** * Show the view for the specified duration. */ 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的源码可以看到系统中创建一个INotificationManager对象,然后通过enqueueToast方法将一个TN对象入队列。这里,我们需要看看这个TN对象是什么?
TN对象是什么?
private static class TN extends ITransientNotification.Stub
又是熟悉的面孔,却是不一样的味道。一个Binder的子类出现了。我们来看看ItransientNotification定义了哪些接口。
/** @hide */ oneway interface ITransientNotification { void show(); void hide(); }
显示与隐藏的方法。我们知道了定义的接口方法在去看它的实现类思路会清晰很多,看看TN的实现:
private static class TN extends ITransientNotification.Stub{
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);
}
};
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView;
View mNextView;
int mDuration;
WindowManager mWM;
static final long SHORT_DURATION_TIMEOUT = 5000;
static final long LONG_DURATION_TIMEOUT = 1000;
TN() {
// 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;
}
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(0, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
// remove the old view if necessary
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);
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
private void trySendAccessibilityEvent() {
AccessibilityManager accessibilityManager =
AccessibilityManager.getInstance(mView.getContext());
if (!accessibilityManager.isEnabled()) {
return;
}
// treat toasts as notifications since they are used to
// announce a transient piece of information to the user
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setClassName(getClass().getName());
event.setPackageName(mView.getContext().getPackageName());
mView.dispatchPopulateAccessibilityEvent(event);
accessibilityManager.sendAccessibilityEvent(event);
}
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
}
在TN类的实现内部,实现了show和hide方法。然后通过Handler进行发送消息来实现。最终是通过handleShow和handleHide来完成Toast的显示和取消。通过上面源码可以看到,本质上又是通过WindowManager来进行管理和显示的。
INotificationManager怎么创建的?
在这里面通过getService方法获取INotificationManager对象。
private static INotificationManager sService; static private INotificationManager getService() { if (sService != null) { return sService; } sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; }
在这段代码里面,我们看到asInterface这个方法接口,我们就可以根据经验判断这个肯定是INotificationManager是一个AIDL文件。而通过:
ServiceManager.getService("notification")
获取的肯定是一个实现该AIDL的Binder对象。
public static IBinder getService(String name) { try { IBinder service = sCache.get(name); if (service != null) { return service; } else { return getIServiceManager().getService(name); } } catch (RemoteException e) { Log.e(TAG, "error in getService", e); } return null; }
不由得让我们联想到NotificationManager通知管理。我们粗略的查看下NotificationManager的源码可以发现这样一段代码:
private static INotificationManager sService; /** @hide */ static public INotificationManager getService() { if (sService != null) { return sService; } IBinder b = ServiceManager.getService("notification"); sService = INotificationManager.Stub.asInterface(b); return sService; }
是不是惊奇的发现,这两段代码是惊人的相似。确实,这也可以印证,Toast和Notification在系统中都是由INotificationManager进行管理完成。
INotificationManager是什么?
INotificationManager是一个AIDL文件,那么它定义了哪些接口呢?
interface INotificationManager { void cancelAllNotifications(String pkg, int userId); void enqueueToast(String pkg, ITransientNotification callback, int duration); void cancelToast(String pkg, ITransientNotification callback); void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id, in Notification notification, inout int[] idReceived, int userId); void cancelNotificationWithTag(String pkg, String tag, int id, int userId); void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled); boolean areNotificationsEnabledForPackage(String pkg, int uid); void setPackagePriority(String pkg, int uid, int priority); int getPackagePriority(String pkg, int uid); void setPackagePeekable(String pkg, int uid, boolean peekable); boolean getPackagePeekable(String pkg, int uid); void setPackageVisibilityOverride(String pkg, int uid, int visibility); int getPackageVisibilityOverride(String pkg, int uid); // TODO: Remove this when callers have been migrated to the equivalent // INotificationListener method. StatusBarNotification[] getActiveNotifications(String callingPkg); StatusBarNotification[] getHistoricalNotifications(String callingPkg, int count); void registerListener(in INotificationListener listener, in ComponentName component, int userid); void unregisterListener(in INotificationListener listener, int userid); void cancelNotificationFromListener(in INotificationListener token, String pkg, String tag, int id); void cancelNotificationsFromListener(in INotificationListener token, in String[] keys); void setNotificationsShownFromListener(in INotificationListener token, in String[] keys); ParceledListSlice getActiveNotificationsFromListener(in INotificationListener token, in String[] keys, int trim); void requestHintsFromListener(in INotificationListener token, int hints); int getHintsFromListener(in INotificationListener token); void requestInterruptionFilterFromListener(in INotificationListener token, int interruptionFilter); int getInterruptionFilterFromListener(in INotificationListener token); void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim); void setInterruptionFilter(String pkg, int interruptionFilter); ComponentName getEffectsSuppressor(); boolean matchesCallFilter(in Bundle extras); boolean isSystemConditionProviderEnabled(String path); int getZenMode(); ZenModeConfig getZenModeConfig(); boolean setZenModeConfig(in ZenModeConfig config, String reason); oneway void setZenMode(int mode, in Uri conditionId, String reason); oneway void notifyConditions(String pkg, in IConditionProvider provider, in Condition[] conditions); oneway void requestZenModeConditions(in IConditionListener callback, int relevance); boolean isNotificationPolicyAccessGranted(String pkg); NotificationManager.Policy getNotificationPolicy(String pkg); void setNotificationPolicy(String pkg, in NotificationManager.Policy policy); String[] getPackagesRequestingNotificationPolicyAccess(); boolean isNotificationPolicyAccessGrantedForPackage(String pkg); void setNotificationPolicyAccessGranted(String pkg, boolean granted); byte[] getBackupPayload(int user); void applyRestore(in byte[] payload, int user); ParceledListSlice getAppActiveNotifications(String callingPkg, int userId); }
在上面我们看到了enqueueToast、cancelToast两个方法,这两个方法就是对应我们show里面的方法。那么这两个方法是在哪里实现的呢?
INotificationManager的实现
通过查看源码,我们发现INotificationManager中的接口是在NotificationManagerService中进行实现的。
private final IBinder mService = new INotificationManager.Stub() { 1062 // Toasts 1063 // ============================================================================ 1064 1065 @Override 1066 public void enqueueToast(String pkg, ITransientNotification callback, int duration) 1067 { 1068 if (DBG) { 1069 Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback 1070 + " duration=" + duration); 1071 } 1072 1073 if (pkg == null || callback == null) { 1074 Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback); 1075 return ; 1076 } 1077 1078 final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg)); 1079 1080 if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) { 1081 if (!isSystemToast) { 1082 Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request."); 1083 return; 1084 } 1085 } 1086 1087 synchronized (mToastQueue) { 1088 int callingPid = Binder.getCallingPid(); 1089 long callingId = Binder.clearCallingIdentity(); 1090 try { 1091 ToastRecord record; 1092 int index = indexOfToastLocked(pkg, callback); 1093 // If it's already in the queue, we update it in place, we don't 1094 // move it to the end of the queue. 1095 if (index >= 0) { 1096 record = mToastQueue.get(index); 1097 record.update(duration); 1098 } else { 1099 // Limit the number of toasts that any given package except the android 1100 // package can enqueue. Prevents DOS attacks and deals with leaks. 1101 if (!isSystemToast) { 1102 int count = 0; 1103 final int N = mToastQueue.size(); 1104 for (int i=0; i<N; i++) { 1105 final ToastRecord r = mToastQueue.get(i); 1106 if (r.pkg.equals(pkg)) { 1107 count++; 1108 if (count >= MAX_PACKAGE_NOTIFICATIONS) { 1109 Slog.e(TAG, "Package has already posted " + count 1110 + " toasts. Not showing more. Package=" + pkg); 1111 return; 1112 } 1113 } 1114 } 1115 } 1116 1117 record = new ToastRecord(callingPid, pkg, callback, duration); 1118 mToastQueue.add(record); 1119 index = mToastQueue.size() - 1; 1120 keepProcessAliveLocked(callingPid); 1121 } 1122 // If it's at index 0, it's the current toast. It doesn't matter if it's 1123 // new or just been updated. Call back and tell it to show itself. 1124 // If the callback fails, this will remove it from the list, so don't 1125 // assume that it's valid after this. 1126 if (index == 0) { 1127 showNextToastLocked(); 1128 } 1129 } finally { 1130 Binder.restoreCallingIdentity(callingId); 1131 } 1132 } 1133 } 1134 1135 @Override 1136 public void cancelToast(String pkg, ITransientNotification callback) { 1137 Slog.i(TAG, "cancelToast pkg=" + pkg + " callback=" + callback); 1138 1139 if (pkg == null || callback == null) { 1140 Slog.e(TAG, "Not cancelling notification. pkg=" + pkg + " callback=" + callback); 1141 return ; 1142 } 1143 1144 synchronized (mToastQueue) { 1145 long callingId = Binder.clearCallingIdentity(); 1146 try { 1147 int index = indexOfToastLocked(pkg, callback); 1148 if (index >= 0) { 1149 cancelToastLocked(index); 1150 } else { 1151 Slog.w(TAG, "Toast already cancelled. pkg=" + pkg 1152 + " callback=" + callback); 1153 } 1154 } finally { 1155 Binder.restoreCallingIdentity(callingId); 1156 } 1157 } 1158 }
在Toast的显示过程中,我们可以发现最终调用的showNextToastLocked方法进行显示。
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(); 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); } keepProcessAliveLocked(record.pid); if (mToastQueue.size() > 0) { record = mToastQueue.get(0); } else { record = null; } } } } 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); }
其中有一段代码:
record.callback.show();
这个就是TN中的show方法。所以这样整个显示的过程就打通了。
通过NotificationManager的源码我们可以得到以下信息: 1、Toast的显示时间长短
static final int LONG_DELAY = 3500; // 3.5 seconds static final int SHORT_DELAY = 2000; // 2 seconds
2、Toast内部管理是通过List集合的形式进行管理。
final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>();
3、Toast在一个包中,最多可在Toast队列中添加50个。
static final int MAX_PACKAGE_NOTIFICATIONS = 50;
4、Toast内部显示消息管理通过Handler进行实现。
总结
Toast在创建的时候都会创建TN对象。该对象实现跨进程的通信协议show、hide方法。最后通过INotificationManager进行统一的管理。最后通过TN实现的show方法进行展示。
相关文章推荐
- android基础进度条原理
- android基础进度条原理
- android基础学习之二维码的生成原理
- Android基础教程(三)之-----简单的Button事件响应综合提示控件Toast的应用
- Android-应用程序基础及原理概要
- Android基础教程(三)之-----简单的Button事件响应综合提示控件Toast的应用
- android 开发原理与基础
- Android原理——自定义Toast原理
- android基础学习之自定义view的简单原理
- android 显示基础原理
- 【android基础学习之三】——基础控件Toast,EditText,RadioGroup,RadioButton
- android开发教程(十三)——JAVA基础之理解JNI原理(java调用C语言接口)
- Android应用基础及原理概要,四大组件
- Android 最基础的Toast
- 【Android基础】Android Toast显示消息的几种方法
- android基础进度条原理
- android基础学习(5)-------Toast的使用
- Toast--android基础
- 【Android应用开发】 推送原理解析 极光推送使用详解 (零基础精通推送)
- 0827Android基础Toast+AlertDialog