Animate Your Keyboard 安卓键盘动画实践

Google 给的实践效果

1. 基础知识准备

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

  • WindowCompat#setDecorFitsSystemWindows 用来设置 DecorView 是否响应 Insets。

  • ViewCompat#setOnApplyWindowInsetsListener 用来设置特定 View 是否响应 Insets,并且支持到 API 21,API 21 以下没有效果,Android SDK 内部的 View#setOnApplyWindowInsetsListener 需要至少 API 20,很奇怪。

  • ViewCompat.setWindowInsetsAnimationCallback 用来设置特定 View 对类似键盘这种动画的回调。

  • 正常回调顺序

    onPrepare
    onApplyWindowInsets
    onProgress
    ...
    onProgress
    onEnd
    onApplyWindowInsets

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()

这里我们分阶段分析

  • 调用显示键盘的代码

    观察到有时候onPrepare()前也会回调一次 onApplyWindowInsets()但是键盘高度还没变,所以不影响
    但肯定的是onPrepare()后一定会回调一次onApplyWindowInsets()但是这时候标志位是true
    所以键盘弹起的时候,无论如何,布局不会发生任何变化。

  • 键盘慢慢弹起

    随着键盘慢慢弹起,所有 View 会开始回调受onProgress()方法影响,并且是有固定顺序的
    界面会发生 RecyclerView 和 容器 LinearLayout 慢慢上移的变化

  • 键盘展示完成

    WindowInsetsAnimationCallback会回调onEnd()方法,然后主动调用 rootView 的onWindowInsetsChanged()
    translationY清零,EditText根据当前状态决定是获取焦点还是清楚焦点

  • 键盘高度突然改变

    这里需要考虑到的一点是,键盘由于语言变化或者本身能调整高度,是会在已经弹起的时候发生改变的
    由于onEnd标志位已经重新设置为false,所以这里也是能够处理的

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

  • 主动调起键盘代码存在兼容性问题,多次测试发现不同代码适用不同版本安卓

    最后使用了版本判断来解决

    public static void showKeyboardCompat(@NonNull View view){
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
            InputMethodManager systemService = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                 systemService.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
       }else{
            showKeyboard(view);
       }
    }
            
    public static void showKeyboard(@NonNull View view) {
        int type = WindowInsetsCompat.Type.ime();
        WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(view);
        if (controller != null) {
            controller.show(type);
        }
        view.requestFocus();
    }
    
    • requestFocus()或者setVisibility()写在onApplyWindowInsets()中导致奇奇怪怪的BUG

      测试过程中出现过UI不更新、动画卡顿的问题,具体还是要依靠自己反复调试

    • 动画结束的checkFocus()应该加上当前是否有焦点的判断

      见代码注释

    • Insets 拦截、传递在各版本上的异同

      在布局中与 EditText 同级的 View 也设置setOnApplyWindowInsetsListener
      测试发现,如果onApplyWindowInsets返回 WindowInsetsCompat.CONSUME
      Android 7.1.1 Smartisan Nut Pro OD105 动画不执行
      Android 8.1.0 Smartisan Nut R1 DE106 动画不执行
      Android 12 Google Pixel3 动画正常
      Android 12 API 31 Pixel 3 模拟器 动画正常

      直接返回 insets,则各机器都正常响应

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容