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

Попробуйте Compose
Jetpack Compose – это рекомендуемый набор инструментов для создания пользовательского интерфейса в Android. Узнайте, как работать с клавиатурой в Compose.

С помощью WindowInsetsCompat приложение может запрашивать информацию об экранной клавиатуре (также называемой IME) и управлять ею так же, как и системными строками. Приложение также может использовать WindowInsetsAnimationCompat, чтобы обеспечить плавные переходы при открытии или закрытии виртуальной клавиатуры.

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

Требования

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

Анимация синхронизированных вставок по умолчанию

Начиная с уровня API 37.2 приложения, которые используют стандартные настройки макета окна (например, SOFT_INPUT_ADJUST_RESIZE или SOFT_INPUT_ADJUST_PAN при показе IME с помощью WindowInsetsController.show(WindowInsets.Type.ime())) без реализации пользовательского WindowInsetsAnimationCompat.Callback, могут использовать синхронизированную покадровую анимацию на уровне системы. Когда эта функция включена, она автоматически применяет вставки к каждому кадру и запускает проход макета, создавая плавный переход при перемещении клавиатуры.

Чтобы включить эту функцию, добавьте в файл AndroidManifest.xml следующий тег <property>:

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

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