DialogFragment的onDismiss()中为何无法获取到View的位置

一、问题发现

Android有一种切换输入法显示和隐藏的方法

val mInputManager = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
mInputManager?.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)

如果输入法显示时,执行该语句块,会隐藏输入法;如果输入法隐藏时,执行该语句块,则会显示输入法。
在完成以下功能时,遇到问题:
在这里插入图片描述

如果想在对话框隐藏时同时隐藏输入法,直观想法是在DialogFragment的onDismiss()中回调中调用上面的语句块。但是有可能在DialogFragment消失前用户已经关闭输入法了,此时会导致输入法显示。
在这里插入图片描述
针对这个问题,我的想法是在onDismiss()中判断输入法是否显示,如果显示,才执行上面的语句块。
但如何判断输入法是否显示呢?可以通过DialogFragment的位置来确定。可点击空白区域隐藏DialogFragment情况下,onDismiss()回调中无论通过View.getLocationOnScreen()或getGlobalVisibleRect()都无法获取到Dialog的View的位置,经过调试发现,此时Dialog.isShowing()为false。这是为什么呢?

二、原因

2.1 先看下DialogFragment的代码,说下DialogFragment中Dialog的创建、布局设置、显示和隐藏。
2.1.1 Dialog的创建
Dialog mDialog;

DialogFragment中用mDialog变量来保存Dialog对象,它的初始化操作在DialogFragment的onCreateDialog(Bundle)方法中

    @NonNull
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        return new Dialog(getActivity(), getTheme());
    }

该方法被调用的地方在DialogFragment的onGetLayoutInflater(Bundle)中

    @Override
    @NonNull
    public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) {
        if (!mShowsDialog) {
            return super.onGetLayoutInflater(savedInstanceState);
        }
        mDialog = onCreateDialog(savedInstanceState);

        if (mDialog != null) {
            setupDialog(mDialog, mStyle);

            return (LayoutInflater) mDialog.getContext().getSystemService(
                    Context.LAYOUT_INFLATER_SERVICE);
        }
        return (LayoutInflater) mHost.getContext().getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
    }

由于显示对话框,所以mShowsDialog为true,这种情况下,onGetLayoutInflater()方法会创建Dialog,并以此获取LayoutInflater对象。
onGetLayoutInflater()被调用的地方在Fragment中的performGetLayoutInflater(Bundle)中

    @NonNull
    LayoutInflater performGetLayoutInflater(@Nullable Bundle savedInstanceState) {
        LayoutInflater layoutInflater = onGetLayoutInflater(savedInstanceState);
        mLayoutInflater = layoutInflater;
        return mLayoutInflater;
    }

这个方法获取LayoutInflater并初始化mLayoutInflater对象,mLayoutInflater对象会被用到FragmentManager类中的moveToState()方法中,

f.performCreateView(f.performGetLayoutInflater(
                                    f.mSavedFragmentState), container, f.mSavedFragmentState);

,调用了Fragment的performCreateView()方法,

    void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
        ...
        mView = onCreateView(inflater, container, savedInstanceState);
        ...
    }

可以看到熟悉的onCreateView()方法,它的inflater参数是onGetLayoutInflater(Bundle)方法创建的。

2.1.2 Dialog的布局设置

在DialogFragment的onActivityCreate(Bundle)方法中,

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ... // 省略校验代码
        View view = getView();
        if (view != null) {
            ...// 省略校验代码
            mDialog.setContentView(view);
        }
        final Activity activity = getActivity();
        if (activity != null) {
            mDialog.setOwnerActivity(activity);
        }
        mDialog.setCancelable(mCancelable);
        mDialog.setOnCancelListener(this);
        mDialog.setOnDismissListener(this);
        if (savedInstanceState != null) {
            Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
            if (dialogState != null) {
                mDialog.onRestoreInstanceState(dialogState);
            }
        }
    }

可以看到通过DialogFragment将用户定义的布局设置给了Dialog。

2.1.3 Dialog的显示和隐藏

Dialog的显示在DialogFragment的onStart()方法中

    @Override
    public void onStart() {
        super.onStart();

        if (mDialog != null) {
            mViewDestroyed = false;
            mDialog.show();
        }
    }

隐藏在onStop()方法中

    @Override
    public void onStop() {
        super.onStop();
        if (mDialog != null) {
            mDialog.hide();
        }
    }
2.2再看下Dialog点击空白处消失的逻辑

Dialog
在Dialog类中,看下它的dispatchTouchEvent(MotionEvent)方法

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     * 
     * @param ev The touch screen event.
     * 
     * @return boolean Return true if this event was consumed.
     */
    @Override
    public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
        if (mWindow.superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

先跳过mWindow.superDispatchTouchEvent(ev)方法,直接看下onTouchEvent(MotionEvent)方法,

    /**
     * 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(@NonNull MotionEvent event) {
        if (mCancelable && mShowing && mWindow.shouldCloseOnTouch(mContext, event)) {
            cancel();
            return true;
        }
        
        return false;
    }

调用了Window类的shouldCloseOnTouch()方法

    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        final boolean isOutside =
                event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
                || event.getAction() == MotionEvent.ACTION_OUTSIDE;
        if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
            return true;
        }
        return false;
    }

如果ActionDown事件时isOutOfBounds(Context, MotionEvent)会返回true,消耗点击事件。看下isOutOfBounds()方法

    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));
    }

比较好理解,如果点击区域在DecorView外,则会返回true。
回到onTouchEvent()中,会调用Dialog的cancel()方法

    /**
     * Cancel the dialog.  This is essentially the same as calling {@link #dismiss()}, but it will
     * also call your {@link DialogInterface.OnCancelListener} (if registered).
     */
    @Override
    public void cancel() {
        if (!mCanceled && mCancelMessage != null) {
            mCanceled = true;
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mCancelMessage).sendToTarget();
        }
        dismiss();
    }
    /**
     * Dismiss this dialog, removing it from the screen. This method can be
     * invoked safely from any thread.  Note that you should not override this
     * method to do cleanup when the dialog is dismissed, instead implement
     * that in {@link #onStop}.
     */
    @Override
    public void dismiss() {
        if (Looper.myLooper() == mHandler.getLooper()) {
            dismissDialog();
        } else {
            mHandler.post(mDismissAction);
        }
    }
    void dismissDialog() {
        if (mDecor == null || !mShowing) {
            return;
        }

        if (mWindow.isDestroyed()) {
            Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
            return;
        }

        try {
            mWindowManager.removeViewImmediate(mDecor);
        } finally {
            if (mActionMode != null) {
                mActionMode.finish();
            }
            mDecor = null;
            mWindow.closeAllPanels();
            onStop();
            mShowing = false;

            sendDismissMessage();
        }
    }
    private void sendDismissMessage() {
        if (mDismissMessage != null) {
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mDismissMessage).sendToTarget();
        }
    }

一路下来,可以看到点击空白区域时,按照removeView、onStop()、onDismiss()的顺序依次执行,由于onDismiss()回调处,View已经被移除,所以无法获取到View在屏幕中的位置。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值