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

上一篇的补充

Activity#onWindowFocusChanged

我们都知道Activity中有个onWindowFocusChanged的可覆写方法,那它的回调时机是什么时候呢?

上一篇介绍的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);
        ...
}

其中mView就是DecorViewDecorView中覆写了View#onWindowFocusChanged方法:

@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
    super.onWindowFocusChanged(hasWindowFocus);
    ...
    final Window.Callback cb = mWindow.getCallback();
    if (cb != null && !mWindow.isDestroyed() && mFeatureId < 0) {
        cb.onWindowFocusChanged(hasWindowFocus);
    }
   ...
}

这个cb就是Activity或Dialog。Activity#onWindowFocusChanged就是这里调用的

handleWindowFocusChanged调用时机

这个因为涉及到的内容比较多,不做详细介绍。

对Activity而言,该方法的注释有这么一句:

As such, while focus changes will generally have some relation to lifecycle changes (an activity that is stopped will not generally get window focus), you should not rely on any particular order between the callbacks here and those in the other lifecycle methods such as {@link #onResume}.

翻译过来就是不要太依赖于onWindowFocusChanged和生命周期回调方法(比如onResume)之间的顺序。

那么对于Activity,onWindowFocusChangedonResume之间就真的没有顺序可言吗(这里说的顺序不是测试得出的,是代码角度分析的)?
因为其中涉及到一些binder/jni调用,从源码直接分析难度比较大,后面我们会间接验证调用顺序

对于DialogFragment而言,onWindowFocusChanged发生在onResume之后


从onPostWindowFocus谈起

依然基于api30 android11

还是上一篇中的片段ImeFocusController#onPostWindowFocus

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);
}

第一点需要注意的是viewForWindowFocus,若focused window中没有focused view,则默认取DecorView

第二点就是startInputAsyncOnWindowFocusGain函数中的参数windowAttribute.softInputMode。还记得上一篇开头如何在打开Ativity的时候就弹起软键盘的吗?

android:windowSoftInputMode="stateVisible"

没错!这里的windowAttribute.softInputMode就是我们在xml文件或者代码中设置的值

softInputMode如何生效

startInputAsyncOnWindowFocusGain最终调用到InputMethodManager#startInputInner

boolean startInputInner(@StartInputReason int startInputReason,
        @Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
        @SoftInputModeFlags int softInputMode, int windowFlags) {
    ...
            //先留意一下这个值
            mCurrentTextBoxAttribute = tba;
    ...
            final InputBindResult res = mService.startInputOrWindowGainedFocus(
                    startInputReason, mClient, windowGainingFocus, startInputFlags,
                    softInputMode, windowFlags, tba, servedContext, missingMethodFlags,
                    view.getContext().getApplicationInfo().targetSdkVersion);
    ...
}

这里最终调用到InputMethodManagerService#startInputOrWindowGainedFocus
该方法如下:

@NonNull
@Override
public InputBindResult startInputOrWindowGainedFocus(
        @StartInputReason int startInputReason, IInputMethodClient client, IBinder windowToken,
        @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
        int windowFlags, @Nullable EditorInfo attribute, IInputContext inputContext,
        @MissingMethodFlags int missingMethods, int unverifiedTargetSdkVersion) {
    ...
    final InputBindResult result;
    synchronized (mMethodMap) {
        final long ident = Binder.clearCallingIdentity();
        try {
            result = startInputOrWindowGainedFocusInternalLocked(startInputReason, client,
                    windowToken, startInputFlags, softInputMode, windowFlags, attribute,
                    inputContext, missingMethods, unverifiedTargetSdkVersion, userId);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }
    ...
}

startInputOrWindowGainedFocusInternalLocked方法:

@NonNull
private InputBindResult startInputOrWindowGainedFocusInternalLocked(
        @StartInputReason int startInputReason, IInputMethodClient client,
        @NonNull IBinder windowToken, @StartInputFlags int startInputFlags,
        @SoftInputModeFlags int softInputMode, int windowFlags, EditorInfo attribute,
        IInputContext inputContext, @MissingMethodFlags int missingMethods,
        int unverifiedTargetSdkVersion, @UserIdInt int userId) {
    ...
    //分支处理不同的softInputMode
    switch (softInputMode & LayoutParams.SOFT_INPUT_MASK_STATE) {
        ...
        case LayoutParams.SOFT_INPUT_STATE_VISIBLE:
            if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
                if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
                if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(
                        unverifiedTargetSdkVersion, startInputFlags)) {
                    if (attribute != null) {
                        res = startInputUncheckedLocked(cs, inputContext, missingMethods,
                                attribute, startInputFlags, startInputReason);
                        didStart = true;
                    }
                    showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, nu
                            SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
                } else {
                    Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
                            + " there is no focused view that also returns true from"
                            + " View#onCheckIsTextEditor()");
                }
            }
            break;
    ...
}

上面列出了stateVisible分支的逻辑
关于这个分支的逻辑,如果能够成功调用到showCurrentInputLocked,那么软键盘就可以弹出来了,否则会打印一行日志

当设置stateVisible,但不调用editText.requestFocus时,控制台确实打印了出来:

2020-10-23 10:17:38.440 499-533/? E/InputMethodManagerService: SOFT_INPUT_STATE_VISIBLE is ignored because there is no focused view that also returns true from View#onCheckIsTextEditor()

要到达这里,有两个关键的条件:

  1. (softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0
  2. InputMethodUtils.isSoftInputModeStateVisibleAllowed

下面一一分析

SOFT_INPUT_IS_FORWARD_NAVIGATION标志位

提到这个标识位,我们不得不说下stateAlwaysVisible在上面的方法中对应的switch分支。具体代码不再贴出,因为和stateVisible就差了一个下面的条件判断(当然log也是有所不同的)

if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
}

如果你掌握了stateVisiblestateAlwaysVisible的区别,那么想必对SOFT_INPUT_IS_FORWARD_NAVIGATION的作用已经有所猜想

对于Activity,该标志位在ActivityThread#handleResumeActivity中被设置

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    ...
    final int forwardBit = isForward
            ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
    ...
        l.softInputMode |= forwardBit;
    ...
}

对于Dialog,该标志位在Dialog#show中被设置

public void show() {
    ...
    WindowManager.LayoutParams l = mWindow.getAttributes();
    boolean restoreSoftInputMode = false;
    if ((l.softInputMode
            & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
        l.softInputMode |=
                WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
        restoreSoftInputMode = true;
    }
    mWindowManager.addView(mDecor, l);
    //view添加完毕,再把该标志位清除掉
    if (restoreSoftInputMode) {
        l.softInputMode &=
                ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
    }
    ...
}

除Dialog中清除,还会在ViewRootImpl#handleWinowFocusChanged中清除:

mImeFocusController.onPostWindowFocus(mView.findFocus(), hasWindowFocus,
        mWindowAttributes);
//onPostWindowFocus方法调用完毕,立即清除SOFT_INPUT_IS_FORWARD_NAVIGATION标识
if (hasWindowFocus) {
    // Clear the forward bit.  We can just do this directly, since
    // the window manager doesn't care about it.
    mWindowAttributes.softInputMode &=
            ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
    ((WindowManager.LayoutParams) mView.getLayoutParams())
            .softInputMode &=
            ~WindowManager.LayoutParams
                    .SOFT_INPUT_IS_FORWARD_NAVIGATION;

关于ActivityThread#handleResumeActivity中的isForward是什么含义,代码中没找到相关信息。但对比Dialog中的实现,以及SOFT_INPUT_IS_FORWARD_NAVIGATION标志位的注释,可以推断当Activity启动时,isForward为true;从其他页面返回或者后台调起的时候,为false

至此,我们还印证了前面的一个问题,对于Activity,onWindowFocusChanged的调用时机晚于onResume。因为onResume同时会决定是否添加SOFT_INPUT_IS_FORWARD_NAVIGATION标识;onWindowFocusChanged(onWindowFocusChangedstartInputOrWindowGainedFocusInternalLocked的调用链都始于handleWindowFocusChanged)同时会检查SOFT_INPUT_IS_FORWARD_NAVIGATION标识。而理所当然,检查应该在修改之后才对

InputMethodUtils#isSoftInputModeStateVisibleAllowed

该方法如下:

static boolean isSoftInputModeStateVisibleAllowed(int targetSdkVersion,
        @StartInputFlags int startInputFlags) {
    if (targetSdkVersion < Build.VERSION_CODES.P) {
        // for compatibility.
        return true;
    }
    if ((startInputFlags & StartInputFlags.VIEW_HAS_FOCUS) == 0) {
        return false;
    }
    if ((startInputFlags & StartInputFlags.IS_TEXT_EDITOR) == 0) {
        return false;
    }
    return true;
}

这里主要关注后面两个条件,即VIEW_HAS_FOCUSIS_TEXT_EDITOR这两个标志位是否有设置
搜索这两个标志位,发现在InputMethodManager#getStartInputFlags中调用:

private int getStartInputFlags(View focusedView, int startInputFlags) {
    startInputFlags |= StartInputFlags.VIEW_HAS_FOCUS;
    if (focusedView.onCheckIsTextEditor()) {
        startInputFlags |= StartInputFlags.IS_TEXT_EDITOR;
    }
    return startInputFlags;
}

VIEW_HAS_FOCUS是肯定会设置的。IS_TEXT_EDITOR依赖于View#onCheckIsTextEditor方法。该方法默认返回false,EditText会返回true

至此,我们也明白了为什么stateVisible需要和editText.requestFocus()一起使用(因为如果不调用editText.requestFocus(),则默认的focused view是DecorView,而DecorView没有覆写onCheckIsTextEditor

那么当focesed view不是EditText的时候,是不是就不可以弹软键盘了呢?当然可以!我们可以手动调用前一篇的Util.showKeyboard方法

softInputMode弹出软键盘方式

前面讲了那么多,终于可以完成我们的目的了

对于Dialog|DialogFragment,可以通过自定义theme的方式设置softInputMode,或者在代码中调用如下方式设定弹出软键盘:

getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
                | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);

至于调用时机,在DialogFragment中比较自由。onActivtityCreatedonResume均可

别忘了requestFocus()

如果需要在其他页面返回时也弹出,则需要使用SOFT_INPUT_STATE_ALWAYS_VISIBLE替换SOFT_INPUT_STATE_VISIBLE,同时将requestFocus()放到onResume中(针对DialogFragment)。上一篇中通过回调弹软件盘的方式也是如此

弹出软键盘方式对比

上一篇介绍了通过回调的方式弹软键盘,和softInputMode方式对比如下:

相同点:

  1. 都需要调用requestFocus
  2. 适用于focused window发生改变的场景,如切换Activity、Dialog、FragmentDialog。如果是新添加Fragment,直接调用Util.showKeyboard即可

不同点:

回调方式 softInputMode
更灵活,不会造成其他影响 对window属性进行了更改,可能影响同window下其他页面
实现稍微有些繁琐 实现简单,xml和java均可实现

最后

说一下上面留意的mCurrentTextBoxAttribute
InputMethodManager中有两个isActive重载方法,从名字和方法的注释看,很容易被用来判断软键盘是否正在显示:

/**
 * Return true if the given view is the currently active view for the
 * input method.
 */
public boolean isActive(View view) {
    // Re-dispatch if there is a context mismatch.
    final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
    if (fallbackImm != null) {
        return fallbackImm.isActive(view);
    }
    checkFocus();
    synchronized (mH) {
        return hasServedByInputMethodLocked(view) && mCurrentTextBoxAttribute != null;
    }
}

/**
 * Return true if any view is currently active in the input method.
 */
public boolean isActive() {
    checkFocus();
    synchronized (mH) {
        return getServedViewLocked() != null && mCurrentTextBoxAttribute != null;
    }
}

网上也有很多文章说可以用来判断软键盘是否展示,然而这是不准确的
比如启动一个空白Activity,设置stateVisible,但不调用requestFocus,这时mCurrentTextBoxAttribute对应DecorView的信息,但因为DecorView#onCheckIsTextEditor是false,所以软键盘不会弹出来。而isActive却返回了true
暂时没发现这个方法到底有什么实际用处

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

相关阅读更多精彩内容

友情链接更多精彩内容