android软键盘,你真的弹出来了吗(一)

2025-04-13  本文已影响0人  sollian

[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的内部类

onPreWindowFocusonPostWindowFocusViewRootImpl#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调用之前,有两个点可以考虑:

  1. mView.dispatchWindowFocusChanged事件
  2. 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

前面已经介绍

  1. InputMethodManager#mCurRootView保存当前获取焦点的window的ViewRootImpl对象
  2. window focus变更时,会触发View#onWindowFocusChanged,以及ViewTreeObserver$OnWindowFocusChangeListener#onWindowFocusChanged

除此之外,还有

  1. 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方法

上一篇 下一篇

猜你喜欢

热点阅读