Управление и анимация программной клавиатуры

Используя WindowInsetsCompat , ваше приложение может запрашивать экранную клавиатуру (также называемую IME ) и управлять ею аналогично тому, как она взаимодействует с системными панелями. Ваше приложение также может использовать WindowInsetsAnimationCompat для создания плавных переходов при открытии или закрытии программной клавиатуры.

Рисунок 1. Два примера перехода «открыто-закрыто» программной клавиатуры.

Предварительные условия

Прежде чем настраивать управление и анимацию для программной клавиатуры, настройте свое приложение для отображения от края до края . Это позволяет ему обрабатывать вставки системных окон , такие как системные панели и экранную клавиатуру.

Проверьте видимость программного обеспечения клавиатуры

Используйте WindowInsets , чтобы проверить видимость программной клавиатуры.

Котлин

val insets = ViewCompat.getRootWindowInsets(view) ?: return
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

Ява

WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;

Альтернативно вы можете использовать ViewCompat.setOnApplyWindowInsetsListener для наблюдения за изменениями видимости программной клавиатуры.

Котлин

ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
  val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
  val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
  insets
}

Ява

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 с представлением, которое будет синхронизироваться с анимацией клавиатуры.

Котлин

ViewCompat.setWindowInsetsAnimationCallback(
  view,
  object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
    // Override methods.
  }
)

Ява

ViewCompat.setWindowInsetsAnimationCallback(
    view,
    new WindowInsetsAnimationCompat.Callback(
        WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
    ) {
      // Override methods.
    });

В WindowInsetsAnimationCompat.Callback можно переопределить несколько методов, а именно onPrepare() , onStart() , onProgress() и onEnd() . Начните с вызова onPrepare() перед любым изменением макета.

onPrepare вызывается при запуске анимации вставок и до того, как представления будут перерисованы из-за анимации. Вы можете использовать его для сохранения начального состояния, которое в данном случае является нижней координатой представления.

Изображение, показывающее нижнюю координату начального состояния корневого представления.
Рисунок 3. Использование onPrepare() для записи начального состояния.

В следующем фрагменте показан пример вызова onPrepare :

Котлин

var startBottom = 0f

override fun onPrepare(
  animation: WindowInsetsAnimationCompat
) {
  startBottom = view.bottom.toFloat()
}

Ява

float startBottom;

@Override
public void onPrepare(
    @NonNull WindowInsetsAnimationCompat animation
) {
  startBottom = view.getBottom();
}

onStart вызывается, когда начинается анимация вставки. Вы можете использовать его, чтобы установить все свойства представления в конечное состояние изменений макета. Если у вас есть обратный вызов OnApplyWindowInsetsListener установленный для любого из представлений, он уже вызывается на этом этапе. Сейчас самое время сохранить конечное состояние свойств представления.

Изображение, показывающее нижнюю координату конечного состояния вида.
Рисунок 4. Использование onStart() для записи конечного состояния.

В следующем фрагменте показан пример вызова onStart :

Котлин

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
}

Ява

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 :

Котлин

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
}

Ява

@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 . Этот метод вызывается после завершения анимации. Это хорошее время, чтобы убрать любые временные изменения.

Дополнительные ресурсы