Dialog 中 Window 加载显示流程

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,不会移除之前的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值