在车机上,导航栏通常在左侧,目的是离驾驶员近一点,方便操作。最近遇到一个问题,发现输入法的键盘会超出屏幕,显示不全。屏幕的物理分辨率为1920x720,输入法为AOSP里原生的输入法,代码路径在/packages/inputmethods/LatinIME 。 全屏页面,输入法显示正常:
当左侧有导航栏时候,显示效果如下,右侧有部分超出了屏幕,输入法显示不全。
用布局检查器查看,键盘是一个自定义view,宽度为1920,期望的宽度应该是1920 - 130(导航栏的宽度) = 1790。
用sougou输入法,存在同样的问题


排查思路
按常理, 子view通常是不会超出父view的,但是对于直接继承View的自定义view,如果在onMeasure里设置的宽度大于父布局的宽度,就会出现子view超出父view的情况。 看看输入法的代码,重点看看MainKeyboardView,onMeasure的实现在父类KeyboardView
java
//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, 在全屏页面下,输入法的右侧会空出一个导航栏的宽度。
java
//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
}
解决方案
现在问题转变为:在全屏页面,getDefaultKeyboardWidth 需要返回1920, 而在带有导航栏的页面,需要返回1790。 需要解决的问题有2个:
-
getDefaultKeyboardWidth在输入法的源码里,怎么更改dm.widthPixels的值?直接在输入法源码吗?
因为搜狗输入法也有同样的问题,我没法改搜狗输入法的源码,所以只能再想想在哪里修改。
-
怎么知道导航栏有没有显示?
2.1 怎么修改dm.widthPixels的值?
看看getDefaultKeyboardWidth 的调用栈
java
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, 因为这个方法是距离输入法代码最近的地方。
java
//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。
java
//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,发现如下代码,每次导航栏显示或者隐藏,会回调到这里。我能想到的是在这里用系统属性记录下导航栏是否显示。
java
//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);
}
};
修改如下:
java
//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的值
java
//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, 此时输入法显示正常。 这需要搜狗输入法适配下车机。