控制和动画软件键盘

使用 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 在内边距动画开始时以及视图由于动画而重新布局之前被调用。您可以使用它来保存起始状态,在本例中是视图的底部坐标。

An image showing the start state bottom coordinate of the root view.
图 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 回调,则它此时已被调用。这是一个保存视图属性结束状态的好时机。

An image showing the end state bottom coordinate of the view
图 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。此方法在动画结束后被调用。这是一个清理任何临时更改的好时机。

其他资源