Android Kotlin 监听软键盘弹出与关闭

前言

众所周知,google是没有为android提供官方的API来监听软键盘的弹出与关闭的,通俗的做法都是监听Activity这个window的布局变化来判断是否弹出/关闭软键盘

代码实现

需要说明的是,这儿并不使用ViewTreeObserver.OnGlobalLayoutListener来实现对布局的监听,而是会开一个子线程定时检查,因为在实际生产中发现,onGlobalLayout()的刷新时间是不确定的,跟布局的复杂程度有关,有的时候可能要2/3秒才会回调一次,靠这个来监听window的变化来判断是否弹出/关闭小键盘都凉成什么样了。
这儿会使用RxJava来开一个子线程,见Android Kotlin 基于RxJava的简单封装
弄一个looper来不断循坏检查window的变化,发生变化时便会吐出一个事件。

IKeyBoardCallback
interface IKeyBoardCallback {

    /**
     * 当键盘显示时回调
     */
    fun onKeyBoardShow()

    /**
     * 当键盘隐藏时回调
     */
    fun onKeyBoardHidden()
}
GlobalLayoutListenerTask
class GlobalLayoutListenerTask(private val activity: Activity) : SingleTask<Unit>(){

    private val iKeyBoardCallbackList = mutableListOf<IKeyBoardCallback>()
    private var status = NONE
    private val interval = 100L

    /** 全屏时的高度 */
    private var fullScreenHeight = -1
    /** 状态栏高度 */
    private var statusBarHeight = -1

    override fun onTaskRun() {
        while (isRunning){
            try {
                //获取可视范围
                val rect = Rect()
                activity.window.decorView.getWindowVisibleDisplayFrame(rect)
                //获取屏幕高度
                val screenHeight = getFullScreenHeight(activity)
                //获取状态栏高度
                val statueHeight = getStatueBarHeight(activity)
                //获取被遮挡高度(键盘高度)(屏幕高度-状态栏高度-可视范围)
                val keyBoardHeight: Int = screenHeight - statusBarHeight - rect.height()
                //显示或者隐藏
                val isKeyBoardShow = keyBoardHeight >= screenHeight / 3
                //当首次或者和之前的状态不一致的时候会回调,反之不回调(用于当状态变化后才回调,防止多次调用)
                if (status == NONE || (isKeyBoardShow && status == HIDDEN) || (!isKeyBoardShow && status == SHOW)) {
                    if (isKeyBoardShow) {
                        status = SHOW
                        dispatchKeyBoardShowEvent()
                    } else {
                        status = HIDDEN
                        dispatchKeyBoardHiddenEvent()
                    }
                }
                Thread.sleep(interval)
            } catch (e: Exception){
                e.printStackTrace()
            }
        }
    }

    /**
     * 用于获取全屏时的整体高度
     *
     * @return 屏幕高度
     */
    private fun getFullScreenHeight(activity: Activity): Int {
        if (fullScreenHeight == -1){
            val vm = activity.windowManager
            fullScreenHeight = vm.defaultDisplay.height
        }
        return fullScreenHeight
    }

    /**
     * 用于获取状态栏高度
     *
     * @return 状态栏高度
     */
    private fun getStatueBarHeight(activity: Activity): Int {
        if (statusBarHeight == -1){
            val res = activity.resources
            val resId = res.getIdentifier("status_bar_height", "dimen", "android")
            statusBarHeight =  res.getDimensionPixelSize(resId)
        }
        return statusBarHeight;
    }

    /**
     * 添加监听回调
     *
     * @param callback 监听的回调类
     */
    fun addCallBack(callback: Any?){
        if (callback is IKeyBoardCallback)  iKeyBoardCallbackList.add(callback)
    }

    /**
     * 移除监听回调
     *
     * @param callback 监听的回调类
     */
    fun removeCallback(callback: Any?){
        if (callback is IKeyBoardCallback)  iKeyBoardCallbackList.remove(callback)
    }

    /**
     * 分发隐藏事件
     */
    private fun dispatchKeyBoardHiddenEvent(){
        for (callback in iKeyBoardCallbackList){
            callback.onKeyBoardHidden()
        }
    }

    /**
     * 分发显示事件
     */
    private fun dispatchKeyBoardShowEvent(){
        for (callback in iKeyBoardCallbackList){
            callback.onKeyBoardShow()
        }
    }

    /**
     * 判断是不是没有监听回调
     *
     * @return true:空 false:不空
     */
    fun isEmpty(): Boolean = iKeyBoardCallbackList.isEmpty()

    companion object {
        private const val NONE = 0;
        private const val SHOW = 1;
        private const val HIDDEN = 2;
    }
}
KeyBoardEventBus
object KeyBoardEventBus {

    private val taskCache: Hashtable<Any,GlobalLayoutListenerTask> = Hashtable()

    /**
     * 用于注册键盘监听,此方法适用于 View、Dialog、Fragement、FragementActivity、Activity
     *
     * @param obj 需要监听的类()
     */
    fun register(obj: Any?){
        val activity = getActivity(obj)
        if (activity == null){
            debug("register时获取activity失败!")
            return
        }
        register(activity, obj)
    }

    /**
     * 此方法区别于 {@link #register(Object)} ,之前的方法会限制注册的类型,当前的不会限制类型
     *
     * @param activity 宿主activity
     * @param obj   监听的类
     */
    fun register(activity: Activity, obj: Any?){
        if (obj == null) {
            debug("object为null!")
            return
        }
        var task = taskCache[activity]
        if (task == null){
            task = GlobalLayoutListenerTask(activity)
        }
        task.addCallBack(obj)
        if (!task.isEmpty()) task.start()
        taskCache[activity] = task
    }

    /**
     * 反注册
     *
     * @param obj 取消监听的类
     */
    fun unRegister(obj: Any?){
        //获取失败则直接停止,反之进行反注册
        val activity = getActivity(obj)
        if (activity == null){
            debug("unRegister时获取activity失败")
            return
        }
        unRegister(activity, obj)
    }

    /**
     * 反注册
     *
     * @param activity 宿主activity
     * @param obj 监听的类
     */
    fun unRegister(activity: Activity, obj: Any?){
        if ( obj == null) {
            debug("activity或object为null!")
            return
        }
        val task = taskCache[activity] ?: return
        task.removeCallback(obj)
        if (task.isEmpty()){
            task.cancel()
            taskCache.remove(task)
        }
    }

    /**
     * 获取对应View、Dialog、Fragment、FragmentActivity、Activity
     * (如果Object为null或者不是支持的类型则返回null)
     *
     * @param obj 需要获取的类
     * @return 返回对应的activity
     */
    private fun getActivity(obj: Any?): Activity?{
        if (obj == null) return null

        return when(obj){
            is View -> obj.context as Activity
            is Dialog -> obj.context as Activity
            is Fragment -> obj.activity
            is FragmentActivity -> obj
            is Activity -> obj
            else -> null
        }
    }

    /**
     * 用于打印信息
     *
     * @param msg 待打印的内容
     */
    private fun debug(msg: String){
        Log.e("KeyBoardEventBus",msg)
    }
}

用法

postUI见Android Kotlin 代码笔记,全局的UI线程回调函数(基于扩展函数)
用来回调到UI线程弹Toast的

class MainActivity : AppCompatActivity(),IKeyBoardCallback {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        KeyBoardEventBus.register(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        KeyBoardEventBus.unRegister(this)
    }

    override fun onKeyBoardShow() {
        postUI {
            Toast.makeText(this, "键盘显示",Toast.LENGTH_SHORT).show()
        }
    }

    override fun onKeyBoardHidden() {
        postUI {
            Toast.makeText(this, "键盘隐藏",Toast.LENGTH_SHORT).show()
        }
    }
}

搞定收工

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344