说是不完全,一是觉得自己语言表述不完美,二是觉得没有方方面面都分析到。
首先我们知道以下知识:
setCancelable(false); dialog弹出后会点击屏幕或物理返回键,dialog不消失
setCanceledOnTouchOutside(false); dialog弹出后点击屏幕,dialog不消失,点击物理返回键dialog消失
看了上面的知识后,我们得问凭啥你说不消失就不消失,你说消失就消失?毫无疑问,答案肯定在Dialog的事件分发机制里面。
/**
* Sets whether this dialog is cancelable with the
* {@link KeyEvent#KEYCODE_BACK BACK} key.
*/
public void setCancelable(boolean flag) {
mCancelable = flag;
}
/**
* Sets whether this dialog is canceled when touched outside the window's
* bounds. If setting to true, the dialog is set to be cancelable if not
* already set.
*
* @param cancel Whether the dialog should be canceled when touched outside
* the window.
*/
public void setCanceledOnTouchOutside(boolean cancel) {
if (cancel && !mCancelable) {
mCancelable = true;
}
mWindow.setCloseOnTouchOutside(cancel);
}
上面的两个方法都涉及到mCancelable,而onTouchEvent恰好也有这个变量,应该不能说恰好,应该说必然。
类似于Activity的dispatchTouchEvent方法,Dialog也有一个window窗口对象,窗口里面肯定也有DecorView这个根View。
从下面Dialog的dispatchTouchEvent方法看,要么DecorView可以消费掉这个触摸事件,不然就得给Dialog的onTouchEvent方法处理。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mWindow.superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
当按下物理返回键时,执行onBackPressed方法,该方法会进行判断mCancelable的真假,当mCancelable为假时,就不做任何处理了。而mCancelable为真时,继续执行cancel方法,取消dialog的显示。默认创建的dialog对象时,mCancelable为真。下面的代码换言之,按下物理返回键,是否取消Dialog的显示,淹看mCancelable是否为真。
/**
* Called when the dialog has detected the user's press of the back
* key. The default implementation simply cancels the dialog (only if
* it is cancelable), but you can override this to do whatever you want.
*/
// 物理返回键的回调方法
public void onBackPressed() {
if (mCancelable) {
cancel();// 取消dialog的显示
}
}
我们可以看一下cancel这个方法究竟干了什么。
可以看到,Dialog消失是肯定的,但是当mCanceled为假(mCanceled默认就是假)且mCancelMessage不为null时,可以发送出去一个消息,用于对此情况的处理,开发者可根据需要自由设置逻辑。
接着看mWindow.shouldCloseOnTouch方法都干了啥。
/**
* Cancel the dialog. This is essentially the same as calling {@link #dismiss()}, but it will
* also call your {@link DialogInterface.OnCancelListener} (if registered).
*/
public void cancel() {
if (!mCanceled && mCancelMessage != null) {
mCanceled = true;
// Obtain a new message so this dialog can be re-used
// 发送给对应handler一个mCancelMessage消息,告诉它在mCanceled为假的情况下,dialog依然取消显示了
// 可以在对应handler的handlemessage中根据该消息进行一些逻辑设置
// 通过setCancelMessage方法可以传入mCancelMessage
// 如果mCanceled为真,说明Dialog是允许取消显示的,也就不需要发送消息了
Message.obtain(mCancelMessage).sendToTarget();
}
dismiss();
}
/**
* Set a message to be sent when the dialog is canceled.
* @param msg The msg to send when the dialog is canceled.
* @see #setOnCancelListener(android.content.DialogInterface.OnCancelListener)
*/
public void setCancelMessage(final Message msg) {
mCancelMessage = msg;
}
在onTouchEvent方法中,需要满足3个条件,才会执行cancel方法,取消显示。
(1)mCancelable为真
(2)mShowing为真
(3)mWindow.shouldCloseOnTouch(mContext, event)为真
也就是说,当setCancelable(false)后,点击Dialog窗口外面位置,Dialog也不会消失。到此,setCancelable(false)的原因已经解释清楚了。
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen outside
* of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation will cancel the dialog when a touch
* happens outside of the window bounds.
*/
public boolean onTouchEvent(MotionEvent event) {
if (mCancelable && mShowing && mWindow.shouldCloseOnTouch(mContext, event)) {
cancel();
return true;
}
return false;
}
还没结束,setCanceledOnTouchOutside(false)还没分析完呢。
接着上面的代码setCanceledOnTouchOutside源码看,执行mWindow.setCloseOnTouchOutside方法。在Window.java中的源码如下,主要就是设置了一个标志位。因为在mWindow.shouldCloseOnTouch方法中要用到这个标志位。
/** @hide */
public void setCloseOnTouchOutside(boolean close) {
mCloseOnTouchOutside = close;
mSetCloseOnTouchOutside = true;
}
接着看mWindow.shouldCloseOnTouch方法都干了啥。
/** @hide */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
// 核心就是判断
// 判断mCloseOnTouchOutside标记是否为ACTION_DOWN事件
// 同时判断event的x,y坐标是不是超出Bounds
// 然后检查FrameLayout的content的id的DecorView是否为空
// 此处的处理对于window边界之外的Touch事件有判断价值
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
}
当setCanceledOnTouchOutside(false)后,mCloseOnTouchOutside也就是false,那么在Dialog的onTouchEvent方法里,也就因为不满足条件,从而不执行cancel了。
上面代码中要mWindow.shouldCloseOnTouch返回true,要满足四个条件:
(1)mCloseOnTouchOutside为真
(2)DOWN事件
(3)isOutOfBounds返回真
(4)peekDecorView()不为null
要isOutOfBounds返回真,则DOWN事件的触摸点在Dialog的外面。
// 判断是否超出DecorView的范围
private boolean isOutOfBounds(Context context, MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop();
final View decorView = getDecorView();
return (x < -slop) || (y < -slop)
|| (x > (decorView.getWidth()+slop))
|| (y > (decorView.getHeight()+slop));
}
综上所述,
Dialog的onTouchEvent方法是针对在Dialog窗口外的触摸点的事件处理,即是否要取消Dialog的显示。
setCancelable(false)是让Dialog类里的一个标志位为false,onTouchEvent和onBackPressed都会对该标志位进行真假判断,如果为假,就不会执行cancel方法;
setCanceledOnTouchOutside(false)是让Dialog所对应的window窗口里的一个标志位为false,onTouchEvent也会对该window窗口的这个标志位进行判断,如果为false,则不会执行cancel方法。