在车机上,导航栏通常在左边,目的是离驾驶员近一点,方便操作。最近遇到一个问题,发现输入法的键盘会超出屏幕,显现不全。屏幕的物理分辨率为1920×720,输入法为AOSP里原生的输入法,代码途径在/packages/inputmethods/LatinIME 。 全屏页面,输入法显现正常:

车机上输入法显现不完整问题解决方案
当左边有导航栏时分,显现作用如下,右侧有部分超出了屏幕,输入法显现不全。

车机上输入法显现不完整问题解决方案
用布局检查器检查,键盘是一个自定义view,宽度为1920,期望的宽度应该是1920 – 130(导航栏的宽度) = 1790。

车机上输入法显现不完整问题解决方案
用sougou输入法,存在相同的问题

车机上输入法显现不完整问题解决方案

车机上输入法显现不完整问题解决方案

1 排查思路

按常理, 子view通常是不会超出父view的,可是对于直接继承View的自定义view,假如在onMeasure里设置的宽度大于父布局的宽度,就会呈现子view超出父view的状况。 看看输入法的代码,重点看看MainKeyboardView,onMeasure的实现在父类KeyboardView

//packages/inputmethods/LatinIME/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
    final Keyboard keyboard = getKeyboard();
    if (keyboard == null) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        return;
    }
    // The main keyboard expands to the entire this {@link KeyboardView}.
    final int width = keyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
    Log.d(TAG, "width:" + width + ",mOccupiedWidth:" + keyboard.mOccupiedWidth);
    final int height = keyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
    setMeasuredDimension(width, height);
}

这儿可以加个打印,把width打印出来,发现,不论页面是否有左边导航栏,width都是1920,keyboard.mOccupiedWidth的值也一直是1920。我测验把width改为1790, 看作用,键盘的布局是不会超出屏幕了,可是仍然显现不全,看源码发现,每个按键的宽度是依据keyboard.mOccupiedWidth核算得来的。 直接把keyboard.mOccupiedWidth改为1790呢? 不行,mOccupiedWidth是final变量, 把final去掉再修正呢? 改了,仍是不行,由于核算按键宽度的逻辑在onMeasure之前。 所以要重点看看keyboard.mOccupiedWidth的值是怎样来的。代码有点多,我也懒得细看代码调用流程,猜测应该有当地会获取屏幕的分辨率,然后赋值给mOccupiedWidth,查找代码果然找到获取屏幕分辨率的当地,代码如下。 在这儿,我直接return 1790, 然后看上面onMeasure里的width也变成了1790, 再看看在有导航栏的页面,输入法显现也OK。 问题就这么解决了? 必定没这么简单,假如这儿直接回来1790, 在全屏页面下,输入法的右侧会空出一个导航栏的宽度。

//packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
public static int getDefaultKeyboardWidth(final Resources res) {
    final DisplayMetrics dm = res.getDisplayMetrics();
    return dm.widthPixels; // 测验直接回来1790
}

2 解决方案

现在问题转变为:在全屏页面,getDefaultKeyboardWidth 需求回来1920, 而在带有导航栏的页面,需求回来1790。 需求解决的问题有2个:

  1. getDefaultKeyboardWidth在输入法的源码里,怎样更改dm.widthPixels的值?直接在输入法源码吗?

    由于搜狗输入法也有相同的问题,我没法改搜狗输入法的源码,所以只能再想想在哪里修正。
    
  2. 怎样知道导航栏有没有显现?

2.1 怎样修正dm.widthPixels的值?

看看getDefaultKeyboardWidth 的调用栈

at com.android.inputmethod.latin.utils.ResourceUtils.getDefaultKeyboardWidth(ResourceUtils.java:189)
at com.android.inputmethod.keyboard.KeyboardSwitcher.loadKeyboard(KeyboardSwitcher.java:115)
at com.android.inputmethod.latin.LatinIME.onStartInputViewInternal(LatinIME.java:994)
at com.android.inputmethod.latin.LatinIME$UIHandler.onStartInputView(LatinIME.java:510)
at com.android.inputmethod.latin.LatinIME.onStartInputView(LatinIME.java:816)
at android.inputmethodservice.InputMethodService.showWindowInner(InputMethodService.java:1863)
at android.inputmethodservice.InputMethodService.showWindow(InputMethodService.java:1803)
at android.inputmethodservice.InputMethodService$InputMethodImpl.showSoftInput(InputMethodService.java:572)
at android.inputmethodservice.IInputMethodWrapper.executeMessage(IInputMethodWrapper.java:207)
at com.android.internal.os.HandlerCaller$MyHandler.handleMessage(HandlerCaller.java:37)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6683)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:982)

看看showWindowInner, 由于这个办法是间隔输入法代码最近的当地。

//frameworks/base/core/java/android/inputmethodservice/InputMethodService.java
void showWindowInner(boolean showInput) {
    //省掉部分代码
    if (mShowInputRequested) {
            if (!mInputViewStarted) {
                //注释1
                if (DEBUG) Log.v(TAG, "CALL: onStartInputView");
                mInputViewStarted = true;
                onStartInputView(mInputEditorInfo, false);
            }
        } else if (!mCandidatesViewStarted) {
            if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView");
            mCandidatesViewStarted = true;
            onStartCandidatesView(mInputEditorInfo, false);
        }
    //省掉部分代码
}

发现,每次输入法弹出时,都会调到注释1处,并且这段代码在framework里,可是运行在输入法进程。测验在这儿改dm.widthPixels。

//frameworks/base/core/java/android/inputmethodservice/InputMethodService.java
void showWindowInner(boolean showInput) {
    //省掉部分代码
    if (mShowInputRequested) {
            if (!mInputViewStarted) {
                //注释1
                if (DEBUG) Log.v(TAG, "CALL: onStartInputView");
                mInputViewStarted = true;
                Resources res = getResources();
                if (res != null) {
                    final DisplayMetrics dm = res.getDisplayMetrics();
                    dm.widthPixels = 1790;
                    Log.d(TAG, "mInputEditorInfo:" + mInputEditorInfo.packageName);
                }
                onStartInputView(mInputEditorInfo, false);
            }
        } else if (!mCandidatesViewStarted) {
            if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView");
            mCandidatesViewStarted = true;
            onStartCandidatesView(mInputEditorInfo, false);
        }
    //省掉部分代码
}

先在这儿写死dm.widthPixels = 1790; 编译后,push到体系,发现在带有导航栏的页面,输入法显现OK。说明这儿改dm.widthPixels的值是有效的。 接下来再看看怎样获取导航栏是否显现, 体系没有相关的API。 假如有读者知道,请告知下,谢谢!

2.2 怎样知道导航栏有没有显现?

看PhoneWindowManager,发现如下代码,每次导航栏显现或者隐藏,会回调到这儿。我能想到的是在这儿用体系属性记录下导航栏是否显现。

//frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
private final BarController.OnBarVisibilityChangedListener mNavBarVisibilityListener =
        new BarController.OnBarVisibilityChangedListener() {
    @Override
    public void onBarVisibilityChanged(boolean visible) {
        mAccessibilityManager.notifyAccessibilityButtonVisibilityChanged(visible);
    }
};

修正如下:

//frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
private final BarController.OnBarVisibilityChangedListener mNavBarVisibilityListener =
        new BarController.OnBarVisibilityChangedListener() {
    @Override
    public void onBarVisibilityChanged(boolean visible) {
        if (visible) {
            SystemProperties.set("sys.navigationbar.show", "true");
        } else {
            SystemProperties.set("sys.navigationbar.show", "false");
        }
        mAccessibilityManager.notifyAccessibilityButtonVisibilityChanged(visible);
    }
};

2.3 终极解决方案

在InputMethodService里通过体系属性sys.navigationbar.show获取导航栏是否显现,然后修正dm.widthPixels的值

//frameworks/base/core/java/android/inputmethodservice/InputMethodService.java
void showWindowInner(boolean showInput) {
    //省掉部分代码
    if (mShowInputRequested) {
            if (!mInputViewStarted) {
                //注释1
                if (DEBUG) Log.v(TAG, "CALL: onStartInputView");
                mInputViewStarted = true;
                Resources res = getResources();
                if (res != null) {
                    final DisplayMetrics dm = res.getDisplayMetrics();
                    String naviShow = SystemProperties.get("sys.navigationbar.show");
                    if ("true".equals(naviShow)) {
                        dm.widthPixels = 1790; //减去导航栏的宽度
                    } else {
                        dm.widthPixels = 1920;
                    }
                    Log.d(TAG, "mInputEditorInfo:" + mInputEditorInfo.packageName);
                }
                onStartInputView(mInputEditorInfo, false);
            }
        } else if (!mCandidatesViewStarted) {
            if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView");
            mCandidatesViewStarted = true;
            onStartCandidatesView(mInputEditorInfo, false);
        }
    //省掉部分代码
}

3 搜狗输入法的问题

可是上述解决办法只对AOSP原生的输入法收效。对搜狗输入法不收效。 估测的原因: 搜狗输入法进程开机启动,在进程起来时,拿到的屏幕宽度就是1920, 后面不再获取屏幕宽度,所以在导航栏显现的页面,键盘会超出屏幕,但假如此刻把搜狗输入法杀掉,再次弹出输入法,此刻拿到的屏幕宽度就是我修正后的1790, 此刻输入法显现正常。 这需求搜狗输入法适配下车机。