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

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

借助 WindowInsetsCompat, 您的应用可以查询和控制屏幕键盘(也称为 IME),方式与 应用与系统栏互动的方式类似。您的应用还可以使用 WindowInsetsAnimationCompat 在打开或关闭软件键盘时创建无缝过渡。

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

前提条件

在为软件键盘设置控制和动画之前,请配置 您的应用以显示边缘到边缘。这样,应用便可以处理系统窗口插边,例如 系统栏和屏幕键盘。

默认同步插边动画

从 API 级别 37.2 及更高版本开始,使用默认窗口布局调整(例如,使用 WindowInsetsController.show(WindowInsets.Type.ime()) 显示 IME 时使用 SOFT_INPUT_ADJUST_RESIZESOFT_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. 同步键盘动画。
  • 图 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。此方法会在动画结束后调用。建议您在此时清理所有临时更改。

其他资源