Controlla e anima la tastiera software

Prova Compose
Jetpack Compose è il toolkit UI consigliato per Android. Scopri come utilizzare la tastiera in Composizione.

Utilizzando WindowInsetsCompat, la tua app può eseguire query e controllare la tastiera sullo schermo (chiamata anche IME) in modo simile a come interagisce con le barre di sistema. La tua app può anche utilizzare WindowInsetsAnimationCompat per creare transizioni fluide quando la tastiera su schermo viene aperta o chiusa.

Figura 1. Due esempi di transizione di apertura e chiusura della tastiera su schermo.

Prerequisiti

Prima di configurare il controllo e l'animazione per la tastiera su schermo, configura la tua app in modo che venga visualizzata da bordo a bordo. In questo modo può gestire gli inset della finestra di sistema, come le barre di sistema e la tastiera sullo schermo.

Animazione di inset sincronizzati predefinita

A partire dal livello API 37.2 e versioni successive, le applicazioni che utilizzano le regolazioni predefinite del layout della finestra (ad esempio SOFT_INPUT_ADJUST_RESIZE o SOFT_INPUT_ADJUST_PAN quando mostrano l'IME utilizzando WindowInsetsController.show(WindowInsets.Type.ime())) senza implementare un WindowInsetsAnimationCompat.Callback personalizzato possono usufruire di animazioni sincronizzate frame per frame a livello di sistema. Se attivata, questa funzionalità applica automaticamente gli inset su ogni frame e attiva un passaggio di layout, creando una transizione fluida mentre la tastiera si sposta.

Per attivare questo comportamento, aggiungi il seguente tag <property> all'attività o all'applicazione nel file AndroidManifest.xml:

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

Eseguire animazioni di rientro fuori dal thread principale

Per impostazione predefinita, la registrazione di un WindowInsetsAnimationCompat.Callback impone che l'animazione dell'inset della finestra venga eseguita sul thread principale dell'app. Ciò è necessario se la tua app aggiorna e sincronizza continuamente il layout della visualizzazione durante l'animazione (ad esempio nel metodo onProgress). Tuttavia, se la tua app deve solo ascoltare le transizioni del ciclo di vita (ad esempio quando la IME inizia o termina l'animazione) e non ha bisogno di aggiornare le visualizzazioni fotogramma per fotogramma, l'esecuzione sul thread principale può causare un sovraccarico e un'interruzione dell'animazione non necessari, soprattutto se il thread principale è altrimenti occupato.

A partire dal livello API 37.2 e versioni successive, puoi utilizzare ViewTreeObserver.WindowInsetsAnimationListener per osservare queste transizioni. Poiché questo listener non riceve aggiornamenti di avanzamento frame per frame (nessuna callback onProgress), il sistema può eseguire l'animazione di inserimento su un thread di animazione dedicato anziché sul thread principale, con conseguente transizione più 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);

Controllare la visibilità del software della tastiera

Utilizza WindowInsets per controllare la visibilità della tastiera 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;

In alternativa, puoi utilizzare ViewCompat.setOnApplyWindowInsetsListener per osservare le modifiche alla visibilità della tastiera su schermo.

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

Sincronizzare l'animazione con la tastiera su schermo

Quando un utente tocca un campo di input di testo, la tastiera scorre in posizione dalla parte inferiore dello schermo, come mostrato nell'esempio seguente:

Figura 2. Animazione della tastiera sincronizzata.
  • L'esempio etichettato "Non sincronizzato" nella figura 2 mostra il comportamento predefinito in Android 10 (livello API 29), in cui il campo di testo e i contenuti dell'app si bloccano in posizione anziché sincronizzarsi con l'animazione della tastiera, un comportamento che può essere visivamente stridente.

  • In Android 11 (livello API 30) e versioni successive, puoi utilizzare WindowInsetsAnimationCompat per sincronizzare la transizione dell'app con lo scorrimento della tastiera verso l'alto e verso il basso dalla parte inferiore dello schermo. L'aspetto è più uniforme, come mostrato nell'esempio etichettato "Sincronizzato" nella Figura 2.

Configura WindowInsetsAnimationCompat.Callback con la visualizzazione da sincronizzare con l'animazione della tastiera.

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

Esistono diversi metodi di override in WindowInsetsAnimationCompat.Callback, ovvero onPrepare(), onStart(), onProgress(), e onEnd(). Inizia chiamando onPrepare() prima di apportare modifiche al layout.

onPrepare viene chiamato quando un'animazione degli insetti sta per iniziare e prima che le visualizzazioni vengano ridisposte a causa di un'animazione. Puoi utilizzarlo per salvare lo stato iniziale, che in questo caso è la coordinata inferiore della visualizzazione.

Un'immagine che mostra la coordinata inferiore dello stato iniziale della visualizzazione principale.
Figura 3. Utilizzando onPrepare() per registrare lo stato iniziale.

Il seguente snippet mostra una chiamata di esempio 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();
}

onStart viene chiamato all'avvio di un'animazione degli inset. Puoi utilizzarlo per impostare tutte le proprietà della visualizzazione sullo stato finale delle modifiche al layout. Se hai impostato un callback OnApplyWindowInsetsListener su una delle visualizzazioni, questo viene già chiamato a questo punto. È un buon momento per salvare lo stato finale delle proprietà della visualizzazione.

Un'immagine che mostra la coordinata inferiore dello stato finale della visualizzazione
Figura 4. Utilizzo di onStart() per registrare lo stato finale.

Il seguente snippet mostra una chiamata di esempio 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;
}

onProgress viene chiamato quando i margini interni cambiano nell'ambito dell'esecuzione di un'animazione, in modo da poterlo sostituire e ricevere una notifica su ogni frame durante l'animazione della tastiera. Aggiorna le proprietà della visualizzazione in modo che l'animazione sia sincronizzata con la tastiera.

A questo punto, tutte le modifiche al layout sono state completate. Ad esempio, se utilizzi View.translationY per spostare la visualizzazione, il valore diminuisce gradualmente a ogni chiamata di questo metodo e alla fine raggiunge 0 nella posizione del layout originale.

Figura 5. Utilizzando onProgress() per sincronizzare le animazioni.

Il seguente snippet mostra una chiamata di esempio 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;
}

Se vuoi, puoi ignorare onEnd. Questo metodo viene chiamato al termine dell'animazione. È un buon momento per liberare spazio dalle modifiche temporanee.

Risorse aggiuntive