一、概述
需求:
如下图所示:(这个需求比较常见)
- 当键盘弹起时,键盘顶部弹出一个“勾选我啊”的View;
- 当键盘收起时,隐藏该View;类似微信发朋友圈时弹起的Emoji图标;
实现方案:
- 监听键盘弹起、收起事件;
- 计算键盘弹起时的高度
keyboardHeight
(此处要注意高度计算需要考虑的场景);- 计算高度要区分是否有虚拟导航栏;
- 有虚拟导航栏要区分虚拟导航栏是否显示;
- 设置指定 View 的 MarginBottom,使
MarginBottom = keyboardHeight
;
顶起键盘 | 收起键盘 |
二、键盘监听
如何判断虚拟导航栏是否显示
/**
* 底部导航栏只能通过这种方式判断
* 注:网上通过判断高度来判断是否展示虚拟导航栏不能解决该问题;
* 参见方法:isNavigationBarShow()
*/
private fun isNavigationBarShow(activity: Activity): Boolean {
//虚拟键的view,为空或者不可见时是隐藏状态
val view = activity.findViewById<View>(android.R.id.navigationBarBackground) ?: return false
val visible = view.visibility
return !(visible == View.GONE || visible == View.INVISIBLE)
}
/**
* 通过尺寸判断是否展示底部导航栏 (该方法不能判断虚拟导航栏是否展示)
* 注:在全面屏手机里不生效,即使没有展示虚拟导航栏,返回值仍为true
*/
private fun isNavigationBarShow(): Boolean {
val display = activity.getWindowManager().getDefaultDisplay()
val size = Point()
val realSize = Point()
display.getSize(size)
display.getRealSize(realSize)
return realSize.y != size.y
}
完整的键盘弹起、收起监听代码
class KeyboardObserver(var activity: Activity) {
companion object {
const val KEYBOARD_THRESHOLD_DP = 100
}
private var isOpened = false
private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
/**
* 屏幕真实可用高度(屏幕高度 - 底部虚拟键盘高度)
*/
private val realHeight: Int by lazy {
activity.resources.displayMetrics.heightPixels
}
private val keyHeight: Float by lazy {
dp2px(activity, KEYBOARD_THRESHOLD_DP.toFloat())
}
fun registerKeyboard(callback: (isOpen: Boolean, height: Int) -> Unit) {
try {
val activityRoot = activity.window.decorView ?: return
globalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
private val rect = Rect()
private val visibleThreshold = Math.round(keyHeight)
override fun onGlobalLayout() {
// 获取用户可见区域的大小
activityRoot.getWindowVisibleDisplayFrame(rect)
// 键盘的高度 = 屏幕真实可用高度 - 可见区域的底部高度 - (底部虚拟导航栏高度)
val heightDiff = getHeight(activityRoot, rect)
val isOpen = heightDiff > visibleThreshold
if (isOpen == isOpened) {
return
}
isOpened = isOpen
callback(isOpen, heightDiff)
}
}
activityRoot.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun unRegisterKeyboard() {
try {
globalLayoutListener?.apply {
val activityRoot = activity.window.decorView ?: return
activityRoot.viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun dp2px(context: Context, dpVal: Float): Float {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dpVal, context.resources.displayMetrics
)
}
/**
* 获取键盘顶部的真是高度
* 条件如下:
* 第一步:判断手机是否有虚拟导航栏
* 1.有虚拟导航栏
* 2.没有虚拟导航栏 (height = 屏幕真实可用高度 - 可见区域的底部高度)
*
* 第二步:有虚拟导航栏场景时, 判断虚拟导航栏是否显示
* 1.显示 (height = 屏幕真实可用高度 - 可见区域的底部高度)
* 2.不显示 (height = 屏幕根View高度 - 可见区域的底部高度)
*/
private fun getHeight(rootView: View, rect: Rect): Int {
// 判断是否有虚拟键盘
if (checkDeviceHasNavigationBar(activity)) {
// 虚拟导航栏可见
if (isNavigationBarShow(activity)) {
return mRealHeight - rect.bottom
} else {
// 虚拟导航栏不可见
return rootView.bottom - rect.bottom
}
} else {
// 没有虚拟导航栏的
return mRealHeight - rect.bottom
}
}
/**
* 是否存在虚拟导航栏
* @return true=存在虚拟导航栏
*/
private fun checkDeviceHasNavigationBar(context: Context): Boolean {
var hasNavigationBar = false
val resource = context.resources
val id = resource.getIdentifier("config_showNavigationBar", "bool", "android")
if (id > 0) {
hasNavigationBar = resource.getBoolean(id)
}
try {
val systemPropertiesClass = Class.forName("android.os.SystemProperties")
val method = systemPropertiesClass.getMethod("get", String::class.java)
val navBarOverride = method.invoke(systemPropertiesClass, "qemu.hw.mainkeys")
if ("1" == navBarOverride) {
hasNavigationBar = false
} else if ("0" == navBarOverride) {
hasNavigationBar = true
}
} finally {
return hasNavigationBar;
}
}
/**
* 底部导航栏只能通过这种方式判断
* 注:网上通过判断高度来判断是否展示虚拟导航栏不能解决该问题,参见方法 isNavigationBarShow()
*/
private fun isNavigationBarShow(activity: Activity): Boolean {
//虚拟键的view,为空或者不可见时是隐藏状态
val view = activity.findViewById<View>(android.R.id.navigationBarBackground) ?: return false
val visible = view.visibility
return !(visible == View.GONE || visible == View.INVISIBLE)
}
}
三、代码示例
class MainActivity : AppCompatActivity() {
private val mKeyboardObserver: KeyboardObserver by lazy {
KeyboardObserver(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mKeyboardObserver.registerKeyboard { isOpen, height ->
if (isOpen) bottomLl.visibility = View.VISIBLE else bottomLl.visibility = View.GONE
(bottomLl.layoutParams as RelativeLayout.LayoutParams).setMargins(0, 0, 0, if (isOpen) height else 0)
bottomLl.requestLayout()
}
}
override fun onDestroy() {
super.onDestroy()
mKeyboardObserver.unRegisterKeyboard()
}
}
布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootRl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:focusable="true"
android:focusableInTouchMode="true">
<TextView
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#33f0f0"
android:gravity="center"
android:text="标题栏"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/topBar"
android:orientation="vertical"
android:paddingLeft="15dp"
android:paddingTop="18dp"
android:paddingRight="15dp">
<EditText
android:id="@+id/contentEdt"
android:layout_width="match_parent"
android:layout_height="120dp"
android:background="#00000000"
android:gravity="top|left"
android:hint="请输入文字"
android:lineSpacingExtra="7dp"
android:textColor="#000000"
android:textColorHint="#999999"
android:textCursorDrawable="@null"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="8dp"
android:background="#e1e1e1" />
</LinearLayout>
<LinearLayout
android:id="@+id/bottomLl"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:background="#9ff3f0"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="visible">
<CheckBox
android:id="@+id/commentCheckbox"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:button="@null"
android:checked="false"
android:drawableStart="@drawable/mt_selector_checkbox"
android:drawablePadding="5dp"
android:paddingStart="15dp"
android:paddingEnd="5dp"
android:text="勾选我啊" />
</LinearLayout>
</RelativeLayout>
四、小结
- 监听键盘弹起、收起事件;
- 计算键盘弹起时的高度
keyboardHeight
(注意高度计算需要考虑不同手机的兼容性问题:如全面屏、有虚拟导航栏、无虚拟导航栏的场景);
2.1 计算高度要区分是否有虚拟导航栏;
2.2 有虚拟导航栏要区分虚拟导航栏是否显示; - 设置指定 View 的 MarginBottom,使
MarginBottom = keyboardHeight
;