# 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 中的聊天界面和发布评论的界面大体都是这样)。在这个过程中,**除了输入框以外的其他界面的元素不受影响**,比如效果图中的背景图片不会上移也不会被压缩。但在实际使用中发现软键盘在弹出时常常把输入框盖住,导致输入框显示不完全。有什么方法可以解决呢?

## 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)