Android应用中,比较常见的一个功能场景是在键盘上方放置一些View,给用户提供一些编辑相关的能力。又或者页面有些输入框,输入框需要在键盘起落的时候保证输入框完整可见。不管是哪种场景,这些view需要跟随键盘的起落事件而展示、隐藏。但是Android开发过程中键盘交互的事件是比较难“插手”的。本篇文章分享一下笔者在开发过程中自己的一些处理方法。
众所周知,Android开发中页面和键盘交互相关的是windowSoftInputMode。带window的页面都可以设置该属性,如Activity、Dialog、PopupWindow等。windowSoftInputMode的取值有四个:
- SOFT_INPUT_ADJUST_UNSPECIFIED
- SOFT_INPUT_ADJUST_RESIZE
- SOFT_INPUT_ADJUST_PAN
- SOFT_INPUT_ADJUST_NOTHING
这几个值的用法网上有很多博客有介绍,这里不再赘述。SOFT_INPUT_ADJUST_UNSPECIFIED
这个值不会有人使用吧,这个值会带来不确定性。SOFT_INPUT_ADJUST_RESIZE
这个值会挤压页面,同时它已经过时,大部分时候应该都不会再使用。现在大部分应用使用的是SOFT_INPUT_ADJUST_PAN
和SOFT_INPUT_ADJUST_NOTHING
。这两种模式各有好处又各带来了一些问题。
-
SOFT_INPUT_ADJUST_NOTHING
此模式下,页面在键盘起落的时候不会有任何动作变化,某些业务场景需要使用这种模式保证页面不会有一些奇怪的行文,但是有需要在键盘起落时候做一些其他处理,比如在键盘上方放置View。这时候适配的方案就要区分Android10及以下或以上版本。
Android10以上版本
Android11开始,Google提供了
setWindowInsertAnimationCallback
方法,这个方法搭配SOFT_INPUT_ADJUST_NOTHING
模式可以很好的实现将View放置在键盘之上,并且动画非常丝滑,使用方法可以参考Google Android官方文档。Android10及以下版本
setWindowInsertAnimationCallback
方法是在(不包含)Android10以后的版本才生效的,尽管提供了ViewCompat,但是在低版本上面还是没用。这样的话低版本的Android设备只能另辟蹊径寻找适配方案了。方案一 网上的方案
网上针对键盘事件的监听是通过在页面上弹出一个不可见的
PopupWindow
,通过把这个PopupWindow
的window的windowSoftInputMode属性设置成SOFT_INPUT_ADJUST_RESIZE
,然后监听PopupWindow的rootview的window.decorView.viewTreeObserver.addOnGlobalLayoutListener
事件,最后在回调中通过屏幕高度、PopupWindow的rootview的高度等值计算的到一个差值来认为是键盘的高度值。这种方式是比较粗糙的,因为这样的计算需要考虑底部导航栏、顶部状态栏、沉浸式等问题。这个方案效果不是很好。方案二 改进的方案
方案一虽然实现的最终效果可能不太好,不过也是有可取之处。基于方案一,在之前的某个项目笔者改进了一下,的出了一个还不错的方案。还是需要一个不可见的
PopupWindow
,通过把这个PopupWindow
的window的windowSoftInputMode属性设置成SOFT_INPUT_ADJUST_PAN
模式,然后给这个PopupWindow
的rootview添加setOnApplyWindowInsetsListener
监听,注意需要使用ViewCompat,因为是低版本。最后由于低版本的Android设备并不会在键盘起落的每次事件都会调用setOnApplyWindowInsetsListener
回调,所以还是需要监听PopupWindow的rootview的window.decorView.viewTreeObserver.addOnGlobalLayoutListener
事件,在这个事件里面调用requestApplyInsets
方法,确保键盘起落的时候每次都会调用回调(有需要的话可以在回调里面对事件进行去重处理)。setOnApplyWindowInsetsListener
监听获取的是系统返回来的各种系统窗口的事件数据信息,可以获取键盘的展示状态、状态栏、导航栏的数据等,都是比较准确的,避免像方案一中那样去计算获取一些错误值的问题。 -
SOFT_INPUT_ADJUST_PAN
此模式会比
SOFT_INPUT_ADJUST_NOTHING
更常用。而此模式下的键盘交互适配却更复杂一些。因为此模式下,键盘起落的时候,页面是有可能会跟随平移的。在获取终止的适配数值的时候需要知道页面伴随平移的值。笔者通过断点源码及阅读源码发现页面伴随键盘平移是由ViewRootImpl
里面的Scrollor
对象来执行的,因此只要拿到这个对象然后再获取该对象的mDeltaY
变量即为页面的伴随平移的值。ViewRootImpl
是系统类,应用开发是无法直接访问的,这时候就必须使用到反射技术了。由于一个window对应一个ViewRootImpl
对象,而每一个window的rootview的parent
刚好就是该window绑定的ViewRootImpl
对象(可以自行去看源码)。因此反射代码:private fun getViewRootImpl(view: View): Any? { val parent: ViewParent? = view.rootView?.parent try { val viewRootImplClass = Class.forName("android.view.ViewRootImpl") if (viewRootImplClass.isInstance(parent)) { return parent } } catch (e: ClassNotFoundException) { Log.e(TAG, "$e") } return null }
获取
mDeltaY
变量:private fun getRootViewScrollDistance(view: View): Int { val viewRootImpl = getViewRootImpl(view) viewRootImpl?.let {impl-> try { val scrollerField : Field? = impl.javaClass.getDeclaredField("mScroller") scrollerField?.isAccessible = true val scroller = scrollerField?.get(impl) scroller?.let { val deltaYField: Field? = scroller.javaClass.getDeclaredField("mDeltaY") deltaYField?.isAccessible = true return (deltaYField?.get(scroller) as? Float)?.toInt() ?: 0 } } catch (e: Exception) { } } return 0 }
只要知道了页面伴随平移的值,再通过
setOnApplyWindowInsetsListener
监听,便可以很好的适配键盘起落事件了(低版本要调用requestApplyInsets
方法)确保回调事件正常。因为页面伴随键盘起落的时候使用的
scrollTo
方法(详见ViewRootImpl源码),所以如果适配过程中页面需要二次平移的话,建议还是用scrollTo
方法。笔者尝试了看起来是比较顺畅。EditText的适配
如果使用
scrollTo
方法进行二次平移,页面包含有EditText的话,在EditText输入的时候,页面会自动回滚,这是因为EditText的输入监听里面会触发ViewRootImpl里面的滚动方法。可以重写EditText,然后覆盖其中的requestRectangleOnScreen
方法,并返回true。以上,几乎纯文字的描述,比较枯燥,整体方案不算难,容易理解。动手试一试即可体验