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

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

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

软键盘进场动画

不知道我们有没有发现,微博和的作用是相同的,也是市面上大部分产品的软键盘进场作用。虽然不影响运用,可是视觉上的作用会差一点。微信的软键盘进场动画是贴着输入框的,看起来非常丝滑流通。

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

以下我将以【小鹅业务所】的输入框为案例来完成流通的软键盘DialogFragment输入框动画。

作用

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

如同差异也不是很大。

细节决定着一个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的作用,你会发现左图的界面没有贴着软键盘走,而右图的界面稳稳地贴着软键盘。

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

那么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