# KeyboardDemo **Repository Path**: linyulong/KeyboardDemo ## Basic Information - **Project Name**: KeyboardDemo - **Description**: 仿微信聊天界面 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2021-12-24 - **Last Updated**: 2025-04-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: Android ## README # Android 监听软键盘的高度并解决其覆盖输入框的问题 ## 1、前言 在某些项目中,我们常常需要自定义一个输入框,软键盘弹出时就把输入框顶上去,关闭时输入框再回到原位(比如下方的效果图,实际上各种 App 中的聊天界面和发布评论的界面大体都是这样)。在这个过程中,**除了输入框以外的其他界面的元素不受影响**,比如效果图中的背景图片不会上移也不会被压缩。但在实际使用中发现软键盘在弹出时常常把输入框盖住,导致输入框显示不完全。有什么方法可以解决呢? ![效果图](https://gitee.com/linyulong/KeyboardDemo/raw/3304e3fbf87e59b1a0ffc700cd112f5d1848298c/%E6%95%88%E6%9E%9C%E5%9B%BE.gif) ## 2、思路分析 ### 2.1 获取软键盘的高度 网上常见的思路是这样的:在输入框的下面放置一个`View`,当软键盘弹出时,获取软键盘高度,然后在代码中动态将该`View`的高度设置成跟软键盘的一样,这样输入框就被它顶上去了。从视觉上来看,就像是被软键盘顶上去一样。 这个思路的难点在于**准确获取软键盘的动态高度**。Android 系统没有提供直接获取软键盘高度的 api,好在我们可以曲线救国:软键盘的高度其实就是屏幕高度减去软键盘上方的可见区域(即没有被软键盘挡住的区域)高度,也就是: > 软键盘高度 = 屏幕高度 - 可见区域高度 此外,还需要考虑状态栏和虚拟导航栏高度,所以我们可以得出以下的计算公式: > 软键盘高度 = 屏幕高度 - 可见区域高度 - 顶部状态栏高度 - 底部导航栏高度 不过有两点需要注意: 1. Activity 为全屏时是没有状态栏的,不必扣除高度; 2. 横屏时虚拟状态栏是在侧边的,这时也不必扣除它的高度了。 最后我们的公式可以修正为: > 软键盘高度 = 屏幕高度 - 可见区域高度 - 顶部状态栏高度(非全屏时) - 底部导航栏高度(竖屏时) 这个公式中的屏幕高度、状态栏高度和导航栏高度都可以通过 Android 的 api 获取,所以,现在问题的难点转换成了**准确获取可见区域的动态高度**。 ### 2.2 获取可见区域高度 准确获取可见区域的动态高度,何为准确,何为动态呢?要想准确,我们必须要**准确获取可见区域的对象**,要想动态,那**必须监听可见区域的高度变化**,也即是: 1. 获取可见区域(对应准确); 2. 监听可见区域的高度变化(对应动态)。 首先来看第一步,`View`类中为我们提供了一个方法`getWindowVisibleDisplayFrame()`,它可以获取某个`View`所在窗口(`Window`)的可见区域(注意:是窗口的可见区域,不是`View`的可见区域!)。它需要传入一个`Rect`对象,从`Rect`对象中,我们就可以获取到可见区域的信息,比如可见区域顶部距离父布局顶部的距离 `top`和可见区域底部部距离父布局顶部的距离`bottom`,两者一相减就是我们需要的可见区域高度了。 那么用哪一个`View`来获取可见区域呢?当前`Activity`或者`Fragment`上面的布局或者控件吗?答案是不行的。因为`Activity`(或`Fragment`)跟软键盘是位于同一个窗口的,也就是说,软键盘也在这个窗口的可见区域内,无论软键盘弹出还是关闭,可见区域的大小都不会变化! 既然如此,那么我们就需要另外一个窗口了。有没有办法创建一个不属于软键盘所在窗口的`View`呢?当然可以,`Dialog`和`PopupWindow`就可以办到。我们需要这个`View`一直存在,便于监听,所以`PopupWindow`无疑是最合适的。 第一步解决后,接下来就是监听可见区域的变化了这个比较简单,可以通过继承接口`ViewTreeObserver.OnGlobalLayoutListener`来,在`onGlobalLayout()`中监听来实现。 ## 3、代码实践 思路已经捋清楚了,现在是代码时间。创建一个`KeyboardStatusWatcher`类,继承于`PopupWindow`和`ViewTreeObserver.OnGlobalLayoutListener`接口: ```kotlin class KeyboardStatusWatcher( private val activity: FragmentActivity, private val lifecycleOwner: LifecycleOwner, private val listener: (isKeyboardShowed: Boolean, keyboardHeight: Int) -> Unit ) : PopupWindow(activity), ViewTreeObserver.OnGlobalLayoutListener { private val rootView by lazy { activity.window.decorView.rootView } private val TAG = "Keyboard-Tag" /** * 可见区域高度 */ private var visibleHeight = 0 /** * 软键盘是否显示 */ var isKeyboardShowed = false private set /** * 最近一次弹出的软键盘高度 */ var keyboardHeight = 0 private set /** * PopupWindow 布局 */ private val popupView by lazy { FrameLayout(activity).also { it.layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT ) //监听布局大小变化 it.viewTreeObserver.addOnGlobalLayoutListener(this) } } init { //初始化 PopupWindow contentView = popupView //软键盘弹出时,PopupWindow 要调整大小 softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE inputMethodMode = INPUT_METHOD_NEEDED //宽度设为0,避免遮挡界面 width = 0 height = ViewGroup.LayoutParams.MATCH_PARENT setBackgroundDrawable(ColorDrawable(0)) rootView.post { showAtLocation(rootView, Gravity.NO_GRAVITY, 0, 0) } //activity 销毁时或者 Fragment onDestroyView 时必须关闭 popupWindow ,避免内存泄漏 lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { super.onDestroy(owner) dismiss() } }) } /** * 监听布局大小变化 */ override fun onGlobalLayout() { val rect = Rect() //获取当前可见区域 popupView.getWindowVisibleDisplayFrame(rect) if (visibleHeight == (rect.bottom - rect.top)) { //可见区域高度不变时不必执行下面代码,避免重复监听 return } else { visibleHeight = (rect.bottom - rect.top) } //粗略计算高度的变化值,后面会根据状态栏和导航栏修正 val heightDiff = rootView.height - visibleHeight //这里取了一个大概值,当窗口高度变化值超过屏幕的 1/3 时,视为软键盘弹出 if (heightDiff > activity.screenHeight / 3) { isKeyboardShowed = true //非全屏时减去状态栏高度 keyboardHeight = if (activity.isFullScreen) heightDiff else heightDiff - activity.statusBarHeight //导航栏显示时减去其高度,但横屏时导航栏在侧边,故不必扣除高度 if (activity.hasNavBar && activity.isNavBarShowed && activity.isPortrait) { keyboardHeight -= activity.navBarHeight } } else { //软键盘隐藏时键盘高度为0 isKeyboardShowed = false keyboardHeight = 0 } listener.invoke(isKeyboardShowed, keyboardHeight) } } ``` 代码都是遵循前面的思路分析编写的,注释也比较详细,就不过多分析了。只要关注一下 `PopupWindow`存在时软键盘的交互。`PopupWindow`与软键盘分属于不同的窗口,软键盘弹出时,默认会被`PopupWindow`覆盖的(你可以通过修改上面的代码,给`PopupWindow`设置颜色且宽度不为 0 来验证),这样`PopupWindow`的高度不发生变化,就无法达到监听的目的。所以我们需要设置`softInputMode`和`inputMethodMode`两个属性,让`PopupWindow`的高度随着软键盘的弹出和关闭而调整。 然后简单看看MainActivity 布局: ```xml ``` 注意:这里`EditText`要加上`android:imeOptions="flagNoExtractUi"`属性,不然横屏时样式发生会变化。 还有,别忘了在清单文件中给Activity加上`android:windowSoftInputMode="adjustNothing|stateHidden"`,否则软键盘弹出时布局会整体上移的。 最后当然是在`Activity`中调用了: ```kotlin KeyboardStatusWatcher(this,this) { isKeyboardShowed: Boolean, keyboardHeight: Int -> vKeyboardBg.updateLayoutParams { bottomMargin = keyboardHeight } Log.d("Tag", "isShowed = $isKeyboardShowed,keyboardHeight = $keyboardHeight") } } ``` ## 4、项目地址 文章到此就结束了,项目地址如下:[Gitee](https://gitee.com/linyulong/KeyboardDemo)。 项目还有一个不足之处:实现需求了,但是使用体验上跟微信相比差很多,微信的输入框在软键盘弹出和收起时上下移动非常顺滑,没有什么闪烁。 如果你有更好的实现方法或者有其他的批评建议,欢迎留言和我交流。 ## 5、参考文章 [android EditText横屏显示问题 - 简书](https://www.jianshu.com/p/89002b76f847) [Android动态获取软键盘的高度,监听软键盘显示或则隐藏。 - 掘金](https://juejin.cn/post/6844903807550226439) [Android获取窗口可视区域大小: getWindowVisibleDisplayFrame()_ccpat的专栏-CSDN博客](https://blog.csdn.net/ccpat/article/details/55224475) [Android全面解析之Window机制_一只修仙的猿-CSDN博客](https://blog.csdn.net/weixin_43766753/article/details/108350589)