专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
鸿洋  ·  一波深入的Android 性能优化 ·  昨天  
郭霖  ·  这可能是Android软键盘监听的最佳方案 ·  3 天前  
鸿洋  ·  Android anr排查之sp卡顿 ·  3 天前  
郭霖  ·  Android 16首个开发者预览版到来 ·  4 天前  
51好读  ›  专栏  ›  郭霖

这可能是Android软键盘监听的最佳方案

郭霖  · 公众号  · android  · 2024-12-24 08:00

正文



/   今日科技快讯   /

由于计算成本居高不下,加之高质量训练数据的稀缺,OpenAI在推进其下一代旗舰模型GPT-5的开发进程上正落后于原定计划。截至目前,OpenAI已至少对GPT-5实施了两轮大规模训练,旨在借助海量数据资源来优化模型效能。然而,首次训练的实际运行速度未能达到预期标准,致使更大规模的训练尝试不仅耗时冗长,而且成本高昂。尽管GPT-5相较于其前代在性能层面有所提升,但这种进步幅度尚不足以充分验证维持该模型运作所需巨额成本是否值得。

/   作者简介   /

本篇文章转自Boybeak的博客,文章主要分享了一种监听 Android 软键盘的方法,相信会对大家有所帮助!

原文地址:
https://juejin.cn/post/7446686241105592371

/   前言   /

先上效果图:


图穷匕见:源码参考如下地址:

https://github.com/boybeak/skb-global

简单做一个补充,@GeekTR 同学提到了一个方式,就是利用WindowInsets API获取键盘高度。在经过简单实验以后,在Android 10及以上的系统版本中,确实是可以的,需要注意的是,键盘弹起过程中,会多次调用onApplyWindowInsets,这就必须通过“防抖”来解决,这就会遇到滞后性、误触性的问题;在低于Android 10的版本中,键盘弹起并不会触发onApplyWindowInsets方法,并且没有WindowInsets.Type.ime()这样专指键盘高度的insets,如果通过insets.systemWindowInsetBottom又容易受到底部导航栏变化的影响。所以,不再做WindowInsets的方案了,有兴趣探索的,可以查看feat/insets分支下的代码。

/   正文   /

背景

监听Android的软键盘状态与高度总是很麻烦,因为官方并没有给一个api来做这个事,我们就只能利用系统的其他机制来实现。

先说一下之前尝试的旧方案:

在布局中添加一个空白的FrameLayout,通过这个FrameLayout的onSizeChanged或者addOnLayoutChangeListener ,来获取软键盘弹出前后的高度值差值来计算键盘的高度,由于尺寸变更的回调,可能在整个过程中,会多次触发,就需要使用一个延迟任务去测量布局的高度,即需要做防抖,如果在任务等待期间,又一次触发了尺寸变化的回调,说明布局还未稳定,则取消掉上一次测量任务,添加一个新的测量任务。但是这种方案有如下问题:

  1. 侵入性,必须在布局中显式的放置测量布局;
  2. 滞后性,测量时机上延迟的,并不能在布局稳定的第一时间获取到键盘的高度;
  3. 误触性,由于获取键盘高度靠延迟任务,延迟的时间太长,则导致滞后性太严重,太短,则可能来不及取消上一个任务,测量高度就被错误触发;
  4. 不定性,由于设备可能会有横竖屏幕切换,因横竖屏切换导致的测量布局尺寸发生变化进而触发键盘高度事件,这个是不可接受的。

为了解决这些问题,我改成现在的监听方案 —— skb-global。

安装

该库托管于jitpack,所以在使用前,请先引入jitpack。

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}

然后添加依赖。

dependencies {
    implementation 'com.github.boybeak:skb-global:Tag'
}

最新版本为:

使用

有两种使用方式:全局和局部。

全局使用

在使用前,需要先在Application中初始化SoftKeyboardGlobal。

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        SoftKeyboardGlobal.install(thistrue)
    }
}

其中,第二个参数传入true,可以在UI界面显示一个高度指示器。

然后,在你应用中的任意位置,可以监听键盘的状态与高度,如下方式:

SoftKeyboardGlobal.addSoftKeyboardCallback(object : SoftKeyboardGlobal.SoftKeyboardCallback {
    override fun onOpen(height: Int) {
        Log.d(TAG, "onOpen height=$height")
    }

    override fun onClose() {
        Log.d(TAG, "onClose")
    }

    override fun onHeightChanged(height: Int) {
        Log.d(TAG, "onHeightChanged height=$height")
    }
})

局部使用

你可以在任意Activity, Fragment或者View中使用这种方式,只要能获取到Activity实例。以Activity为例,如下方式:

class MainActivity : AppCompatActivity() {

    private val observer by lazy { KeyboardObserver.create(thistrue) }
    private val switchBtn: SwitchCompat by lazy { findViewById(R.id.switchBtn) }

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

        switchBtn.setOnCheckedChangeListener { buttonView, isChecked ->
            if (isChecked) {
                observer.watch()
            } else {
                observer.unwatch()
            }
        }
    }
}

同样的,在创建KeyboardObserver时,第二个参数传入true可以显示一个键盘高度指示器,方便调试。

原理分析

该方案是在多年开发过程中,实践出来的最佳的方案。正如在摘要中说的,旧方案有四点问题,为了解决这些问题,我决定完全抛弃旧方案。

旧方案之所以出现这些问题,就是因为单一布局测量。

  1. 无法知道布局稳定的准确时机;
  2. 键盘弹出时,弹出前的高度,可能因为横竖屏幕切换,导致变得不可靠。

进而我改成双布局测量,但是双布局都放置在原有布局中,同样避免不了因键盘弹出,同时遭到尺寸修改。

再进而,我改为不受键盘弹出影响的PopupWindow。用两个PopupWindow,一个用于测量当前屏幕的高度(实际上并不是屏幕高度,而是键盘底部的位置),称为Ruler——尺子,Ruler并不会随着键盘的弹出而改变尺寸;相反的,另外一个会跟随软键盘的弹出而改变尺寸,称为Cursor——游标。

两个PopupWindow的创建代码如下:

private fun makeRulerPopWin(activity: Activity) = PopupWindow(activity).apply {
    contentView = if (showDebug) {
        TextView(activity).apply {
            background = GradientDrawable().apply {
                this.setStroke(1.dp, Color.LTGRAY)
            }
            gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
            setTextColor(Color.RED)
        }
    } else {
        View(activity)
    }
    setBackgroundDrawable(null)
    width = if (showDebug) 80.dp else 1     // if set to 0, getGlobalVisibleRect will not work
    height = WindowManager.LayoutParams.MATCH_PARENT
    elevation = 0F

    isFocusable = false
    isTouchable = false
    isOutsideTouchable = false
}
private fun makeCursorPopWin(activity: Activity) = PopupWindow(activity).apply {
    contentView = if (showDebug) {
        FrameLayout(activity).apply {
            addView(
                View(activity).apply {
                    background = ColorDrawable(Color.RED)
                },
                FrameLayout.LayoutParams(
                    FrameLayout.LayoutParams.MATCH_PARENT,
                    1.dp,
                    Gravity.BOTTOM
                )
            )
        }
    } else {
        View(activity)
    }
    setBackgroundDrawable(null)

    width = if (showDebug) 80.dp else 1     // if set to 0, getGlobalVisibleRect will not work
    height = WindowManager.LayoutParams.MATCH_PARENT
    elevation = 0F

    softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
    inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED

    isFocusable = false
    isTouchable = false
    isOutsideTouchable = false
}

二者的关键区别就在于makeCursorPopWin时的这两行代码:

softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED

正是这两行代码,让Cursor会跟随键盘弹出而改变尺寸,尺寸改变后,再显示Ruler,必须是这样的时机,不然Ruler在某些厂商的系统中,也会跟随键盘尺寸发生变化,就失去了比较意义。等待Ruler显示后,会触发onLayoutChange,且只触发这一次,则只需要在这一次回调中,去检测二者的高度差值,即为键盘高度值。

这样做,可以避免侵入。

  1. 低侵入性,只需要调用watch与unwatch即可;
  2. 无滞后性,由于不采用延迟任务的方式,所以没有滞后性;
  3. 无误触性,同样是因为没有采用延迟任务的方式,所以没有误触性;
  4. 稳定性,由于采用的是双布局的差值比较,所以不会因为横竖屏幕切换导致的触发键盘高度事件;

想要更多细节,请查看代码:skb-global。地址如下:
https://github.com/boybeak/skb-global

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
这一次,让EventBus纯粹一些
HarmonyOS Next(纯血鸿蒙)它到底像谁?

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注