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在屏幕中的位置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章