Dialog 经常被用到,我们看看它的构造方法
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == 0) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
创建 Dialog 时,可以传入 R.style.Dialog 自定义类型 themeResId,在 if (createContextThemeWrapper) 中,会把它的类型添加进去,比如弹框半透明或全透明等等,这里稍后分析,这里先知道有这么回事就行,这里把 themeResId 包裹一层,生成一个新的 Context;往下看,通过 getSystemService(Context.WINDOW_SERVICE) 获取 WindowManager 实际为
WindowManagerImpl 类型,注意下面,new 了一个 PhoneWindow,这里使用了 Window 来标识,这里用到了多态,然后就是设置回调,这里用的是 this,是因为 Dialog 本身实现了 Window.Callback、Window.OnWindowDismissedCallback 等接口;注意 w.setWindowManager(mWindowManager, null, null) 这行代码,它调用到 Window 中的方法,最终调用
Window:
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated
|| SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
这个方法上一章提到过,看最后一行,这里通过系统提供的 WindowManagerImpl,重新创建了一个新的 WindowManagerImpl,到这基本可以判断,Dialog 和 Activity 一样,每个 Dialog 都对应一个 WindowManagerImpl; 最后一行 mListenersHandler = new ListenersHandler(this),创建了一个 Handler,用来切换线程,注意,这里创建的 Handler 所用的Looper是创建 Dialog 的线程对应的 Looper,并不一定是UI线程,所以,创建 Dialog 和调用 show() 方法时,保证在UI线程中。 再看看 show() 方法
public void show() {
if (mShowing) {
if (mDecor != null) {
if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
}
mDecor.setVisibility(View.VISIBLE);
}
return;
}
mCanceled = false;
if (!mCreated) {
dispatchOnCreate(null);
}
onStart();
mDecor = mWindow.getDecorView();
if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
final ApplicationInfo info = mContext.getApplicationInfo();
mWindow.setDefaultIcon(info.icon);
mWindow.setDefaultLogo(info.logo);
mActionBar = new WindowDecorActionBar(this);
}
WindowManager.LayoutParams l = mWindow.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
nl.copyFrom(l);
nl.softInputMode |=
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
l = nl;
}
try {
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
} finally {
}
}
方法中第一行 if (mShowing) 判断,默认值为 false,先跳过去,往下看。 mCanceled 也赋值为 false,mCreated 初始值为 false,所以走入 if (!mCreated) 判断中,看看 dispatchOnCreate(null) 方法
void dispatchOnCreate(Bundle savedInstanceState) {
if (!mCreated) {
onCreate(savedInstanceState);
mCreated = true;
}
}
protected void onCreate(Bundle savedInstanceState) {
}
这个对应的就是 Dialog 的 onCreate() 方法,我们一般在它里面设置layout布局,比如 setContentView(R.layout.dia),我们看看它的方法
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}
这里调用到了 window 的方法,即 PhoneWindow 的方法
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
看到这有没有很熟悉的感觉,简直就和上一章介绍 Activity 时一样,其实也就是一样,因为它们都使用了 PhoneWindow 来添加view,在 setContentView() 这个方法中,找到了 id 为 content 的 ViewGroup,然后把我们传进去的 layoutResID 转化为 view,然后添加到 ViewGroup 中,在此就完成了把自己的布局添加到系统 mContentParent 中。 从 Dialog 中的 show() 方法继续看,紧接着是 mDecor = mWindow.getDecorView() 方法,获取 PhoneWindow 的根节点view,
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
...
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
}
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
从这个方法中可以看出,即使我们没有调用 setContentView() 方法,但我们一旦调用 getDecorView() 方法,PhoneWindow 内部还是会去创建 mDecor,new 一个对象,不会报 null。回到 show() 中, 下面是 mActionBar 判断,忽略;看看 WindowManager.LayoutParams l = mWindow.getAttributes() 获取了View大小相关的属性,这个默认值是
private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
注意了,是 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,也就是说,默认的值是铺满全屏的,如果我们想修改它的大小,则改变它的值, Window 对外暴露了个方法
Window
public void setAttributes(WindowManager.LayoutParams a) {
mWindowAttributes.copyFrom(a);
dispatchWindowAttributesChanged(mWindowAttributes);
}
protected void dispatchWindowAttributesChanged(WindowManager.LayoutParams attrs) {
if (mCallback != null) {
mCallback.onWindowAttributesChanged(attrs);
}
}
Dialog
public void onWindowAttributesChanged(WindowManager.LayoutParams params) {
if (mDecor != null) {
mWindowManager.updateViewLayout(mDecor, params);
}
}
setAttributes() 方法中,通过 clone 的形式,把 WindowManager.LayoutParams a 的值,全都拷贝给了 mWindowAttributes;onWindowAttributesChanged() 中用来更新 mDecor 的宽和高,同时,如果想监听 LayoutParam 变化,可以在自己的Dialog 中重写此方法。
继续 show() 方法,接着就是 onStart() 方法,排在 onCreate() 之后。mWindowManager.addView(mDecor, l) 这句话才是开启了把 mDecor 添加到 Window中的步伐,通过 ViewRootImpl 来完成后续的操作,上一章也有介绍,这里就不多分析了,mShowing = true 标记标识; sendShowMessage() 方法是发送一条信息,标识 Dialog 展现了
private void sendShowMessage() {
if (mShowMessage != null) {
Message.obtain(mShowMessage).sendToTarget();
}
}
public void setOnShowListener(OnShowListener listener) {
if (listener != null) {
mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
} else {
mShowMessage = null;
}
}
private static final class ListenersHandler extends Handler {
private WeakReference<DialogInterface> mDialog;
public ListenersHandler(Dialog dialog) {
mDialog = new WeakReference<DialogInterface>(dialog);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DISMISS:
((OnDismissListener) msg.obj).onDismiss(mDialog.get());
break;
case CANCEL:
((OnCancelListener) msg.obj).onCancel(mDialog.get());
break;
case SHOW:
((OnShowListener) msg.obj).onShow(mDialog.get());
break;
}
}
}
setOnShowListener() 方法是设置的监听,是 Dialog show的时候回调的,在这个方法中,把 OnShowListener 回调封装到 Message中,赋值给 mShowMessage,然后调用 sendShowMessage() 方法时,把 mShowMessage 发送给 ListenersHandler,ListenersHandler接收后,通过 ((OnShowListener) msg.obj).onShow(mDialog.get()) 来触发
OnShowListener 回调,达到目的;同理,Dialog 消失和取消的监听,也是这样。 show() 方法下面就有一个 hide() 方法
public void hide() {
if (mDecor != null) {
mDecor.setVisibility(View.GONE);
}
}
如果调用了,Dialog 会隐藏,并不会消失;这时候重新看 show() 方法开头的 if (mShowing) 判断,这时候如果再次调用 show() 方法,mDecor.setVisibility(View.VISIBLE) 因此 Dialog 就展现了。 show() 方法分析完了,对应的是 dismiss() 方法
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}
private final Runnable mDismissAction = new Runnable() {
public void run() {
dismissDialog();
}
};
void dismissDialog() {
if (mDecor == null || !mShowing) {
return;
}
if (mWindow.isDestroyed()) {
return;
}
try {
mWindowManager.removeViewImmediate(mDecor);
} finally {
if (mActionMode != null) {
mActionMode.finish();
}
mDecor = null;
mWindow.closeAllPanels();
onStop();
mShowing = false;
sendDismissMessage();
}
}
dismiss() 方法中会先判断当前线程是否为UI线程,如果是,执行 dismissDialog() 方法,如果不是,通过 mHandler 执行 mDismissAction 方法,它里面也是 dismissDialog() 方法,总之都是在UI线程中执行 dismissDialog() 方法,显示非空判断,然后是 Window 的生命周期判断
Window:
public final void destroy() {
mDestroyed = true;
}
public final boolean isDestroyed() {
return mDestroyed;
}
这两个方法,尤其是 destroy() 方法,应该是Window被回移除时所调用。 继续 dismissDialog() 方法,mWindowManager.removeViewImmediate(mDecor) 这行代码是移除 mDecor,上一章也分析过,然后是 mDecor = null 置空,mShowing = false 标识属性,onStop() 方法我们一般可以重写,做些资源释放回收的操作,最后就是 sendDismissMessage() 方法,这个与 Dialog 展示的时候监听的道理一样,只要设置的回到,这里就会触发。cancel() 方法则有些取巧了
public void cancel() {
if (!mCanceled && mCancelMessage != null) {
mCanceled = true;
Message.obtain(mCancelMessage).sendToTarget();
}
dismiss();
}
这里最终调用还是 dismiss() 方法,前面有个判断, mCanceled 默认为 false,调用 show() 方法后,也会赋值为 false,mCancelMessage 为设置的取消回调,只有这两个条件都满足,才会触发回调机制。
至于屏蔽返回键和点击Dialog中layout布局外面的蒙层,Dialog 不消失,则设置下面两个属性即可
setCancelable(false);
setCanceledOnTouchOutside(false);
看看它们的源码
public void setCancelable(boolean flag) {
mCancelable = flag;
}
回退键按钮的方法
public void onBackPressed() {
if (mCancelable) {
cancel();
}
}
由于 setCancelable(false) 设置 mCancelable 为 false,则 onBackPressed() 方法中,不会走进 if 语句判断,所以 Dialog 不会消失。 setCanceledOnTouchOutside() 方法就比较有意思了
public void setCanceledOnTouchOutside(boolean cancel) {
if (cancel && !mCancelable) {
mCancelable = true;
}
mWindow.setCloseOnTouchOutside(cancel);
}
如果设置 setCanceledOnTouchOutside(false),则触摸Dialog四周也不会消失,但如果 setCancelable(false),setCanceledOnTouchOutside(true) ,这时候屏蔽回退按钮失效,点击返回键 Dialog依然可以消失,因为 mCancelable 属性置为 true 了;下面分析一下为什么Dialog四周会产生作用,Dialog的触摸事件,是通过 PhoneWindow 中的 DecorView 的
dispatchTouchEvent() 最早接收,然后通过 getCallback() 传递给 Dialog 中的 dispatchTouchEvent() 方法
Dialog:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mWindow.superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
PhoneWindow:
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
在这里传递给了根节点 mDecor,触摸事件开启传递,如果是在 layout 四周的触摸事件,则这里面不会消耗,最终执行 Dialog 的 onTouchEvent(ev) 方法
public boolean onTouchEvent(MotionEvent event) {
if (mCancelable && mShowing && mWindow.shouldCloseOnTouch(mContext, event)) {
cancel();
return true;
}
return false;
}
看看 if 判断语句,先会判断 setCancelable() 设置的 mCancelable 属性,然后是mShowing, 重点是 mWindow.shouldCloseOnTouch(mContext, event) 方法,只有三个都满足才会把 Dialog取消,也就是说只要设置一个 setCancelable(false),同样能屏幕触摸四周dialog消失的功能;绕回 shouldCloseOnTouch() 方法,我们知道,setCanceledOnTouchOutside() 方法中有mWindow.setCloseOnTouchOutside(cancel) 设置,看看他们几个的方法
Window:
public void setCloseOnTouchOutside(boolean close) {
mCloseOnTouchOutside = close;
mSetCloseOnTouchOutside = true;
}
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
}
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));
}
PhoneWindow:
public final View peekDecorView() {
return mDecor;
}
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
setCloseOnTouchOutside() 方法设置 mCloseOnTouchOutside 为true后,mWindow.shouldCloseOnTouch(mContext, event) 方法中,手指头按下时,它的 if 判断语句前两个条件满足,看看后面两个,isOutOfBounds() 方法是判断手指头按在屏幕上的位置, ViewConfiguration.get(context).getScaledWindowTouchSlop() 获取的值是 WINDOW_TOUCH_SLOP = 16,
event.getX() 获取的是 点击事件距离控件左边,即视图坐标,最后判断意思是在view的四周,也就是出了view的范围,所以也为 true,最后一个是获取的 mDecor 不为 null,满足这些条件,则触摸layout四周的范围, onTouchEvent() 方法中 if 语句满足,执行 cancel() 方法,故 Dialog 消失。
我们在 Dialog 中设置自己布局id时,可以在 Dialog 的 onCreate() 中设置,也可以在外面设置,故此就看看设置布局的方法
Dialog:
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}
public void setContentView(View view) {
mWindow.setContentView(view);
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
mWindow.setContentView(view, params);
}
public void addContentView(View view, ViewGroup.LayoutParams params) {
mWindow.addContentView(view, params);
}
他们四个对应 PhoneWindow 中的四个方法
PhoneWindow:
public void setContentView(int layoutResID) {
...
mContentParent.removeAllViews();
mContentParent.addView(view, params);
...
}
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
...
mContentParent.removeAllViews();
mContentParent.addView(view, params);
...
}
public void addContentView(View view, ViewGroup.LayoutParams params) {
...
mContentParent.addView(view, params);
...
}
这是简化后的代码,上面三个 setContentView() 方法意思一样,调用时,都会把 mContentParent 中的子view移除后再添加新的view布局,每次都会替换之前的;而 addContentView()方法则是每次都是往 mContentParent 里面添加view,不会移除之前的。