Android — 通过ViewCompat和WindowInsetsCompat处理软键盘

在App的开发中,少不了与软键盘打交道,例如进入搜索页时主动显示软键盘、点击软键盘外区域主动收起软键盘等。本文介绍如何通过ViewCompatWindowInsetsCompat处理软键盘。

软键盘处理

判断软键盘是否显示

通过ViewCompatsetOnApplyWindowInsetsListener()方法,对当前WindowdecorView进行监听。在OnApplyWindowInsetsListener回调中,通过WindowInsetsCompat获取软键盘的高度,代码如下:

kotlin 复制代码
class KeyboardExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutKeyboardExampleActivityBinding

    private var keyboardShown = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutKeyboardExampleActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, windowInsets ->
            // 当窗口发生变化时,回调会被执行(例如系统状态栏变化、软键盘变化)
            windowInsets.getInsets(WindowInsetsCompat.Type.ime()).run {
                // 当软键盘显示时,bottom的值为软键盘的高度加导航栏的高度。软键盘隐藏时,bottom的值为0
                binding.tvKeyboardHeight.text = "keyboard height: $bottom"
                keyboardShown = bottom > 0
            }
            WindowInsetsCompat.CONSUMED
        }
    }
}

效果如图:

显示或隐藏软键盘

使用WindowInsetsControllerCompatshow()hide()方法,可以手动显示和隐藏软键盘,代码如下:

kotlin 复制代码
class KeyboardExampleActivity : AppCompatActivity() {

    ......

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        ......
        
        binding.btnShowKeyboard.setOnClickListener { showKeyboard(binding.etInputData) }
        binding.btnHideKeyboard.setOnClickListener { hideKeyboard(binding.etInputData) }
    }

    private fun showKeyboard(editText: View) {
        if (!keyboardShown) {
            editText.isFocusable = true
            editText.isFocusableInTouchMode = true
            editText.requestFocus()
            WindowInsetsControllerCompat(window, editText).show(WindowInsetsCompat.Type.ime())
        }
    }

    private fun hideKeyboard(editText: View) {
        if (keyboardShown) {
            editText.clearFocus()
            WindowInsetsControllerCompat(window, editText).hide(WindowInsetsCompat.Type.ime())
        }
    }
}

效果如图:

触摸非软键盘与输入框区域隐藏软键盘

重写ActivitydispatchTouchEvent()方法,判断手指的落点是否在输入框控件区域内,不是的话隐藏软键盘。落在软键盘上的点击会被软键盘消耗掉,不会调用ActivitydispatchTouchEvent方法。具体代码如下:

kotlin 复制代码
class KeyboardExampleActivity : AppCompatActivity() {

    ......

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        if (!isTouchTargetView(ev, binding.etInputData)) {
            // 判断不是触摸输入框的位置则隐藏软键盘
            currentFocus?.let { hideKeyboard(it) }
        }
        return super.dispatchTouchEvent(ev)
    }

    private fun isTouchTargetView(ev: MotionEvent?, view: View?): Boolean {
        return if (ev != null && view != null) {
            val screenLocation = IntArray(2)
            view.getLocationOnScreen(screenLocation)
            val viewX = screenLocation[0]
            val viewY = screenLocation[1]
            (ev.rawX >= viewX && ev.rawX <= (viewX + view.width)) && (ev.rawY >= viewY && ev.rawY <= (viewY + view.height))
        } else {
            false
        }
    }
}

效果如图:

无处理 添加处理

输入框被遮挡

有时候输入框可能在页面中的下半部分,甚至可能在底部,如果不做额外处理会被软键盘遮挡。

通过WindowsetSoftInputMode()方法,设置软键盘显示时通过平移窗口来保证焦点控件可见(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)。然后对比窗口可见区域和输入框的位置来判断是否需要进行处理,代码如下:

kotlin 复制代码
class KeyboardExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutKeyboardExampleActivityBinding

    private var keyboardShown = false

    private var needRestore = false
    
    ......

    private fun handleKeyboardObscuringView() {
        if (keyboardShown) {
            currentFocus?.let { focusView ->
                val rect = Rect()
                // 获取当前窗口的可见区域相对于页面根布局的位置和尺寸
                binding.root.getWindowVisibleDisplayFrame(rect)
                val locations = IntArray(2)
                // 获取当前焦点控件在页面中的位置
                focusView.getLocationInWindow(locations)
                // 当前窗口可见区域的底部高度小于焦点控件的y轴坐标加上它的高度时
                // 判定为软键盘已经遮挡住了控件
                if (rect.bottom < locations[1] + focusView.height) {
                    // 如果输入框被软件盘遮挡,则滚动页面至可以完全显示输入框的位置
                    binding.root.scrollTo(0, (locations[1] + focusView.height + focusView.height / 2) - rect.bottom)
                    needRestore = true
                }
            }
        } else if (needRestore) {
            // 需要时回滚到页面的顶部
            binding.root.scrollTo(0, 0)
        }
    }
}

效果如图:

无处理 添加处理

示例

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

相关推荐
HLC++11 小时前
Linux的进程间通信
android·linux·服务器
爱笑鱼15 小时前
Binder(二):AIDL 生成的 Proxy、Stub 和 Parcel 到底在做什么?
android
爱笑鱼15 小时前
Binder(一):一次方法调用,究竟怎样跨进程执行?
android
壮哥_icon16 小时前
【Android 系统开发】使用 BAT 脚本高效自动化管理 /system/priv-app/ 系统应用(安装与卸载)
android·运维·自动化
用户693717500138418 小时前
Claude Code终端日志Token占用实测:一个过滤器砍掉60%-90%
android·前端·后端
蝉蜕日记18 小时前
AccumuPDF高级版 v2.57 | 视图文转换PDF,合并分割压缩
android·智能手机·pdf·生活·软件需求
吐了啊取名字太难18 小时前
美颜系统AI修图本地跑并支持Mac、win、安卓、iOS不卡顿
android·人工智能·windows·数码相机·mac·ai编程
阿pin19 小时前
Android随笔-Retrofit
android·retrofit
想取一个与众不同的名字好难19 小时前
安卓自定义颜色选择器
android
我命由我1234520 小时前
Android 开发问题:java.lang.NoSuchMethodError:No virtual method readAllBytes()
android·java·java-ee·kotlin·android studio·android-studio·android runtime