背景
咱app社交模块,键盘和自定义面板切换的时候,出现了闪烁的现象
我们使用了JKeyboardPanelSwitch解决了这个问题,此篇文章简要分析其原理。
键盘、面板交互方式为:
键盘.show();
面板.hide();
软键盘布局解释
项目中,常用的软键盘弹出模式(windowSoftInputMode)有两种
- adjustResize
布局高度压缩,腾出位置给键盘 -
adjustPan
键盘空间不够的时候,布局整体向上推,腾出位置给键盘
原因分析
键盘、面板的visible操作,触发了两次根布局的onMeasure、onLayout事件,
键盘->自定义面板
1. onMeasure,宽:1080 高:1127
2. onLayout,宽:1080 高:1127
3. onMeasure,宽:1080 高:1127
4. onMeasure,宽:1080 高:1930
5. onLayout,宽:1080 高:1930
自定义面板->键盘
1. onMeasure,宽:1080 高:1930
2. onLayout,宽:1080 高:1930
3. onMeasure,宽:1080 高:1930
4. onMeasure,宽:1080 高:1127
5. onLayout,宽:1080 高:1127
解决思路
简单点说,界面切换平滑的前提:
- panel的显示、隐藏操作,不能引起重新测量、重新布局。
- panel的高度和键盘一致,这样切换面板,输入框的Y轴位置保持固定。
下面,我们看JKeyboardPanelSwitch是如何解决闪动问题
1.避免重复onLayout方法
根布局两次Layout的触发的原因
- 在adjustResize情况下,键盘弹开、收起
- 位于面板容器visible状态改变,引起父容器重新测量
2.键盘、面板高度一致
在监听根布局的时候,收缩、展开高度的差,经过计算,就是键盘的高度。把该高度赋值给面板,我们就能保证键盘、面板切换时,输入框在Y轴上的位置不变了。
JKeyboardPanelSwitch核心类分析
JKeyboardPanelSwitch在布局上提供了两个容器
- 根节点容器:通过布局高度的变化,计算出偏差值,得出键盘高度,通知面板显示、隐藏。
-
面板容器:负责接收根节点的隐藏、显示通知,并延后至onMeasure里执行以防闪烁。
根节点
根节点容器处理了两个重要的事件,我们逐个分析
onGlobalLayout
计算键盘高度、并赋值给面板容器;
计算键盘是否弹出状态onMeasure
根据布局高度变化的差值,通知面板显示、隐藏
为什么要在onGlobalLayout中处理真正的键盘变化并且进行键盘高度变化存储?
因为效率:由于onMeasure与onLayout可能被多次调用,而onGlobalLayout是布局变化后只会被一次调用,并且我们需要处理所有键盘高度的变化(如搜狗输入法的动态调整键盘高度)因此在onGlobalLayout中计算键盘高度变化以及有效高度进行存储更为恰当。
为什么需要在根节点的onMeasure中判断,而不是其他地方判断是否是真正键盘引起变化的?
对于我们可见的'最早'获知布局变化的,就是根节点的onMeasure,其次,必须要在面板容器的onMeasure之前获知,因此比较恰当的地方就是面板容器的父布局的onMeasure,还有就是,其实真正感知键盘变化我们可以非常确定的也只有对我们可见的最外层的布局。综合之,并且就封装而言就只能是根节点的onMeasure甚至更早。
面板节点
面板节点重写了setVisibility()、onMeasure()函数,修改了显示、隐藏操作触发的时机;
- 显示:键盘->面板
在键盘收起、面板展开的时候,拦截面板的setVisibility操作;
延迟到在根节点因键盘收起完毕导致onMeasure的事件里,此时再显示面板。
@Override
public void setVisibility(int visibility) {
拦截用户显示操作,用于键盘收起->面板展开的流程
visible的操作,在根布局的onMeasure里会通知
if (panelLayoutHandler.filterSetVisibility(visibility)) {
return;
}
super.setVisibility(visibility);
}
- 隐藏:面板->键盘
根节点键盘弹出完毕后,会触发一次onMeasure,此时标记面板要隐藏
根节点分发onMeasure事件到面板节点,面板节点检测到隐藏标记,在此时Gone掉自己,同时把宽高设置为0,从而在当前帧不会被键盘顶起
public int[] processOnMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mIsHide) {
panelLayout.setVisibility(View.GONE);
/*
* The current frame will be visible nil.
*/
widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.EXACTLY);
heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.EXACTLY);
}
final int[] processedMeasureWHSpec = new int[2];
processedMeasureWHSpec[0] = widthMeasureSpec;
processedMeasureWHSpec[1] = heightMeasureSpec;
return processedMeasureWHSpec;
}
小结:我们基于 adjustResize在根布局可以监听到布局改变,从而控制面板达到防抖动效果。
但是在以下两种状态下,onMeasure检测不到高度的改变,我们需要另行处理
- 全屏模式
- 状态栏透明,但是根布局没有设置fitSystemWindow=true
onMeasure失效情况下的做法
键盘模式采用adjustPan。这种模式下,我们仅需要引入面板节点即可。
adjustPan我们知道,当输入框底部的空隙可以容纳键盘高度时,界面是不会滚动的。
因此,JKeyboardPanelSwitch的做法是:
- 键盘弹出
在输入框触摸时,把面板设置为invisible,把输入框顶高。此时键盘弹出的空间足够,界面就不会滚动
focusView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
/*
* Show the fake empty keyboard-same-height panel to fix the conflict when
* keyboard going to show.
* @see KPSwitchConflictUtil#showKeyboard(View, View)
*/
panelLayout.setVisibility(View.INVISIBLE);
}
return false;
}
});
键盘隐藏
根布局的onGlobalLayout检测到键盘高度发生了变化,如果是收起,则通知面板去隐藏
KPSwitchFSPanelLayoutHandler#onKeyboardShowing
面板弹出
面板从InVisible 切换到Visible,隐藏键盘
额外注意
@Override
protected void onPause() {
super.onPause();
panelRoot.recordKeyboardStatus(getWindow());
}
我们可以看到接入的Activity的onPause函数里多了一段代码。注释掉后,发现Activity的切到后台,回来之后,界面又被顶上去了。我们回顾下原因
- activity切到后台,键盘隐藏,触发onGlobalLayout,隐藏panel
- 切回activity,因为键盘拥有焦点,activity如果没有声明
stateAlwaysHidden
,键盘会自动弹出。 - 此时因为panel是gone的,键盘容纳空间不够,因此会把界面顶上去。
因此,我们在onPause的时候,需要标记键盘是否弹出
private void saveFocusView(final View focusView) {
recordedFocusView = focusView;
focusView.clearFocus();
panelLayout.setVisibility(View.GONE);
}
在切回activity的时候触发onGlobalLayout,判断是否有保存的标记,如果有,手动执行一次面板invisble、键盘弹出操作
private void restoreFocusView() {
panelLayout.setVisibility(View.INVISIBLE);
KeyboardUtil.showKeyboard(recordedFocusView);
}