UI简化开发

Animate Your Keyboard 安卓键盘动画实践

2021-12-20  本文已影响0人  小强开学前

Google 给的实践效果

1. 基础知识准备

androidx.core下集成了对屏幕内影响界面布局元素的监听及查看支持,状态栏、键盘、手势区域等,现在统一叫 Insets。
涉及到键盘的主要有

1. 谷歌示例源码分析

Google 界面布局最外层为 LinearLayout,内部主要是 RecyclerView + 作为输入框的容器LinearLayout

1. Window 设置

使用 WindowCompat#setDecorFitsSystemWindows(window,false),设置当前 decorView 不响应 Insets。

2. 最外层 View

给布局最外层 View 也就是 RootView 设置WindowInsetsAnimationCallback
重写onPrepare判断如果是键盘动画,则设置deferredInsets = true
重写onProgress但是不做任何操作,直接返回参数 insets
重写onEnd,如果deferredInsets == true则重写设置deferredInsets = false,然后调用dispatchApplyWindowInsets向下传递一次 insets。

同时设置OnApplyWindowInsetsListener,给自身设置 Padding,然后返回 WindowInsetsCompat.CONSUME不继续向下分发。
这里记录下这次传入的WindowInsetsCompat赋值给lastWindowInsets
同时设置 Padding 时会判断deferredInsets,这个逻辑特别重要如果是false,会将键盘与导航栏的高度取最高值作为 padding,true 则会将导航栏高度作为 padding。

3. 容器 LinearLayout

此为输入框的容器,给它设置WindowInsetsAnimationCallback,重写onProgress根据当前键盘位置设置自己的translationY,重写onEnd在键盘动画结束时重置translationY为0

4. TextInputLayout

给 TextInputLayout 设置WindowInsetsAnimationCallback,重写onEnd,让其能在键盘动画结束时根据当前键盘状态执行获取焦点或者清除焦点的操作

5. RecyclerView

这里与第3点中容器 LinearLayout 的逻辑完全一致,因为都是键盘慢慢弹起,它们慢慢上移。

6. 为什么谷歌的代码会产生预期的效果

首先我们打印下生命周期

弹起

onPrepare                           // 设置标志位 `deferredInsets == true`,我们要开始动画了
onApplyWindowInsets: Insets{left=0, top=66, right=0, bottom=132} // 因为`deferredInsets`为true,我们只使用导航栏高度作为 padding
onProgress  // 啥也没干,分发
RecyclerView ->onProgress           // 通过设置`translationY`开始上移
LinearLayout ->onProgress           // 通过设置`translationY`开始上移
AppCompatEditText ->onProgress      // 啥也没干
...
...
...
...
onProgress                          // 啥也没干,分发
RecyclerView ->onProgress           // 通过设置`translationY`开始上移
LinearLayout ->onProgress           // 通过设置`translationY`开始上移
AppCompatEditText ->onProgress      // 啥也没干
onEnd                               // 设置`deferredInsets = false`,再次调用`onApplyWindowInsets()`
onApplyWindowInsets: Insets{left=0, top=66, right=0, bottom=912} // 将键盘高度作为 padding
RecyclerView ->onEnd                // 重置`translationY`
LinearLayout ->onEnd                // 重置`translationY`
AppCompatEditText ->onEnd           // 键盘可见,requestFocus()

收起

onPrepare                           // 设置标志位 `deferredInsets == true`,我们要开始动画了
onApplyWindowInsets: Insets{left=0, top=66, right=0, bottom=132} // 因为`deferredInsets`为true,我们只使用导航栏高度作为 padding,
onProgress                          // 啥也没干,分发
RecyclerView ->onProgress           // 通过设置`translationY`开始下移
LinearLayout ->onProgress           // 通过设置`translationY`开始下移
AppCompatEditText ->onProgress      // 啥也没干
...
...
...
...
onProgress                          // 啥也没干,分发
RecyclerView ->onProgress           // 通过设置`translationY`开始下移
LinearLayout ->onProgress           // 通过设置`translationY`开始下移
AppCompatEditText ->onProgress      // 啥也没干
onEnd                               // 设置`deferredInsets = false`,再次调用`onApplyWindowInsets()`
onApplyWindowInsets: Insets{left=0, top=66, right=0, bottom=132}    // 使用导航栏高度作为 padding
RecyclerView ->onEnd                // 重置`translationY`
LinearLayout ->onEnd                // 重置`translationY`
AppCompatEditText ->onEnd           // 键盘不可见,clearFocus()

这里我们分阶段分析

7. 谷歌动画的问题点

弹起结束时,rootView 的onEnd()会将 Padding 更新为键盘高度了,然后传递给下一个 View 回调 onEnd
在这个时间段内其实其他View并未及时将 translationY 清零,所以需要这里的时间足够短
否则执行位移动画的 View 会有一瞬间在很上面然后再归位。

同理,在收起开始时,rootView 的onApplyWindowInsets()会将 Padding 更新为导航栏高度了,然后才会回调onProgress()
在这个时间段内其实其他View translationY 依然为0,所以这里的时间也需要足够短
否则执行位移动画的 View 会有一瞬间在很下面然后再归位。

2. 处理我们自己动画的需求

需求还是比较简单的,除了 EditText 其他完全不用管,键盘弹起,显示输入框并获取焦点,键盘隐藏,隐藏输入框并清除焦点
由于需要改变其位置,同时需要响应动画,很明显ViewCompat#setOnApplyWindowInsetsListenerViewCompat.setWindowInsetsAnimationCallback是都需要的

实现过程中发现如果直接通过 Insets 判断键盘状态来显示或者隐藏输入框,会影响其他焦点的逻辑(比如页面上有第二个输入框)
效果(显示隐藏的画面不小心被裁掉了)


键盘动画

源码

TranslateInsetsAnimationListener.java

优化

以上是最终代码,实现过程中有很多由于机型、安卓版本等问题导致的特殊情况。
现在流程为点击按钮,显示 EditText,弹出键盘
动画中只对 EditText focus 进行操作
然后在UI层对 focus 进行监听,失去焦点则隐藏 EditText

上一篇下一篇

猜你喜欢

热点阅读