在车机上,导航栏通常在左边,目的是离驾驶员近一点,方便操作。最近遇到一个问题,发现输入法的键盘会超出屏幕,显现不全。屏幕的物理分辨率为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个:
-
getDefaultKeyboardWidth在输入法的源码里,怎样更改dm.widthPixels的值?直接在输入法源码吗?
由于搜狗输入法也有相同的问题,我没法改搜狗输入法的源码,所以只能再想想在哪里修正。
-
怎样知道导航栏有没有显现?
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, 此刻输入法显现正常。 这需求搜狗输入法适配下车机。