一、問題發現
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在屏幕中的位置。