[TOC]
打开一个带有输入框的Activity、Fragment、Dialog,自动弹出软键盘,这是很常见的需求场景。但是就这么一个场景,你真的弹对了吗?
常用方案梳理
Activity
Activity只需要2步
1 在清单文件中添加如下代码:
android:windowSoftInputMode="stateVisible"
2 布局文件中添加requestFocus
<EditText
android:layout_width="match_parent"
android:inputType="text"
android:layout_height="wrap_content">
<requestFocus />
</EditText>
Fragment Dialog
对于Fragment|Dialog,可以使用如下方式
第一种:
editText.post(new Runnable() {
@Override
public void run() {
Util.showKeyboard(editText);
}
});
第二种:
在第一种的基础上加一个延迟时间,即使用postDelayd方法
第三种:
Looper.myQueue().addIdleHandler(new IdleHandler() {
@Override
public boolean queueIdle() {
Util.showKeyboard(editText);
return false;
}
});
其中,showKeyboard方法如下:
public static void showKeyboard(View vFocus) {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(vFocus, InputMethodManager.SHOW_IMPLICIT);
}
若无法弹出软键盘,检查xml文件是否添加了
<requestFocus />。或者在代码中调用editText.requestFocus()方法
第一种方式在api30(即android 11)上无法弹出
第二种存在一定体验上的问题,并且不够优雅,延迟时间不好把握
第三种有一定概率弹不出
那么疑问来了,有没有百分百并且及时弹出的方式呢:yiw:
我这刨根问底的心
首先要弄明白,为什么在页面启动时,直接调用Util.showKeyboard(editText));压根不会弹出软键盘
为了找到答案,查源码吧!以api30为例
InputMethodManager#showSoftInput
public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
...
checkFocus();
synchronized (mH) {
if (!hasServedByInputMethodLocked(view)) {
return false;
}
try {
//弹出逻辑
return mService.showSoftInput(
mClient, view.getWindowToken(), flags, resultReceiver);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
通过调试发现,hasServedByInputMethodLocked(view)方法返回false,导致软键盘弹出逻辑没有执行,该方法如下:
/**
* Returns {@code true} when the given view has been served by Input Method.
*/
private boolean hasServedByInputMethodLocked(View view) {
final View servedView = getServedViewLocked();
return (servedView == view
|| (servedView != null && servedView.checkInputConnectionProxy(view)));
}
这个方法的作用是判断参数view是否是当前被服务的view。换言之,只有被服务的view才会弹出软键盘
既然被服务的view才会弹软键盘,那这个view怎么获取呢:yiw:
下面看看getServedViewLocked方法:
private View getServedViewLocked() {
return mCurRootView != null ? mCurRootView.getImeFocusController().getServedView() : null;
}
mCurRootView是当前window所拥有的ViewRootImpl对象。
下面看看ImeFocusController#getServedView
public View getServedView() {
return mServedView;
}
这里保存的mServedView便是当前被InputMethodManager所服务的view。mServedView在如下方法中被赋值:
public boolean checkFocus(boolean forceNewFocus, boolean startInput) {
final InputMethodManagerDelegate immDelegate = getImmDelegate();
//注意这个条件,mServedView只能是当前window中存在的view
if (!immDelegate.isCurrentRootView(mViewRootImpl)
|| (mServedView == mNextServedView && !forceNewFocus)) {
return false;
}
...
mServedView = mNextServedView;
...
return true;
}
在该方法中,mServedView被赋值为mNextServedView。而mNextServedView在如下方法被赋值:
void onViewFocusChanged(View view, boolean hasFocus) {
...
//同样mNextServedView只能是当前window中存在的view
if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) {
return;
}
...
if (hasFocus) {
mNextServedView = view;
}
//会调用checkFocus
mViewRootImpl.dispatchCheckFocus();
...
}
onViewFocusChanged在如下方法中被调用
void onPostWindowFocus(View focusedView, boolean hasWindowFocus,
WindowManager.LayoutParams windowAttribute) {
...
// Update mNextServedView when focusedView changed.
final View viewForWindowFocus = focusedView != null ? focusedView : mViewRootImpl.mView;
onViewFocusChanged(viewForWindowFocus, true);
...
immDelegate.startInputAsyncOnWindowFocusGain(viewForWindowFocus,
windowAttribute.softInputMode, windowAttribute.flags, forceFocus);
}
startInputAsyncOnWindowFocusGain(留意一下这个方法,下一篇盘它)会调用前面的checkFocus方法,这样就完成了mServedView的赋值
看到onPostWindowFocus,自然会想到onPreWindowFocus,这个方法很简单,用来修改当前服务的窗口,即前面提到的mCurRootView:
void onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) {
if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
return;
}
if (hasWindowFocus) {
getImmDelegate().setCurrentRootView(mViewRootImpl);
}
}
ViewRootImpl与ImeFocusController一一对应
getImmDelegate获取到的对象是InputMethodManager的内部类
onPreWindowFocus和onPostWindowFocus在ViewRootImpl#handleWindowFocusChanged中调用:
private void handleWindowFocusChanged() {
...
//注意这个标志位,标识当前view所在的window是否获取焦点
mAttachInfo.mHasWindowFocus = hasWindowFocus;
mImeFocusController.updateImeFocusable(mWindowAttributes, true /* force */);
mImeFocusController.onPreWindowFocus(hasWindowFocus, mWindowAttributes);
if (mView != null) {
mAttachInfo.mKeyDispatchState.reset();
mView.dispatchWindowFocusChanged(hasWindowFocus);
mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus);
if (mAttachInfo.mTooltipHost != null) {
mAttachInfo.mTooltipHost.hideTooltip();
}
}
// Note: must be done after the focus change callbacks,
// so all of the view state is set up correctly.
mImeFocusController.onPostWindowFocus(mView.findFocus(), hasWindowFocus,
mWindowAttributes);
...
}
到这里已经可以得到弹出软键盘的最佳时机了
前面我们已经知道onPostWindowFocus会设置好mServedView,那么我们只需要在该方法调用后,弹软键盘就可以了
但从源码看,调用该方法之后没有很好的点让我们做弹软键盘的操作,那么是否可以在该方法调用之前找到一个时机,通过post弹出软键盘呢?这样既可以保证一定会弹,也可以保证足够及时
可以看到,在onPostWindowFocus调用之前,有两个点可以考虑:
-
mView.dispatchWindowFocusChanged事件 -
mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange事件
dispatchWindowFocusChanged事件
该事件最终会调用View#onWindowFocusChanged方法,我们可以通过继承EditText来覆写该方法,做一个监听器出来:
public class ListenFocusEditText extends EditText {
private OnWindowFocusChangeListener windowFocusChangeListener;
public ListenFocusEditText(Context context) {
super(context);
}
public ListenFocusEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ListenFocusEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (windowFocusChangeListener != null) {
windowFocusChangeListener.onWindowFocusChanged(hasWindowFocus);
}
}
public void setWindowFocusChangeListener(
OnWindowFocusChangeListener windowFocusChangeListener) {
this.windowFocusChangeListener = windowFocusChangeListener;
}
public interface OnWindowFocusChangeListener {
void onWindowFocusChanged(boolean hasFocus);
}
}
然后如下调用:
editText.setWindowFocusChangeListener(new OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus) {
vEdit.setWindowFocusChangeListener(null);
vEdit.post(new Runnable() {
@Override
public void run() {
Util.showKeyboard(editText);
}
});
}
}
});
dispatchOnWindowFocusChange事件
这个事件就简单多了,注册ViewTreeObserver的监听器即可:
editText.getViewTreeObserver()
.addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus) {
vEdit.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
vEdit.post(new Runnable() {
@Override
public void run() {
Util.showKeyboard(editText);
}
});
}
}
});
不过很不幸:ll:,这个监听器要求api>=18
通过以上分析,我们已经掌握了及时、准确无误的弹出软键盘的方式
下面区分一下window的focus和view的focus
window focus VS view focus
window focus
前面已经介绍
-
InputMethodManager#mCurRootView保存当前获取焦点的window的ViewRootImpl对象 - window focus变更时,会触发
View#onWindowFocusChanged,以及ViewTreeObserver$OnWindowFocusChangeListener#onWindowFocusChanged
除此之外,还有
-
View#hasWindowFocus判断该view所在的window是否获取了焦点
public boolean hasWindowFocus() {
//mHasWindowFocus前面已经见过
return mAttachInfo != null && mAttachInfo.mHasWindowFocus;
}
view focus
当调用了View#requestFocus方法后,最终会调用到handleFocusGainInternal:
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
...
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
//设置focus标志位
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
if (mParent != null) {
mParent.requestChildFocus(this, this);
updateFocusedInCluster(oldFocus, direction);
}
if (mAttachInfo != null) {
//ViewTreeObserver的view focus变更事件
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
}
}
注意ViewTreeObserver#dispatchOnGlobalFocusChange分发的是view focus变更事件,window focus变更时,不会触发该事件
mParent.requestChildFocus代码:
@Override
public void requestChildFocus(View child, View focused) {
...
// Unfocus us, if necessary
super.unFocus(focused);
// We had a previous notion of who had focus. Clear it.
if (mFocused != child) {
if (mFocused != null) {
//清除当前获得焦点的view的焦点
mFocused.unFocus(focused);
}
mFocused = child;
}
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
onFocusChanged方法代码:
protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
@Nullable Rect previouslyFocusedRect) {
...
if (!gainFocus) {
...
} else if (hasWindowFocus()) {
//注意这里
notifyFocusChangeToImeFocusController(true /* hasFocus */);
}
invalidate(true);
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnFocusChangeListener != null) {
//分发监听器事件
li.mOnFocusChangeListener.onFocusChange(this, gainFocus);
}
...
}
其中notifyFocusChangeToImeFocusController方法:
private void notifyFocusChangeToImeFocusController(boolean hasFocus) {
if (mAttachInfo == null) {
return;
}
mAttachInfo.mViewRootImpl.getImeFocusController().onViewFocusChanged(this, hasFocus);
}
这里就进入了mServedView的赋值逻辑
需要注意的是,onViewFocusChanged->ViewRootImpl#dispatchCheckFocus会通过handler消息来调用ImeFocusController#checkFocus,从而更新mServedView
那么在当前window获得焦点的情况下,调用如下代码直接弹软键盘是不是就不可取了呢?
editText.requestFocus();
Util.showKeyboard(editText);
实际是可以弹出来的,因为InputMethodManager#showSoftInput中也调用了checkFocus方法
完








网友评论