一、问题发现
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在屏幕中的位置。