View — 计算键盘高度

一、概述

需求:

如下图所示:(这个需求比较常见)

  1. 当键盘弹起时,键盘顶部弹出一个“勾选我啊”的View;
  2. 当键盘收起时,隐藏该View;类似微信发朋友圈时弹起的Emoji图标;

实现方案:

  1. 监听键盘弹起、收起事件;
  2. 计算键盘弹起时的高度 keyboardHeight (此处要注意高度计算需要考虑的场景);
    1. 计算高度要区分是否有虚拟导航栏;
    2. 有虚拟导航栏要区分虚拟导航栏是否显示;
  3. 设置指定 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>

四、小结

  1. 监听键盘弹起、收起事件;
  2. 计算键盘弹起时的高度 keyboardHeight (注意高度计算需要考虑不同手机的兼容性问题:如全面屏、有虚拟导航栏、无虚拟导航栏的场景);
    2.1 计算高度要区分是否有虚拟导航栏;
    2.2 有虚拟导航栏要区分虚拟导航栏是否显示;
  3. 设置指定 View 的 MarginBottom,使 MarginBottom = keyboardHeight
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值