【Android爬坑日记】监听软键盘实现丝滑的输入DialogFragment

3,631 阅读5分钟

再懒也要逼自己每个星期输出至少一篇文章,哪怕没人看。这篇是Android爬坑日记第三篇,也是小鹅爬坑日记的第二篇,小鹅事务所是我开源的记录事务APP可以看看我之前的一篇文章。

什么是输入Dialog,输入Dialog一般出现在评论区、聊天框,我下面放一张动图,分别是微博轻享版,掘金和微信(当然微信这个不是Dialog)

输入框对比.gif

软键盘入场动画

不知道大家有没有发现,微博和掘金的效果是一样的,也是市面上大部分产品的软键盘入场效果。虽然不影响使用,但是视觉上的效果会差一点。微信的软键盘入场动画是贴着输入框的,看起来非常丝滑流畅。

动画交互非常影响用户对APP的第一印象。 —— 米奇律师

以下我将以【小鹅事务所】的输入框为案例来实现流畅的软键盘DialogFragment输入框动画。

效果

优化前后.gif

好像区别也不是很大。

细节决定着一个APP的品质。 —— 米奇律师

先上代码:github.com/MReP1/Littl…

爬坑

使用Dialog来实现会比较好管理,由于这个输入框在底部,因此使用了BottomSheetDialogFragment来实现。并且弹出Dialog的时候弹出软键盘,软键盘收起的同时关闭Dialog。

BottomSheetDialogFragment

BottomSheetDialogFragment是Material Design里的一个组件。自带了缓入缓出的入场出场动画。如果需要自己到XML文件里写动画,比较难实现缓入缓出的效果,因此考虑到我比较懒这一点,直接使用BottomSheetDialogFragment是明智之选。

弹出软键盘

坑点

弹出Dialog的时候同时弹出软键盘也是有坑的,如果说到网上去找工具类,大概率软键盘弹不出来。举个例子:

object KeyBoard {
    fun show(focusView: View){
        val inputManager = appContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        inputManager.showSoftInput(focusView, InputMethodManager.SHOW_IMPLICIT)
    }
}

如果在DialogFragment中使用这个工具方法,你会发现软键盘弹不出来。

class InputTextDialogFragment : BottomSheetDialogFragment() {    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.etInput.requestFocus()
        KeyBoard.show(binding.etInput)
    }
}

我猜测是因为Fragment的视图没有绑定到Dialog的Window上,或者Dialog还没有弹出来,导致没有办法弹出软键盘。

这个时候就可能会有人告诉大家:这还不简单,我等它几百毫秒绑定好了再弹不就好了。

class InputTextDialogFragment : BottomSheetDialogFragment() {    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.etInput.postDelay(300) {
            binding.etInput.requestFocus()
            KeyBoard.show(binding.etInput)
        }
    }
}

这种方法还真可以,但是在我看来不优雅。

于是我看了米奇律师的沉浸式状态栏灵光一动写出了以下代码。

binding.etInput.doOnAttach { 
    binding.etInput.requestFocus()
    KeyBoard.show(binding.etInput)
}

很遗憾这种方法也不能实现,View绑定到Root View了,但是可能Dialog还没有弹出来,因此软键盘也弹不出来,具体原因我没有深究,有不同理解的欢迎到评论区交流。

解决方法

其实想要启动DialogFragment的时候弹出软键盘没那么复杂。只需要给Dialog所在的Window设置以下软键盘输入标志位就好了,只要Dialog一出现,软键盘马上弹出来。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    binding.etInput.requestFocus()
    dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
}

监听软键盘弹起

在Android API 达到30及以上,Android引入了WindowInsetsAnimationAPI,此时就可以监听WindowInsets的占用屏幕大小的变化了。因此就可以实现类似于图1动图中微信那样流畅的动画了。那么具体怎么实现呢?我先放一下代码,在注释中讲解一下。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    ViewCompat.setWindowInsetsAnimationCallback(
        binding.root,
        object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
            // 存放根视图的的初始高度
            private var startHeight = 0
            private var lastDiffH = 0
            
            override fun onPrepare(animation: WindowInsetsAnimationCompat) {
                if (startHeight == 0) {
                    // 此处赋值根视图的初始高度,由于动画开始前,根视图已经绑定到Window上了
                    // 因此能够获得初始高度
                    startHeight = binding.root.height
                }
            }
            override fun onProgress(
                insets: WindowInsetsCompat,
                runningAnimations: MutableList<WindowInsetsAnimationCompat>
            ): WindowInsetsCompat {
                // 获取软键盘的Inset
                val typesInset = insets.getInsets(WindowInsetsCompat.Type.ime())
                // 获取系统状态栏、导航栏的Inset
                val otherInset = insets.getInsets(WindowInsetsCompat.Type.systemBars())
                // 获取它们的差值
                val diff = Insets.subtract(typesInset, otherInset).let {
                    Insets.max(it, Insets.NONE)
                }
                
                // 获取需要调整的高度
                val diffH = abs(diff.top - diff.bottom)
                
                // 父布局为 wrap_content 适应高度,而所有子布局都是贴着父布局底部的
                // 因此只需调整bottomMargin,子布局往上走的同时父布局高度自适应
                binding.etInput.updateLayoutParams<ViewGroup.MarginLayoutParams> {
                    bottomMargin = diffH
                }
                
                // 当察觉到软键盘高度越来越小(说明在收起了),就可以dismiss该DialogFragment了
                if (diffH < lastDiffH) {
                    dismiss()
                    ViewCompat.setWindowInsetsAnimationCallback(binding.root, null)
                }
                
                // 存放每一次软键盘的高度
                lastDiffH = diffH
                return insets
            }
        }
    )
}

我的布局是贴着父layout底部的,所以需要调整marginBottom。

记得给该Window设置软键盘的插入模式为WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING,这意味着该DialogFragment不会被软键盘顶上去,也就是说软键盘背后的内容其实也是View的一部分,但是是空白的。

dialog?.window?.setSoftInputMode(
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE or WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
    } else {
        WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
    }
)

这里还需要分情况,在Android11以下的手机无法使用WindowInsetsAnimation API,因此就没有办法用这个API享受到如此丝滑的动画啦。

其实在这个动图中,左图为Android10的效果,右图为Android11的效果,你会发现左图的界面没有贴着软键盘走,而右图的界面稳稳地贴着软键盘。

优化前后.gif

那么Android11以下如何监听软键盘收起dismiss掉DialogFragment呢?

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    // Android11 以上动画
} else {
    binding.root.viewTreeObserver.addOnGlobalLayoutListener(object :
        ViewTreeObserver.OnGlobalLayoutListener {
        var lastBottom = 0
        override fun onGlobalLayout() {
            ViewCompat.getRootWindowInsets(binding.root)?.let { insets ->
                val bottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
                if (lastBottom != 0 && bottom == 0) {
                    // 收起键盘了,可以 dismiss 了
                    dismiss()
                    binding.root.viewTreeObserver.removeOnGlobalLayoutListener(this)
                }
                lastBottom = bottom
            }
        }
    })
}

lastBottom存放软键盘的高度,当它被赋值不为0时,说明软键盘弹出来了,此时如果将它赋值为0,也就也为这软键盘收起了,就可以dissmiss掉DialogFragment了。

总结

文章结束啦,再放一遍代码吧!

这个实现或许看起来也不算很优雅,可能不是最佳实践,是我界面交互优化的一个尝试,有的时候UI给的设计稿是静态的,作为开发赶工期肯定是怎么方便怎么来,功能实现就行、UI设计师不提BUG就行。但是谷歌团队提供了这么多好用的API来给大家优化界面,在如今大家手机都性能过剩的背景下,把主线程腾出点空间用于更舒服的用户交互,也未尝不是一种应用优化。

其实我个人比较喜欢上层界面层的开发,基于这篇文章我再开一个坑吧,关于交互、动画,有空慢慢填坑。

参考

zhuanlan.zhihu.com/p/343022200