Cómo controlar y animar el teclado en pantalla

Prueba hacerlo con Compose
Jetpack Compose es el kit de herramientas de IU recomendado para Android. Obtén información para trabajar con el teclado en Compose.

Con WindowInsetsCompat, tu app puede consultar y controlar el teclado en pantalla (también llamado IME) de manera similar a como interactúa con las barras del sistema. Tu app también puede usar WindowInsetsAnimationCompat para crear transiciones fluidas cuando se abre o cierra el teclado de software.

Figura 1. Dos ejemplos de la transición de apertura y cierre del teclado en pantalla.

Requisitos previos

Antes de configurar el control y la animación del teclado en pantalla, configura tu app para que se muestre de borde a borde. Esto le permite controlar las inserciones de ventanas del sistema, como las barras del sistema y el teclado en pantalla.

Animación predeterminada de inserciones sincronizadas

A partir del nivel de API 37.2 y versiones posteriores, las aplicaciones que usan los ajustes de diseño de ventana predeterminados (como SOFT_INPUT_ADJUST_RESIZE o SOFT_INPUT_ADJUST_PAN cuando se muestra el IME con WindowInsetsController.show(WindowInsets.Type.ime())) sin implementar un WindowInsetsAnimationCompat.Callback personalizado pueden beneficiarse de las animaciones sincronizadas fotograma por fotograma a nivel del sistema. Cuando se habilita, esta función aplica automáticamente las inserciones en cada fotograma y activa un pase de diseño, lo que crea una transición fluida a medida que se mueve el teclado.

Para habilitar este comportamiento, agrega la siguiente etiqueta <property> a tu actividad o aplicación en el archivo AndroidManifest.xml:

<property
    android:name="android.window.PROPERTY_COMPAT_ALLOW_SYNCHRONIZED_INSETS_ANIMATION"
    android:value="true" />

Realiza animaciones de inserciones fuera del subproceso principal

De forma predeterminada, el registro de un WindowInsetsAnimationCompat.Callback exige que la animación de inserción de la ventana se ejecute en el subproceso principal de la app. Esto es necesario si tu app actualiza y sincroniza continuamente el diseño de la vista durante la animación (como en el método onProgress). Sin embargo, si tu app solo necesita detectar transiciones de ciclo de vida (por ejemplo, cuando el IME comienza o termina de animarse) y no necesita actualizar las vistas fotograma por fotograma, ejecutarse en el subproceso principal puede generar una sobrecarga innecesaria y una animación entrecortada, en especial si el subproceso principal está ocupado.

A partir del nivel de API 37.2 y versiones posteriores, puedes usar ViewTreeObserver.WindowInsetsAnimationListener para observar estas transiciones. Como este objeto de escucha no recibe actualizaciones de progreso fotograma por fotograma (no hay una devolución de llamada de onProgress), el sistema puede ejecutar la animación de inserción en un subproceso de animación dedicado en lugar del subproceso principal, lo que genera una transición más fluida.

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);

Cómo verificar la visibilidad del software del teclado

Usa WindowInsets para verificar la visibilidad del teclado de software.

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;

Como alternativa, puedes usar ViewCompat.setOnApplyWindowInsetsListener para observar los cambios en la visibilidad del teclado en pantalla.

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;
});

Cómo sincronizar la animación con el teclado en pantalla

Cuando un usuario presiona un campo de entrada de texto, el teclado se desliza desde la parte inferior de la pantalla hasta su posición, como se muestra en el siguiente ejemplo:

Figura 2. Animación de teclado sincronizada.
  • El ejemplo etiquetado como "No sincronizado" en la figura 2 muestra el comportamiento predeterminado en Android 10 (nivel de API 29), en el que el campo de texto y el contenido de la app se ajustan en su lugar en lugar de sincronizarse con la animación del teclado, un comportamiento que puede ser visualmente discordante.

  • En Android 11 (nivel de API 30) y versiones posteriores, puedes usar WindowInsetsAnimationCompat para sincronizar la transición de la app con el teclado que se desliza hacia arriba y hacia abajo desde la parte inferior de la pantalla. Esto se ve más fluido, como se muestra en el ejemplo etiquetado como "Sincronizado" en la figura 2.

Configura WindowInsetsAnimationCompat.Callback con la vista que se sincronizará con la animación del teclado.

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.
    });

Hay varios métodos para anular en WindowInsetsAnimationCompat.Callback, a saber, onPrepare(), onStart(), onProgress() y onEnd(). Comienza llamando a onPrepare() antes de realizar cualquier cambio en el diseño.

Se llama a onPrepare cuando comienza una animación de inserciones y antes de que se vuelvan a diseñar las vistas debido a una animación. Puedes usarlo para guardar el estado inicial, que, en este caso, es la coordenada inferior de la vista.

Imagen que muestra la coordenada inferior del estado inicial de la vista raíz.
Figura 3: Usa onPrepare() para registrar el estado inicial.

En el siguiente fragmento, se muestra un ejemplo de llamada a 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();
}

Se llama a onStart cuando comienza una animación de inserciones. Puedes usarlo para establecer todas las propiedades de la vista en el estado final de los cambios de diseño. Si tienes una devolución de llamada OnApplyWindowInsetsListener establecida en cualquiera de las vistas, ya se la llamó en este punto. Este es un buen momento para guardar el estado final de las propiedades de la vista.

Una imagen que muestra la coordenada inferior del estado final de la vista
Figura 4: Usa onStart() para registrar el estado final.

En el siguiente fragmento, se muestra un ejemplo de llamada a 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;
}

Se llama a onProgress cuando cambian las inserciones como parte de la ejecución de una animación, por lo que puedes anularla y recibir notificaciones en cada fotograma durante la animación del teclado. Actualiza las propiedades de la vista para que se anime en sincronización con el teclado.

En este punto, se completaron todos los cambios de diseño. Por ejemplo, si usas View.translationY para desplazar la vista, el valor disminuye de forma gradual con cada llamada a este método y, finalmente, alcanza 0 en la posición de diseño original.

Figura 5: Usa onProgress() para sincronizar las animaciones.

En el siguiente fragmento, se muestra un ejemplo de llamada a 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;
}

De manera opcional, puedes anular onEnd. Se llama a este método después de que finaliza la animación. Este es un buen momento para limpiar los cambios temporales.

Recursos adicionales