Controlar e animar o teclado de software

Experimente trabalhar com o Compose
O Jetpack Compose é o kit de ferramentas de IU recomendado para Android. Saiba como trabalhar com o teclado no Compose.

Usando WindowInsetsCompat, seu app pode consultar e controlar o teclado na tela (também chamado de IME) de forma semelhante à maneira como ele interage com as barras de sistema. O app também pode usar WindowInsetsAnimationCompat para criar transições perfeitas quando o teclado de software é aberto ou fechado.

Figura 1. Dois exemplos da transição de abertura e fechamento do teclado de software.

Pré-requisitos

Antes de configurar o controle e a animação do teclado de software, configure o app para exibição de ponta a ponta. Isso permite que ele processe inserções de janela do sistema, como as barras de sistema e o teclado na tela.

Animação de encartes sincronizados padrão

A partir do nível 37.2 da API, os aplicativos que usam os ajustes padrão de layout de janela (como SOFT_INPUT_ADJUST_RESIZE ou SOFT_INPUT_ADJUST_PAN ao mostrar o IME usando WindowInsetsController.show(WindowInsets.Type.ime())) sem implementar um WindowInsetsAnimationCompat.Callback personalizado podem se beneficiar de animações sincronizadas quadro a quadro no nível do sistema. Quando ativado, esse recurso aplica automaticamente as inserções em cada frame e aciona uma passagem de layout, criando uma transição suave conforme o teclado se move.

Para ativar esse comportamento, adicione a seguinte tag <property> à atividade ou ao aplicativo no arquivo AndroidManifest.xml:

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

Realizar animações de encartes fora da linha de execução principal

Por padrão, o registro de um WindowInsetsAnimationCompat.Callback garante que a animação de inserção de janela seja executada na linha de execução principal do app. Isso é necessário se o app estiver atualizando e sincronizando continuamente o layout de visualização durante a animação (como no método onProgress). No entanto, se o app precisar apenas detectar transições de ciclo de vida (como quando o IME inicia ou encerra a animação) e não precisar atualizar as visualizações frame a frame, a execução na linha de execução principal poderá causar sobrecarga desnecessária e travamento da animação, especialmente se a linha de execução principal estiver ocupada.

A partir do nível 37.2 da API, use ViewTreeObserver.WindowInsetsAnimationListener para observar essas transições. Como esse listener não recebe atualizações de progresso frame a frame (sem callback onProgress), o sistema pode executar a animação de inserção em uma linha de execução dedicada, em vez da linha de execução principal, resultando em uma transição mais suave.

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

Verificar a visibilidade do software do teclado

Use WindowInsets para verificar a visibilidade do 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, use ViewCompat.setOnApplyWindowInsetsListener para observar mudanças na visibilidade do teclado de software.

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

Sincronizar a animação com o teclado de software

Quando um usuário toca em um campo de entrada de texto, o teclado desliza para o lugar na parte de baixo da tela, como mostrado no exemplo a seguir:

Figura 2. Animação de teclado sincronizada.
  • O exemplo rotulado como "Não sincronizado" na figura 2 mostra o comportamento padrão no Android 10 (nível 29 da API), em que o campo de texto e o conteúdo do app se ajustam ao lugar em vez de sincronizar com a animação do teclado, um comportamento que pode ser visualmente desagradável.

  • No Android 11 (nível 30 da API) e versões mais recentes, é possível usar WindowInsetsAnimationCompat para sincronizar a transição do app com o teclado deslizando para cima e para baixo na parte de baixo da tela. Isso parece mais suave, como mostrado no exemplo rotulado como "Synchronized" na Figura 2.

Configure WindowInsetsAnimationCompat.Callback com a visualização a ser sincronizada com a animação do 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.
    });

Há vários métodos para substituir em WindowInsetsAnimationCompat.Callback, como onPrepare(), onStart(), onProgress() e onEnd(). Comece chamando onPrepare() antes de qualquer mudança no layout.

onPrepare é chamado quando uma animação de encartes está começando e antes que as visualizações sejam reorganizadas devido a uma animação. Você pode usá-lo para salvar o estado inicial, que, neste caso, é a coordenada inferior da visualização.

Uma imagem mostrando a coordenada inferior do estado inicial da visualização raiz.
Figura 3. Usar onPrepare() para registrar o estado inicial.

O snippet a seguir mostra um exemplo de chamada para 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 é chamado quando uma animação de encartes é iniciada. Você pode usá-lo para definir todas as propriedades de visualização para o estado final das mudanças de layout. Se você tiver um callback OnApplyWindowInsetsListener definido para qualquer uma das visualizações, ele já será chamado nesse momento. Este é um bom momento para salvar o estado final das propriedades da visualização.

Uma imagem mostrando a coordenada inferior do estado final da visualização
Figura 4. Usar onStart() para registrar o estado final.

O snippet a seguir mostra um exemplo de chamada para 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 é chamado quando as inserções mudam como parte da execução de uma animação. Assim, é possível substituí-lo e receber notificações em cada frame durante a animação do teclado. Atualize as propriedades da visualização para que ela seja animada em sincronia com o teclado.

Todas as mudanças de layout foram concluídas. Por exemplo, se você usar View.translationY para deslocar a visualização, o valor vai diminuir gradualmente a cada chamada desse método e, por fim, vai chegar a 0 na posição original do layout.

Figura 5. Usar onProgress() para sincronizar as animações.

O snippet a seguir mostra um exemplo de chamada para 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 quiser, é possível substituir onEnd. Esse método é chamado após o término da animação. Este é um bom momento para limpar as alterações temporárias.

Outros recursos