控制软件键盘并为其添加动画效果

尝试使用 Compose 方式
Jetpack Compose 是推荐用于 Android 的界面工具包。了解如何在 Compose 中使用键盘。

通过使用 WindowInsetsCompat,您的应用可以查询和控制屏幕键盘(也称为 IME),就像它与系统栏互动一样。您的应用还可以使用 WindowInsetsAnimationCompat 在打开或关闭软件键盘时创建无缝过渡效果。

图 1. 软件键盘打开-关闭过渡的两个示例。

前提条件

在为软件键盘设置控制和动画效果之前,请将应用配置为全屏显示。这样一来,它就可以处理系统窗口边衬区,例如系统栏和屏幕键盘。

检查键盘软件的显示设置

使用 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. 同步了键盘动画。
  • 图 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。您可以使用它来保存开始状态,在本例中,开始状态是视图的底部坐标。

一张图片,显示了根视图的起始状态底部坐标。
图 3. 使用 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 回调,则系统会在此时调用该回调。此时是保存视图属性最终状态的好时机。

一张图片,显示视图的最终状态底部坐标
图 4. 使用 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,即原始布局位置。

图 5. 使用 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。此方法在动画结束时调用。现在是清理所有临时更改的好时机。

其他资源