通过使用 WindowInsetsCompat,您的应用可以查询和控制屏幕键盘(也称为 IME),就像它与系统栏互动的方式一样。您的应用还可以使用 WindowInsetsAnimationCompat 在打开或关闭软件键盘时创建无缝过渡效果。
前提条件
在为软件键盘设置控制和动画效果之前,请将应用配置为全屏显示。这样一来,它就可以处理系统窗口边衬区,例如系统栏和屏幕键盘。
默认的同步边衬区动画
从 API 级别 37.2 及更高版本开始,使用默认窗口布局调整(例如使用 WindowInsetsController.show(WindowInsets.Type.ime()) 显示 IME 时使用 SOFT_INPUT_ADJUST_RESIZE 或 SOFT_INPUT_ADJUST_PAN)但未实现自定义 WindowInsetsAnimationCompat.Callback 的应用可以受益于系统级同步逐帧动画。启用后,此功能会自动在每个帧上应用边衬区,并触发布局传递,从而在键盘移动时创建平滑的过渡效果。
如需启用此行为,请将以下 <property> 标记添加到 AndroidManifest.xml 文件中的 activity 或应用:
<property
android:name="android.window.PROPERTY_COMPAT_ALLOW_SYNCHRONIZED_INSETS_ANIMATION"
android:value="true" />
在主线程之外执行边衬区动画
默认情况下,注册 WindowInsetsAnimationCompat.Callback 会强制窗口边衬区动画在应用的主线程上运行。如果您的应用在动画期间不断更新和同步其视图布局(例如在 onProgress 方法中),则必须执行此操作。不过,如果您的应用只需要监听生命周期转换(例如 IME 开始或结束动画时),而不需要逐帧更新视图,那么在主线程上运行可能会导致不必要的开销和动画卡顿,尤其是在主线程本身很忙的情况下。
从 API 级别 37.2 及更高版本开始,您可以使用 ViewTreeObserver.WindowInsetsAnimationListener 来观察这些过渡。由于此监听器不会接收逐帧进度更新(没有 onProgress 回调),因此系统可以在专用动画线程(而非主线程)上运行插屏动画,从而实现更平滑的过渡。
Kotlin
val listener = object : ViewTreeObserver.WindowInsetsAnimationListener { override fun onPrepare(animation: WindowInsetsAnimation) { // Handle preparation before animation starts } override fun onEnd(animation: WindowInsetsAnimation) { // Clean up temporary changes } } // Add the listener view.viewTreeObserver.addWindowInsetsAnimationListener(listener) // Remove the listener when no longer needed view.viewTreeObserver.removeWindowInsetsAnimationListener(listener)
Java
ViewTreeObserver.WindowInsetsAnimationListener listener = new ViewTreeObserver.WindowInsetsAnimationListener() { @Override public void onPrepare(@NonNull WindowInsetsAnimation animation) { // Handle preparation before animation starts } @Override public void onEnd(@NonNull WindowInsetsAnimation animation) { // Clean up temporary changes } }; // Add the listener view.getViewTreeObserver().addWindowInsetsAnimationListener(listener); // Remove the listener when no longer needed view.getViewTreeObserver().removeWindowInsetsAnimationListener(listener);
检查键盘软件的显示设置
使用 WindowInsets 检查软件键盘的可见性。
Kotlin
val insets = ViewCompat.getRootWindowInsets(view) ?: return val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
Java
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view); boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
或者,您也可以使用 ViewCompat.setOnApplyWindowInsetsListener 来观察软件键盘可见性的变化。
Kotlin
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets -> val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom insets }
Java
ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> { boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom; return insets; });
使动画与软件键盘同步
当用户点按文本输入字段时,键盘会从屏幕底部滑入到位,如以下示例所示:
图 2 中标记为“未同步”的示例展示了 Android 10(API 级别 29)中的默认行为,即文本字段和应用内容会快速到位,而不是与键盘的动画同步,这种行为可能会造成视觉上的不适。
在 Android 11(API 级别 30)及更高版本中,您可以使用
WindowInsetsAnimationCompat将应用的过渡与键盘从屏幕底部向上和向下滑动的过渡同步。这样看起来会更流畅,如图 2 中标记为“已同步”的示例所示。
配置 WindowInsetsAnimationCompat.Callback,以使视图与键盘动画同步。
Kotlin
ViewCompat.setWindowInsetsAnimationCallback( view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { // Override methods. } )
Java
ViewCompat.setWindowInsetsAnimationCallback( view, new WindowInsetsAnimationCompat.Callback( WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP ) { // Override methods. });
WindowInsetsAnimationCompat.Callback 中有多种方法可供替换,即 onPrepare()、onStart()、onProgress() 和 onEnd()。在进行任何布局更改之前,先调用 onPrepare()。
当插边动画开始播放时,以及在视图因动画而重新布局之前,系统会调用 onPrepare。您可以使用它来保存开始状态,在本例中,开始状态是视图的底部坐标。
onPrepare() 记录开始状态。
以下代码段显示了对 onPrepare 的调用示例:
Kotlin
var startBottom = 0f override fun onPrepare( animation: WindowInsetsAnimationCompat ) { startBottom = view.bottom.toFloat() }
Java
float startBottom; @Override public void onPrepare( @NonNull WindowInsetsAnimationCompat animation ) { startBottom = view.getBottom(); }
当插边动画开始播放时,系统会调用 onStart。您可以使用它将所有视图属性设置为布局更改的最终状态。如果您已为任何视图设置 OnApplyWindowInsetsListener 回调,则系统会在此时调用该回调。此时是保存视图属性最终状态的好时机。
onStart() 记录结束状态。
以下代码段显示了对 onStart 的调用示例:
Kotlin
var endBottom = 0f override fun onStart( animation: WindowInsetsAnimationCompat, bounds: WindowInsetsAnimationCompat.BoundsCompat ): WindowInsetsAnimationCompat.BoundsCompat { // Record the position of the view after the IME transition. endBottom = view.bottom.toFloat() return bounds }
Java
float endBottom; @NonNull @Override public WindowInsetsAnimationCompat.BoundsCompat onStart( @NonNull WindowInsetsAnimationCompat animation, @NonNull WindowInsetsAnimationCompat.BoundsCompat bounds ) { endBottom = view.getBottom(); return bounds; }
当插边在运行动画时发生变化时,系统会调用 onProgress,因此您可以替换它,并在键盘动画期间的每一帧收到通知。更新视图属性,使视图与键盘同步动画显示。
此时,所有布局更改均已完成。例如,如果您使用 View.translationY 来平移视图,则每次调用此方法时,该值都会逐渐减小,最终达到 0,即原始布局位置。
onProgress() 同步动画。
以下代码段显示了对 onProgress 的调用示例:
Kotlin
override fun onProgress( insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat> ): WindowInsetsCompat { // Find an IME animation. val imeAnimation = runningAnimations.find { it.typeMask and WindowInsetsCompat.Type.ime() != 0 } ?: return insets // Offset the view based on the interpolated fraction of the IME animation. view.translationY = (startBottom - endBottom) * (1 - imeAnimation.interpolatedFraction) return insets }
Java
@NonNull @Override public WindowInsetsCompat onProgress( @NonNull WindowInsetsCompat insets, @NonNull List<WindowInsetsAnimationCompat> runningAnimations ) { // Find an IME animation. WindowInsetsAnimationCompat imeAnimation = null; for (WindowInsetsAnimationCompat animation : runningAnimations) { if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) { imeAnimation = animation; break; } } if (imeAnimation != null) { // Offset the view based on the interpolated fraction of the IME animation. view.setTranslationY((startBottom - endBottom) * (1 - imeAnimation.getInterpolatedFraction())); } return insets; }
您可以选择替换 onEnd。此方法在动画结束时调用。现在是清理所有临时更改的好时机。
其他资源
- GitHub 上的 WindowInsetsAnimation。