前言
顯示頁(yè)面除了Activity,使用最多的可能就是Dialog、PopupWindow、Toast了。這三者有相似之處也有不一樣的地方,本篇文章旨在厘清三者關(guān)系,闡明各自的優(yōu)缺點(diǎn),并探討哪種場(chǎng)合使用它們。
本篇文章涉及到WindowManager相關(guān)知識(shí),如有需要請(qǐng)移步:Window/WindowManager 不可不知之事
通過(guò)本篇文章,你將了解到:
1、Dialog/PopupWindow/Toast 生命周期
2、Dialog/PopupWindow/Toast 異同之處
3、Dialog/PopupWindow/Toast 使用場(chǎng)合
Dialog/PopupWindow/Toast 生命周期
在之前的文章有提過(guò):任何View都需要添加到Window上才能展示,這個(gè)過(guò)程大致分為四個(gè)步驟:
1、構(gòu)造顯示的目標(biāo)View
2、獲取WindowManager 實(shí)例
2、構(gòu)造約束Window的WindowManager.LayoutParams
3、WindowManager.addView(View, LayoutParams)
Dialog/PopupWindow/Toast 實(shí)際上就是封裝了上述四個(gè)步驟,并提供更進(jìn)一步的功能及其更豐富的接口使用,接下來(lái)我們逐步分析。
Dialog 生命周期
先來(lái)看看簡(jiǎn)單demo
//自定義View
MyGroup myGroup = new MyGroup(v.getContext());
//Dialog 實(shí)例
Dialog dialog = new Dialog(v.getContext());
//添加View
dialog.setContentView(myGroup);
//最終展示
dialog.show();
先看看Dialog構(gòu)造函數(shù):
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
//themeResId 指定Dialog樣式
if (createContextThemeWrapper) {
if (themeResId == Resources.ID_NULL) {
//若不指定,則使用默認(rèn)的樣式
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
//獲取WindowManager,context是Activity類型,因此此時(shí)獲取的WindowManager
//即是Activity的WindowManager
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//構(gòu)造Window對(duì)象
final Window w = new PhoneWindow(mContext);
mWindow = w;
//監(jiān)聽(tīng)touch/key event等事件
w.setCallback(this);
//省略
w.setWindowManager(mWindowManager, null, null);
//Window默認(rèn)居中
w.setGravity(Gravity.CENTER);
}
構(gòu)造Window對(duì)象時(shí):
#Window.java
//構(gòu)造LayoutParams
private final WindowManager.LayoutParams mWindowAttributes =
new WindowManager.LayoutParams();
//WindowManager.java
public static final int TYPE_APPLICATION = 2;
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
可以看出,Dialog構(gòu)造方法主要做了兩件事:
1、構(gòu)造WindowManager
2、構(gòu)造Window對(duì)象,同時(shí)在Window里會(huì)初始化WindowManager.LayoutParams 變量
完成了四個(gè)步驟的第二、三步:構(gòu)造WindowManager/LayoutParams對(duì)象。
再看看setContentView(XX)
#Dialog.java
public void setContentView(@android.annotation.NonNull View view) {
//Window 方法,實(shí)例是PhoneWindow
mWindow.setContentView(view);
}
#PhoneWindow.java
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
//構(gòu)造DecorView
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//省略
} else {
//mContentParent 為 DecorView 子View
//將自定義View添加到mContentParent里,最終也是掛到了DecorView Tree里
mContentParent.addView(view, params);
}
//省略
}
其中有關(guān)DecorView的創(chuàng)建過(guò)程請(qǐng)移步:Android DecorView 一窺全貌(上)
setContentView(XX)構(gòu)造了DecorView,并將自定義View添加到DecorView里
最后看看dialog.show()
public void show() {
if (mShowing) {
//Dialog 正在展示,則退出
return;
}
if (!mCreated) {
//最終調(diào)用onCreate(xx)
dispatchOnCreate(null);
} else {
//省略
}
onStart();
//獲取DecorView,在setContentView(XX)時(shí)已經(jīng)構(gòu)造好DecorView
mDecor = mWindow.getDecorView();
//在創(chuàng)建Window時(shí)已經(jīng)構(gòu)造好
WindowManager.LayoutParams l = mWindow.getAttributes();
//添加DecorView
mWindowManager.addView(mDecor, l);
mShowing = true;
}
dialog.show() 完成了四個(gè)步驟中的最后一步:addView(xx)
至此,Dialog創(chuàng)建完畢并顯示,通過(guò)上述分析可知,Dialog將四個(gè)步驟封裝了。
如何關(guān)閉Dialog
既然是通過(guò)WindowManager.addView(xx)添加的View,那么Dialog關(guān)閉相應(yīng)的也需要調(diào)用WindowManager.removeView(xx),此處調(diào)用的是WindowManager.removeViewImmediate(xx),表示立即執(zhí)行銷毀動(dòng)作。
#Dialog.java
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
//主線程直接執(zhí)行
dismissDialog();
} else {
//子線程切換到主線程執(zhí)行
mHandler.post(mDismissAction);
}
}
@UnsupportedAppUsage
void dismissDialog() {
if (mDecor == null || !mShowing) {
return;
}
try {
//移除DecorView
mWindowManager.removeViewImmediate(mDecor);
} finally {
//調(diào)用onStop
onStop();
mShowing = false;
sendDismissMessage();
}
}
Dialog 生命周期如下:
PopupWindow 生命周期
同樣的簡(jiǎn)單demo
//PopupWindow 寬、高
popupWindow = new PopupWindow(400, 400);
MyGroup myGroup = new MyGroup(v.getContext());
popupWindow.setContentView(myGroup);
//展示popupWindow
popupWindow.showAsDropDown(button);
看得出來(lái)PopupWindow創(chuàng)建與Dialog類似。
先看看構(gòu)造函數(shù):
public PopupWindow(View contentView, int width, int height, boolean focusable) {
//contentView 為自定義View
if (contentView != null) {
mContext = contentView.getContext();
//獲取WindowManager mContext 屬于Activity類型
//與Dialog 一樣,WindowManager 就是Activity WindowManager
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
}
//設(shè)置 mContentView = contentView;
setContentView(contentView);
//設(shè)置Window寬、高
setWidth(width);
setHeight(height);
//設(shè)置獲取焦點(diǎn)與否
setFocusable(focusable);
}
注意,PopupWindow 默認(rèn)寬高為0,因此需要外部設(shè)置寬高值
setContentView(XX)
public void setContentView(View contentView) {
if (isShowing()) {
return;
}
//賦值
mContentView = contentView;
if (mContext == null && mContentView != null) {
//獲取Context
mContext = mContentView.getContext();
}
if (mWindowManager == null && mContentView != null) {
//根據(jù)Context獲取WindowManager
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
}
}
popupWindow.showAsDropDown(View anchor)
View anchor 指的是先錨定一個(gè)View,PopupWindow根據(jù)這個(gè)View的位置來(lái)確定自己的位置。
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
if (isShowing() || !hasContentView()) {
//正在展示,則不處理后續(xù)
return;
}
//一系列監(jiān)聽(tīng)錨定的View
attachToAnchor(anchor, xoff, yoff, gravity);
//構(gòu)造 LayoutParams,并設(shè)置其一些參數(shù)
final WindowManager.LayoutParams p =
createPopupLayoutParams(anchor.getApplicationWindowToken());
//構(gòu)造"DecorView",該DecorView不是我們常見(jiàn)的DecorView,而是PopupWindow里的內(nèi)部類
//該View作為Window的根View
preparePopup(p);
//根據(jù)anchor確認(rèn)Window的起始位置
final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
p.width, p.height, gravity, mAllowScrollingAnchorParent);
updateAboveAnchor(aboveAnchor);
//添加到Window里。WindowManager.addView(xx)
invokePopup(p);
}
至此,PopupWindow創(chuàng)建完畢,可以看出以上步驟包括了Window顯示的四個(gè)步驟。
如何關(guān)閉PopupWindow
與Dialog 類似,PopupWindow 有個(gè)方法:
public void dismiss();
該方法最后調(diào)用了WindowManager.removeViewImmediate(xx)方法移除Window。
Toast 生命周期
還是一個(gè)小demo:
Toast.makeText(App.getApplication(), "hello toast", Toast.LENGTH_LONG).show();
makeText(XX)是個(gè)靜態(tài)方法:
public static Toast makeText(@android.annotation.NonNull Context context, @android.annotation.Nullable Looper looper,
@android.annotation.NonNull CharSequence text, @Duration int duration) {
//構(gòu)造 Toast對(duì)象
Toast result = new Toast(context, looper);
//加載View
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是v的子View 設(shè)置顯示的內(nèi)容
tv.setText(text);
//記錄到Toast里
result.mNextView = v;
result.mDuration = duration;
return result;
}
Toast.show()方法
public void show() {
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
//構(gòu)造TN對(duì)象
TN tn = mTN;
tn.mNextView = mNextView;
final int displayId = mContext.getDisplayId();
try {
//加入到隊(duì)列里
service.enqueueToast(pkg, tn, mDuration, displayId);
} catch (RemoteException e) {
// Empty
}
}
到此Toast創(chuàng)建并顯示出來(lái),但是我們并沒(méi)有看到熟悉的WindowManager.addView(xx),繼續(xù)來(lái)看看。
show()方法里構(gòu)造了TN對(duì)象,最后該對(duì)象被加入到了INotificationManager里。該類是底層服務(wù)類,其實(shí)現(xiàn)類是:NotificationManagerService.java。既然傳給了底層,那么勢(shì)必要有傳回來(lái)的動(dòng)作,查看TN類發(fā)現(xiàn):
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
//發(fā)送到handler執(zhí)行
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
public void handleShow(IBinder windowToken) {
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();
}
//獲取 WindowManager 對(duì)象
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
//WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
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;
}
//設(shè)置Toast 坐標(biāo)等屬性
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);
}
try {
//添加到Window
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
又看到了熟悉的addView(xx)流程??偨Y(jié)來(lái)說(shuō):
make() 方法構(gòu)造Toast
show() 方法 將要顯示的內(nèi)容加入到service
service根據(jù)時(shí)間長(zhǎng)短通過(guò)handler通知UI進(jìn)行展示
如何關(guān)閉Toast
既然Toast顯示策略都在service里完成,那么當(dāng)時(shí)間到了之后讓Toast消失也是service通知上層銷毀Window
public void cancel() {
if (localLOGV) Log.v(TAG, "CANCEL: " + this);
mHandler.obtainMessage(CANCEL).sendToTarget();
}
public void handleHide() {
if (mView != null) {
if (mView.getParent() != null) {
//銷毀Window
mWM.removeViewImmediate(mView);
}
try {
getService().finishToken(mPackageName, this);
} catch (RemoteException e) {
}
mView = null;
}
}
Dialog/PopupWindow/Toast 異同之處
上邊分析了三者的生命周期,了解到他們都是通過(guò)addView(xx)添加View到Window進(jìn)行展示的,那么他們各自的特點(diǎn)以及側(cè)重點(diǎn)是體現(xiàn)在哪些方面呢?接下來(lái)分析。
當(dāng)我們分別運(yùn)行上邊的三個(gè)demo,發(fā)現(xiàn):
Dialog 表現(xiàn):
居中展示、外部有蒙層、點(diǎn)擊屏幕外Dialog消失、點(diǎn)擊返回鍵Dialog消失、Dialog 攔截了屏幕上所有的touch/key 事件。
Dialog需要Activity類型的Context啟動(dòng)。
有動(dòng)畫(huà)。
PopupWindow 表現(xiàn)
基于某個(gè)錨點(diǎn)顯示,可以偏移任何距離。點(diǎn)擊屏幕外PopupWindow不消失,PopupWindow 僅僅攔截自身區(qū)域內(nèi)的touch/key 事件。
PopupWindow需要Activity類型的Context啟動(dòng)。
有動(dòng)畫(huà)。
Toast 表現(xiàn)
Toast 在屏幕底部彈出一段文本,該文本在展示指定的時(shí)間后消失。
Toast 不強(qiáng)制需要Activity類型的Context啟動(dòng)。
有動(dòng)畫(huà)。
接下來(lái)看看造成以上差異之處的原因:
Window 位置確定
WindowManager.LayoutParams.gravity
指定Window方位,如居中、居左、居右、居底、居頂。
WindowManager.LayoutParams.x
WindowManager.LayoutParams.y
這倆參數(shù)確定Window 距離"gravity"指定方位的偏移。
如當(dāng)gravity=Gravity.LEFT 那么layoutParams.x = 200(正數(shù)),表示X軸向右偏移的距離,負(fù)數(shù)反之。
當(dāng)gravity=Gravity.RIGHT 那么layoutParams.x = 200,表示X軸向左偏移的距離,負(fù)數(shù)反之。
同理垂直方向也是一樣道理。
因此Window 位置確定是通過(guò)gravity 和x/y屬性結(jié)合判斷的。
Dialog 位置確定
Dialog(@android.annotation.NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
//省略
final Window w = new PhoneWindow(mContext);
//設(shè)置gravity
w.setGravity(Gravity.CENTER);
}
Dialog 構(gòu)造函數(shù)里設(shè)置Window居中,因此demo里表現(xiàn)出來(lái)的Dialog居中展示。
因此改變"gravity"默認(rèn)值:
dialog.getWindow().getAttributes().gravity = Gravity.XX
PopupWindow 位置確定
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
//省略...
//確定layoutParams.x/layoutParams.y 的值
//xoff/yoff 表示的是window 距離錨點(diǎn)anchor的偏移,默認(rèn)是anchor的左下角
//gravity指的是window與anchor的對(duì)齊方式,比如Gravity.RIGHT,表示W(wǎng)indow與anchor右對(duì)齊
//當(dāng)xoff/yoff、gravity同時(shí)設(shè)置時(shí),先按照anchor的左下角偏移xoff/yoff,得出當(dāng)前的layoutParams.x/layoutParams.y值
//再根據(jù)gravity調(diào)整layoutParams.x/layoutParams.y值
final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
p.width, p.height, gravity, mAllowScrollingAnchorParent);
//省略...
}
findDropDownPosition(xx) 該方法確定了PopupWindow 的WindowManager.LayoutParams.x/WindowManager.LayoutParams.y值。
再來(lái)看看WindowManager.LayoutParams.gravity如何確定的:
protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
//計(jì)算出LayoutParams.gravity
p.gravity = computeGravity();
//省略
return p;
}
private int computeGravity() {
//根據(jù)mGravity來(lái)確定gravity
int gravity = mGravity == Gravity.NO_GRAVITY ? Gravity.START | Gravity.TOP : mGravity;
if (mIsDropdown && (mClipToScreen || mClippingEnabled)) {
gravity |= Gravity.DISPLAY_CLIP_VERTICAL;
}
return gravity;
}
而mGravity是可以在外部設(shè)置的:
public void showAtLocation(View parent, int gravity, int x, int y) {d
mParentRootView = new WeakReference<>(parent.getRootView());
showAtLocation(parent.getWindowToken(), gravity, x, y);
}
public void showAtLocation(IBinder token, int gravity, int x, int y) {
//省略...
mGravity = gravity;
//省略
}
因此,可以通過(guò)showAtLocation(xx)設(shè)置PopupWindow的Gravity。
此處需要注意的是:
showAsDropDown(xx)參數(shù)里的gravity指的是PopupWindow與錨點(diǎn)View的對(duì)齊方式。
而showAtLocation(xx)參數(shù)里的gravity才是PopupWindow的Gravity。
Toast 位置確定
Toast 默認(rèn)底部水平居中。在Toast.TN 類里,當(dāng)展示Toast時(shí)調(diào)用handleShow(xx)方法:
public void handleShow(IBinder windowToken) {
//省略
if (mView != mNextView) {
// 省略
//通過(guò)mGravity計(jì)算
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
//x、y的值
mParams.x = mX;
mParams.y = mY;
}
}
而mGravity、mX、mY可以在外部設(shè)置:
public void setGravity(int gravity, int xOffset, int yOffset) {
mTN.mGravity = gravity;
mTN.mX = xOffset;
mTN.mY = yOffset;
}
因此調(diào)用setGravity(xx)可以改變Toast展示的位置
Window外部區(qū)域變暗
Dialog彈出時(shí)外部區(qū)域會(huì)變暗,該效果由以下字段控制
WindowManager.LayoutParams.dimAmount
取值float類型
范圍[0-1]
值越大表示不透明度越高
0表示不變暗,1表示完全變暗
該值需要生效,需要配合另外字段使用:
layoutParams.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
Dialog 外部變暗
protected ViewGroup generateLayout(DecorView decor) {
if (a.getBoolean(R.styleable.Window_backgroundDimEnabled,
mIsFloating)) {
if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
//設(shè)置標(biāo)記,表示支持變暗
params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
}
if (!haveDimAmount()) {
//設(shè)置變暗的具體值
params.dimAmount = a.getFloat(
android.R.styleable.Window_backgroundDimAmount, 0.5f);
}
}
}
可以看出Dialog dimAmount值從style里獲取,該style里的默認(rèn)值是0.6。當(dāng)然我們可以在外部修改dimAmount值。
dialog.setContentView(myGroup);
dialog.getWindow().getAttributes().dimAmount = 0.3f;
dialog.show();
需要注意的是,dimAmount賦值操作需要在setContentView(xx)之后進(jìn)行,否則設(shè)置的值會(huì)被setContentView(xx)重置。
PopupWindow和Toast 沒(méi)有對(duì)此設(shè)置相應(yīng)的值,因此就沒(méi)有外部區(qū)域變暗的說(shuō)法。
Window touch/key 事件
Dialog 事件接收
點(diǎn)擊Dialog 外部時(shí)(touch),Dialog消失;點(diǎn)擊物理返回鍵時(shí)(key),Dialog消失。因此我們可以猜測(cè)出Dialog是接收到了touch/key事件,并判斷如果touch事件在Window外部,那么關(guān)閉Dialog。
涉及到兩個(gè)步驟:
1、能接收到外部touch/key 事件
2、對(duì)事件進(jìn)行相應(yīng)的處理(是否關(guān)閉Dialog)
1、設(shè)置Dialog能否接收touch/key 事件
Window 默認(rèn)接收外部點(diǎn)擊事件和key事件,Dialog沒(méi)有更改此默認(rèn)值,因此能接收到touch/key 事件。
2、對(duì)接收的事件做處理
Dialog 實(shí)現(xiàn)了Window.Callback 接口,重寫(xiě)方法里對(duì)touch事件做處理
#Dialog.java
public boolean dispatchTouchEvent(@android.annotation.NonNull MotionEvent ev) {
//先交給Dialog可見(jiàn)區(qū)域處理
if (mWindow.superDispatchTouchEvent(ev)) {
return true;
}
//事件沒(méi)消費(fèi),繼續(xù)處理
return onTouchEvent(ev);
}
public boolean onTouchEvent(@android.annotation.NonNull MotionEvent event) {
//shouldCloseOnTouch(xx)
//該方法判斷是否是up事件且是否點(diǎn)擊在Dialog外部區(qū)域且是否設(shè)置了可以關(guān)閉Dialog的標(biāo)記
//都滿足,則返回true
if (mCancelable && mShowing && mWindow.shouldCloseOnTouch(mContext, event)) {
//符合條件,則關(guān)閉Dialog
cancel();
return true;
}
return false;
}
同樣的,Dialog 實(shí)現(xiàn)了KeyEvent.Callback,重寫(xiě)方法里對(duì)key事件做處理
#Dialog.java
public boolean dispatchKeyEvent(@android.annotation.NonNull KeyEvent event) {
if ((mOnKeyListener != null) && (mOnKeyListener.onKey(this, event.getKeyCode(), event))) {
return true;
}
//可見(jiàn)區(qū)域做處理
if (mWindow.superDispatchKeyEvent(event)) {
return true;
}
//繼續(xù)分發(fā)
return event.dispatch(this, mDecor != null
? mDecor.getKeyDispatcherState() : null, this);
}
public boolean onKeyUp(int keyCode, @android.annotation.NonNull KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE)
&& event.isTracking()
&& !event.isCanceled()) {
onBackPressed();
return true;
}
return false;
}
public void onBackPressed() {
//標(biāo)記生效,則移除Dialog
if (mCancelable) {
cancel();
}
}
從上面可以看出,Dialog點(diǎn)擊外部和點(diǎn)擊物理返回鍵消失需要同時(shí)滿足兩個(gè)條件,那么想要Dialog不消失,只要不滿足其中某個(gè)條件即可。實(shí)際上Dialog是根據(jù)第二個(gè)條件設(shè)置標(biāo)記位,已經(jīng)為我們封裝好了方法:
點(diǎn)擊外部不消失:
dialog.setCanceledOnTouchOutside(false);
點(diǎn)擊物理返回鍵不消失:
dialog.setCancelable(false);
值得注意的是:調(diào)用了上述方法,Dialog還是接收了事件,只是不關(guān)閉Dialog而已。事件并沒(méi)有分發(fā)到其底下的Window。
PopupWindow 事件接收
與Dialog類似,看其是否滿足兩個(gè)條件。
先來(lái)看看PopupWindow 調(diào)用棧:
showAsDropDown(xx)->createPopupLayoutParams(xx)->computeFlags(xx)
#PopupWindow.java
private int computeFlags(int curFlags) {
//省略
if (!mFocusable) {
//焦點(diǎn)功能沒(méi)開(kāi)啟,則標(biāo)記FLAG_NOT_FOCUSABLE
//該標(biāo)記下,Window不接收其外部區(qū)域的touch事件
//也不接收key事件
curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
if (mInputMethodMode == INPUT_METHOD_NEEDED) {
//鍵盤(pán)相關(guān)
curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
}
} else if (mInputMethodMode == INPUT_METHOD_NOT_NEEDED) {
curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
}
//省略
}
computeFlags(xx)計(jì)算WindowManager.LayoutParams.flags的值。PopupWindow是否接收事件取決于"mFocusable",在我們的demo里并沒(méi)有對(duì)該值進(jìn)行設(shè)置,默認(rèn)為false,因此PopupWindow不能接收外部點(diǎn)擊事件與key事件,當(dāng)然也就不能處理是否關(guān)閉PopupWindow的邏輯了。
而"mFocusable"字段的賦值可以在PopupWindow構(gòu)造函數(shù)里指定或者調(diào)用
public void setFocusable(boolean focusable)
當(dāng)指定focusable=true時(shí),PopupWindow就能接收touch/key事件了,PopupDecorView 負(fù)責(zé)接收事件處理:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//onTouch 優(yōu)先執(zhí)行
if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
return true;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
//接收Down事件關(guān)閉
if ((event.getAction() == MotionEvent.ACTION_DOWN)
&& ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
dismiss();
return true;
} else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
//另一類事件
dismiss();
return true;
} else {
return super.onTouchEvent(event);
}
}
key事件差不多,此處略過(guò)。
總結(jié)來(lái)說(shuō):
設(shè)置focusable為true即可點(diǎn)擊外部消失PopupWindow,反之則不消失
網(wǎng)上一些文章說(shuō)的是PopupWindow 會(huì)阻塞程序,這種觀點(diǎn)是錯(cuò)誤的。實(shí)際上是下一層的Window(Activity)沒(méi)有接收到事件,當(dāng)然不會(huì)做任何處理了
Toast 事件接收
Toast 一般用來(lái)定時(shí)展示一個(gè)文本,因此一般無(wú)需接收事件。
在Toast 構(gòu)造函數(shù)里,會(huì)構(gòu)造TN對(duì)象,該對(duì)象里初始化WindowManager.LayoutParams.flags參數(shù):
TN(String packageName, @android.annotation.Nullable Looper looper) {
final WindowManager.LayoutParams params = mParams;
//省略
params.setTitle("Toast");
//設(shè)置不接收外部的touch事件和key事件
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
//省略
}
關(guān)于Window touch/key 事件詳細(xì)字段內(nèi)容請(qǐng)移步:Window/WindowManager 不可不知之事
本篇只說(shuō)明設(shè)置了哪些參數(shù)。
啟動(dòng)Dialog/PopupWindow/Toast 所需的Context限制
請(qǐng)移步:Android各種Context的前世今生
Window 動(dòng)畫(huà)
控制Window 動(dòng)畫(huà)的字段是:
WindowManager.LayoutParams.windowAnimations
Dialog 動(dòng)畫(huà)
Dialog 默認(rèn)動(dòng)畫(huà):
<style name="Animation.Dialog">
<item name="windowEnterAnimation">@anim/dialog_enter</item>
<item name="windowExitAnimation">@anim/dialog_exit</item>
</style>
替換Dialog默認(rèn)動(dòng)畫(huà),定義Style
<style name="myAnim">
<item name="android:windowEnterAnimation">@anim/myanim</item>
</style>
<style name="myDialog" parent="myTheme">
<item name="android:windowAnimationStyle">@style/myAnim</item>
</style>
Dialog 構(gòu)造函數(shù)引用該Style。
當(dāng)然也可以單獨(dú)設(shè)置
dialog.getWindow().getAttributes().windowAnimations = R.style.myAnim;
PopupWindow 動(dòng)畫(huà)
PopupWindow 默認(rèn)沒(méi)有動(dòng)畫(huà),其加載動(dòng)畫(huà)時(shí)機(jī):
createPopupLayoutParams(xx)->computeAnimationResource(xx)
在外部指定其動(dòng)畫(huà):
public void setAnimationStyle(int animationStyle) {
mAnimationStyle = animationStyle;
}
popupWindow.setAnimationStyle(R.style.myAnim);
Toast 動(dòng)畫(huà)
在Toast.TN的構(gòu)造函數(shù)里,有默認(rèn)動(dòng)畫(huà):
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
<style name="Animation.Toast">
<item name="windowEnterAnimation">@anim/toast_enter</item>
<item name="windowExitAnimation">@anim/toast_exit</item>
</style>
Toast 沒(méi)有提供對(duì)外接口設(shè)置Window動(dòng)畫(huà)。
Dialog/PopupWindow/Toast 使用場(chǎng)合
從上邊分析可以看出,造成Window表現(xiàn)差異的實(shí)際上就是WindowManager.LayoutParams 參數(shù)的差異。因此重點(diǎn)是我們能否拿到WindowManager.LayoutParams對(duì)象。
對(duì)于Dialog:
可以通過(guò)dialog.getWindow().getAttributes() 獲取WindowManager.LayoutParams對(duì)象,對(duì)象獲取到了那么里邊的各種參數(shù)就可以設(shè)置了。
需要注意的是:setContentView(xx)可能會(huì)重置LayoutParams里的一些參數(shù),因此一般我們更改LayoutParams參數(shù)最好在setContentView(xx)之后。
對(duì)于PopupWindow/Toast
這兩者并沒(méi)有提供方法獲取WindowManager.LayoutParams對(duì)象,僅僅提供一些方法單獨(dú)設(shè)置WindowManager.LayoutParams對(duì)象里的一些變量。比如設(shè)置Window的位置、設(shè)置touch/key 事件接收、動(dòng)畫(huà)等。
使用建議
1、對(duì)于想要設(shè)置背景蒙層的,建議使用Dialog。PopupWindow/Toast并沒(méi)有提供方法設(shè)置該參數(shù)
2、對(duì)于想要基于某個(gè)錨點(diǎn)(View)位置展示W(wǎng)indow的,建議使用PopupWindow。當(dāng)然Dialog/Toast也是可以指定位置,只是PopupWindow已經(jīng)將這套封裝了,不用重復(fù)造輪子
3、對(duì)于想要監(jiān)聽(tīng)外部touch/key 事件的,建議使用Dialog;Dialog重寫(xiě)touch/key比較方便。
4、對(duì)于想要簡(jiǎn)單彈出提示,并且有時(shí)長(zhǎng)限制的,建議使用Toast。
如若對(duì)Dialog/PopupWindow/Toast 都不能解決你的需求,那就更容易了。這三者都是封裝了WindowManager的操作,我們直接使用原生的WindowManager,能拿到所有參數(shù),想要啥效果都可以設(shè)置。
Dialog/PopupWindow/Toast 默認(rèn)動(dòng)畫(huà)都是用了系統(tǒng)的屬性,對(duì)styleable/style/attr 有疑問(wèn)的,請(qǐng)移步:
全網(wǎng)最深入 Android Style/Theme/Attr/Styleable/TypedArray 清清楚楚明明白白
本文源碼基于Android 10.0