您的位置:首页 > 移动开发 > Android开发

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方法进行展示。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: