我们知道每个view的宽高变化都会导致
OnGlobalLayoutListener
的触发,但是对当前Activity的Window对象中的DecorView进行监听时,一般来说,DecorView的尺寸不会发生变化,发生变化的主要原因就是键盘的收起和展开,这时候加上简单的判断(变化超过某个阈值)就可以获取键盘的高度,以及是否弹起。
- 有没有使用
OnGlobalLayoutListener
监听键盘失效的情景?
在Android7.0上,我们可以使用多任务键开启分屏/多窗口模式,当我们开启分屏之后,调整分屏的分界线时,都会触发
DecorView
的OnGlobalLayoutListener
,但是此时键盘并未触发任何动作;而且,当我们点击某个输入框之后,键盘在分屏模式下会变成悬浮模式,不会挤压Activity的控件,所以当键盘弹出或收起时,OnGlobalLayoutListener
不会接收到任何事件。这就导致了OnGlobalLayoutListener
完全失效。还有一些其他的场景导致监听键盘事件失效的情景,暂时想不起来,可以在评论处补充。
3.3 获取键盘高度
在一般情况下,我们对Activity
的PhoneWindow
中DecorView
的布局变化进行监听,一般来说,变化值超过60dp
就可以认为是键盘弹出或收起了。而且在非全屏主题下, 键盘高度 = 屏幕高度 - 状态栏高度 - 主视图高度 - 标题栏高度, 于是我们可以通过下面代码间接计算出键盘高度。
mDecorView = this.getActivity().getWindow().getDecorView();
mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect rect = new Rect();
mDecorView.getWindowVisibleDisplayFrame(rect);
// 不能使用decorView.getHeight()获取decorview的高度,获取的高度不会发生变化
int displayHeight = rect.bottom;
if (Math.abs(displayHeight - mOldDecorViewHeight) > dp60) {
mOldDecorViewHeight =displayHeight;
int rootHeight = mRoot.getHeight();
int statusBarHeight = getStatusBarHeight();
int screenHeight = getScreenHeight();
int titleBarHeight = getTitleBarHeight();
//在非全屏模式下, 键盘高度 = 屏幕高度 - 状态栏高度 - 主视图高度 - 标题栏高度
int keyboardHeight = screenHeight - statusBarHeight - rootHeight - titleBarHeight;
Log.i(“lxc”, "keyboardHeight —> " + keyboardHeight + " 键盘: " + (keyboardHeight > 0 ? “弹出” : “收起”));
}
}
};
mDecorView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
当我点击输入框弹出和收起键盘时,会出现下面的log日志:
‘’‘console 05-29 15:22:50.368 8982-8982/com.orzangleli.myapplication I/lxc: keyboardHeight —> 0 键盘: 收起 05-29 15:22:51.736 8982-8982/com.orzangleli.myapplication I/lxc: keyboardHeight —> 873 键盘: 弹出 05-29 15:22:52.739 8982-8982/com.orzangleli.myapplication I/lxc: keyboardHeight —> 0 键盘: 收起 05-29 15:22:53.892 8982-8982/com.orzangleli.myapplication I/lxc: keyboardHeight —> 873 键盘: 弹出 ‘’’
4. 键盘与面板的切换冲突
4.1 问题描述
在IM聊天页面,通常下面会做成类似于微信的样式(点击后可切换表情面板和键盘)。点击表情按钮,会弹出表情面板,且表情按钮变成键盘模式;再次点击键盘模式,或者点击输入框,会弹出输入框,并收起表情面板。以下篇幅均称表情面板为面板。
4.2 常规思路
通常这样的页面布局是一个RecyclerView+输入区域。输入区域在RecyclerView下面,所以整个布局可以使用垂直的LinearLayout。键盘模式我们选择adjustResize
。
常规的逻辑如下:
- 输入区域包含输入框和下面的表情面板,默认表情面板的
visibility
为GONE
。 - 点击表情按钮时,面板的可见性为
VISIBLE
;收起输入法键盘;按钮图片变为键盘模式。 - 再次点击键盘模式按钮,面板的可见性为
GONE
;展开输入法键盘;按钮推盘变成表情模式。
我们按照上述思路写下关键代码:
private void initView(View root) {
mInputEt = root.findViewById(R.id.et_input);
mFaceBtn = root.findViewById(R.id.btn_face);
mPanel = root.findViewById(R.id.panel);
mFaceBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mPanel.getVisibility() == View.VISIBLE) {
mPanel.setVisibility(View.GONE);
mFaceBtn.setImageResource(R.drawable.emoji_download_icon);
openKeyBoard(mInputEt);
} else {
mPanel.setVisibility(View.VISIBLE);
mFaceBtn.setImageResource(R.drawable.zz_chat_reply_keyboard);
closeKeyBoard(mInputEt);
}
}
});
}
运行看看结果:
出现了奇怪的一帧:
结论:
当屏幕中已显示键盘时,点击表情按钮弹出面板前,需要隐藏键盘,但是隐藏键盘我们只是调用的一个远程服务(Context.INPUT_METHOD_SERVICE
),它是何时执行我们无法控制(一般来说,涉及跨进程通信,所以执行顺序肯定是在面板显示之后),所以我们无论我们先调用隐藏键盘api再显示表情面板,还是先显示表情面板在调用隐藏键盘的api都会出现这一帧现象,给人的感觉就是闪烁了一下。
4.3 解决方案
因此,我们隐藏和显示表情面板的时机不是点击表情按钮时就立刻执行,而是需要等到输入法面板完全显示或完全隐藏后再进行。
这里可能涉及到一些监听键盘弹起/隐藏操作和获取键盘高度的知识。可以参见上一节小结如何获取键盘高度。我们获取的键盘高度每次更新后直接存储在SharedPreferences
,某些应用需要重新将弹起的面板高度重新设置为与键盘高度相同,如微信就需要记录键盘的高度。但也不是每个应用都需要面板与键盘高度一致,如果你的应用不需要可以不用看如何获取键盘高度。
如果我们在OnGlobalLayoutListener中监听键盘的弹出或收起,并根据相应状态设置面板的隐藏或显示时会出现一些闪烁的问题(代码可以看Demo中的半解决切换键盘冲突)。因为闪烁的时间很短,所以录制gif的时候无法看到,有兴趣的可以运行Demo中半解决切换键盘冲突方案。
以键盘弹起为例,我们的流程是这样的:
触发键盘弹起 --> OnGlobalLayoutListener接收到布局变化 --> 此时键盘已经完全弹起 --> X --> 隐藏表情面板
这个流程中的X
指的是bug出现的时候,键盘完全弹起时,表情键盘并没有立即隐藏,而是随后隐藏的,这就导致了半解决冲突的微弱闪烁的现象。
- 为什么会出现这样的微弱的闪烁?
我们来看看ViewGroup的测量过程。ViewGroup测量时,会先去遍历测量所有的子View的尺寸,然后结合ViewGroup的测量模式计算出合适的尺寸。在我们这个案例里,当表情面板已经展开时,如果切换到键盘,首页键盘会挤压整个布局,也就是我们说的ViewGroup的布局,但是此时执行ViewGroup的onMeasure时,里面的表情面板仍然是可见的。然后我们在
OnGlobalLayoutListener
的回调里将表情面板的可见性设为GONE
, 但此时已经和键盘刚展开时已经不是同一帧了,所以看到了微弱的闪烁效果。
根据上面的分析,我们需要在键盘收起时的那一帧中,测量ViewGroup尺寸时,直接重新测量的面板控件的尺寸就可以了。我们把表情区域放进一个自定义的布局控件KBPanelConflictLayout
,整个页面的根布局设为自定义控件KBRootConflictLayout
(代码可以看Demo中的解决切换键盘冲突)。
在KBRootConflictLayout
的onMeasure
方法中,根据布局高度变化是否超过某个阈值来判断是否键盘弹起。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
preNotifyChild(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void preNotifyChild(int width, int height) {
if (mOldHeight < 0) {
mOldHeight = height;
return ;
}
int deltaY = height - mOldHeight;
mOldHeight = height;
int minKeyboardHeight = 180;
if (Math.abs(deltaY) >= minKeyboardHeight) {
if (deltaY < 0) {
// 键盘弹起
if (mKBPanelConflictLayout != null) {
// 隐藏面板
mKBPanelConflictLayout.setHide();
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)
![](https://i-blog.csdnimg.cn/blog_migrate/96864d2d0ab429e64715a8aae09ed0d4.jpeg)
分享读者
作者2013年java转到Android开发,在小厂待过,也去过华为,OPPO等大厂待过,18年四月份进了阿里一直到现在。
被人面试过,也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长,而且极易碰到天花板技术停滞不前!
我们整理了一份阿里P7级别的Android架构师全套学习资料,特别适合有3-5年以上经验的小伙伴深入学习提升。
主要包括阿里,以及字节跳动,腾讯,华为,小米,等一线互联网公司主流架构技术。如果你有需要,尽管拿走好了。
35岁中年危机大多是因为被短期的利益牵着走,过早压榨掉了价值,如果能一开始就树立一个正确的长远的职业规划。35岁后的你只会比周围的人更值钱。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
)]
35岁中年危机大多是因为被短期的利益牵着走,过早压榨掉了价值,如果能一开始就树立一个正确的长远的职业规划。35岁后的你只会比周围的人更值钱。