Android: Gerenciamento Suave de Teclado e Painéis de Entrada sem Piscadas

Implementar um painel de entrada similar ao do WeChat em aplictaivos Android, especialmente o gerenciamento da transição entre o painel e o teclado virtual, pode ser desafiaodr. Frequentemente, essa transição resulta em piscadas visíveis na interface, prejudicando a experiência do usuário. Após explorar diversas soluções e projetos de código aberto, que nem sempre se alinham com os requisitos de SDK ou são excessivamente complexos, uma abordagem mais direta utilizando a configuração dinâmica do windowSoftInputMode se mostrou eficaz.

Este artigo detalha a implementação, focando nas partes cruciais do código para obter uma transição suave e sem interrupções visuais, espelhando o comportamento do WeChat.

Layout XML (res/layout/seu_layout.xml)


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:orientation="vertical">

   <LinearLayout
       android:id="@+id/input_container"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="vertical"
       android:background="?attr/colorSurface">

       <!-- Linha divisória superior -->
       <View
           android:layout_width="match_parent"
           android:layout_height="1px"
           android:background="?android:attr/listDivider"/>

       <!-- Barra de entrada (botões e campo de texto) -->
       <RelativeLayout
           android:id="@+id/input_bar"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:paddingTop="9dp"
           android:paddingBottom="9dp">

           <ImageView
               android:id="@+id/btn_voice"
               android:layout_width="32dp"
               android:layout_height="32dp"
               android:layout_alignParentStart="true"
               android:layout_marginStart="10dp"
               android:contentDescription="@string/desc_voice_input"
               android:src="@drawable/ic_voice"/>

           <LinearLayout
               android:id="@+id/right_controls"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_alignParentEnd="true"
               android:layout_marginEnd="10dp"
               android:gravity="center_vertical"
               android:orientation="horizontal">

               <ImageView
                   android:id="@+id/btn_emoji"
                   android:layout_width="32dp"
                   android:layout_height="32dp"
                   android:contentDescription="@string/desc_emoji"
                   android:src="@drawable/ic_emoji"/>

               <ImageView
                   android:id="@+id/btn_attach"
                   android:layout_width="32dp"
                   android:layout_height="32dp"
                   android:layout_marginStart="15dp"
                   android:contentDescription="@string/desc_attachment"
                   android:src="@drawable/ic_attach"/>

               <TextView
                   android:id="@+id/btn_send"
                   android:layout_width="41dp"
                   android:layout_height="30dp"
                   android:layout_marginStart="6dp"
                   android:visibility="gone"
                   android:clickable="true"
                   android:focusable="true"
                   android:background="@drawable/bg_send_button"
                   android:gravity="center"
                   android:text="@string/send"
                   android:textColor="?attr/colorOnPrimary"
                   android:textSize="14sp"/>

           </LinearLayout>

           <LinearLayout
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:layout_toStartOf="@id/right_controls"
               android:layout_toEndOf="@id/btn_voice"
               android:layout_centerVertical="true"
               android:orientation="vertical"
               android:gravity="center">

               <EditText
                   android:id="@+id/edit_message"
                   android:layout_width="match_parent"
                   android:layout_height="wrap_content"
                   android:minHeight="32dp"
                   android:maxHeight="97dp"
                   android:background="@null"
                   android:gravity="center_vertical"
                   android:textSize="16sp"
                   android:hint="@string/hint_type_message"
                   android:textColorHint="?android:attr/textColorHint"
                   android:layout_marginStart="5dp"
                   android:layout_marginEnd="5dp"/>

               <View
                   android:layout_width="match_parent"
                   android:layout_height="1px"
                   android:background="?android:attr/listDivider"/>

           </LinearLayout>

       </RelativeLayout>

       <!-- Área de painéis estendidos (voz, emoji, anexos) -->
       <RelativeLayout
           android:id="@+id/extended_panel_area"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:visibility="gone">

           <View
               android:id="@+id/panel_top_divider"
               android:layout_width="match_parent"
               android:layout_height="1px"
               android:background="?android:attr/listDivider"/>

           <LinearLayout
               android:id="@+id/panel_voice"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:layout_below="@id/panel_top_divider"
               android:orientation="vertical"
               android:padding="30dp"
               android:visibility="gone">

               <TextView
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:text="@string/voice_input_label"/>

           </LinearLayout>

           <LinearLayout
               android:id="@+id/panel_emoji"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:layout_below="@id/panel_top_divider"
               android:orientation="vertical"
               android:padding="30dp"
               android:visibility="gone">

               <TextView
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:text="@string/emoji_panel_label"/>

           </LinearLayout>

           <LinearLayout
               android:id="@+id/panel_attachment"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:layout_below="@id/panel_top_divider"
               android:orientation="vertical"
               android:paddingTop="30dp"
               android:visibility="gone">

               <LinearLayout
                   android:id="@+id/btn_gallery"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:layout_marginStart="27dp"
                   android:orientation="vertical"
                   android:gravity="center"
                   android:clickable="true"
                   android:focusable="true">

                   <LinearLayout
                       android:layout_width="56dp"
                       android:layout_height="56dp"
                       android:background="@drawable/bg_icon_circle"
                       android:gravity="center"
                       android:orientation="vertical">

                       <ImageView
                           android:layout_width="32dp"
                           android:layout_height="32dp"
                           android:contentDescription="@string/desc_gallery"
                           android:src="@drawable/ic_gallery"/>

                   </LinearLayout>

                   <TextView
                       android:layout_width="wrap_content"
                       android:layout_height="wrap_content"
                       android:layout_marginTop="4dp"
                       android:text="@string/gallery"
                       android:textSize="14sp"
                       android:textColor="?android:attr/textColorSecondary"/>

               </LinearLayout>

           </LinearLayout>

       </RelativeLayout>

   </LinearLayout>

</LinearLayout>
 

Código Java da Atividade/Fragmento


import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;

public class ChatActivity extends AppCompatActivity implements View.OnClickListener {

   private RelativeLayout inputContainer;
   private RelativeLayout inputBar;
   private ImageView btnVoice;
   private LinearLayout rightControls;
   private ImageView btnEmoji;
   private ImageView btnAttach;
   private TextView btnSend;
   private EditText editMessage;
   private RelativeLayout extendedPanelArea;
   private LinearLayout panelVoice;
   private LinearLayout panelEmoji;
   private LinearLayout panelAttachment;

   private boolean isPanelVisible = false;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_chat);
       initViews();
   }

   private void initViews() {
       // Inicializa Views
       inputContainer = findViewById(R.id.input_container);
       inputBar = findViewById(R.id.input_bar);
       btnVoice = findViewById(R.id.btn_voice);
       rightControls = findViewById(R.id.right_controls);
       btnEmoji = findViewById(R.id.btn_emoji);
       btnAttach = findViewById(R.id.btn_attach);
       btnSend = findViewById(R.id.btn_send);
       editMessage = findViewById(R.id.edit_message);
       extendedPanelArea = findViewById(R.id.extended_panel_area);
       panelVoice = findViewById(R.id.panel_voice);
       panelEmoji = findViewById(R.id.panel_emoji);
       panelAttachment = findViewById(R.id.panel_attachment);

       // Configura Listeners
       btnVoice.setOnClickListener(this);
       btnEmoji.setOnClickListener(this);
       btnAttach.setOnClickListener(this);
       btnSend.setOnClickListener(this);
       editMessage.setOnFocusChangeListener((v, hasFocus) -> {
           if (hasFocus) {
               hideExtendedPanel(false);
           }
       });
       editMessage.setOnClickListener(this);
       // Adicionar TextWatcher se necessário
   }

   @Override
   public void onClick(View v) {
       int id = v.getId();
       if (id == R.id.btn_voice) {
           handleVoiceButtonClick();
       } else if (id == R.id.btn_emoji) {
           handleEmojiButtonClick();
       } else if (id == R.id.btn_attach) {
           handleAttachButtonClick();
       } else if (id == R.id.edit_message) {
           handleMessageEditClick();
       }
       // Outros cliques...
   }

   private void handleVoiceButtonClick() {
       if (isPanelVisible && panelVoice.getVisibility() == View.VISIBLE) {
           hideExtendedPanel(true);
       } else {
           showExtendedPanel(PanelType.VOICE);
       }
   }

   private void handleEmojiButtonClick() {
       if (isPanelVisible && panelEmoji.getVisibility() == View.VISIBLE) {
           hideExtendedPanel(true);
       } else {
           showExtendedPanel(PanelType.EMOJI);
       }
   }

   private void handleAttachButtonClick() {
       if (isPanelVisible && panelAttachment.getVisibility() == View.VISIBLE) {
           hideExtendedPanel(true);
       } else {
           showExtendedPanel(PanelType.ATTACHMENT);
       }
   }

   private void handleMessageEditClick() {
       hideExtendedPanel(true);
   }

   private enum PanelType {
       VOICE, EMOJI, ATTACHMENT
   }

   private void showExtendedPanel(PanelType type) {
       editMessage.clearFocus(); // Remove o foco do EditText
       hideKeyboard(); // Esconde o teclado virtual

       // Configura o modo de janela para evitar ajustes automáticos que causam pisca
       getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);

       // Define a altura do painel estendido com base na altura do teclado detectada
       ViewGroup.LayoutParams params = extendedPanelArea.getLayoutParams();
       params.height = KeyboardUtil.getKeyboardHeight(this); // Assumindo que KeyboardUtil calcula a altura
       extendedPanelArea.setLayoutParams(params);

       extendedPanelArea.setVisibility(View.VISIBLE);
       isPanelVisible = true;

       // Lógica para mostrar o painel correto
       panelVoice.setVisibility(View.GONE);
       panelEmoji.setVisibility(View.GONE);
       panelAttachment.setVisibility(View.GONE);

       switch (type) {
           case VOICE:
               panelVoice.setVisibility(View.VISIBLE);
               break;
           case EMOJI:
               panelEmoji.setVisibility(View.VISIBLE);
               break;
           case ATTACHMENT:
               panelAttachment.setVisibility(View.VISIBLE);
               break;
       }

       // Adiciona animação de entrada se o teclado não estava visível
       if (!isKeyboardShown()) {
            animatePanelIn();
       }
   }

   private void hideExtendedPanel(boolean animate) {
       if (!isPanelVisible) return;

       isPanelVisible = false;
       extendedPanelArea.setVisibility(View.GONE);

       // Restaura o modo de janela para ajuste normal
       getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);

       if (animate) {
           animatePanelOut();
       } else {
           // Se não animar, garante que os painéis internos estejam ocultos
           panelVoice.setVisibility(View.GONE);
           panelEmoji.setVisibility(View.GONE);
           panelAttachment.setVisibility(View.GONE);
       }
   }

    private void showInputKeyboard() {
       editMessage.requestFocus(); // Foca o EditText
       showKeyboard(); // Mostra o teclado virtual

       // Esconde o painel estendido após um pequeno atraso para garantir que o teclado apareceu
       new Handler().postDelayed(() -> {
           hideExtendedPanel(false); // Não anima a saída do painel ao mostrar o teclado
           getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
       }, 300); // Atraso em milissegundos
   }

   private void resetInputBarState() {
       btnVoice.setSelected(false);
       btnEmoji.setSelected(false);
       btnAttach.setSelected(false);

       if (isPanelVisible) {
           hideExtendedPanel(true); // Anima a saída do painel
       } else if (isKeyboardShown()) {
           hideKeyboard(); // Esconde o teclado se estiver visível
       }
   }

   // Métodos auxiliares para animação
   private void animatePanelIn() {
       Animation slideIn = AnimationUtils.loadAnimation(this, R.anim.slide_in_bottom);
       inputContainer.startAnimation(slideIn);
   }

   private void animatePanelOut() {
       Animation slideOut = AnimationUtils.loadAnimation(this, R.anim.slide_out_bottom);
       slideOut.setAnimationListener(new Animation.AnimationListener() {
           @Override
           public void onAnimationStart(Animation animation) { }

           @Override
           public void onAnimationEnd(Animation animation) {
               // Garante que os painéis internos estejam ocultos após a animação
               panelVoice.setVisibility(View.GONE);
               panelEmoji.setVisibility(View.GONE);
               panelAttachment.setVisibility(View.GONE);
           }

           @Override
           public void onAnimationRepeat(Animation animation) { }
       });
       inputContainer.startAnimation(slideOut);
   }

   // Métodos auxiliares para gerenciamento do teclado (implementação omitida para brevidade)
   private void showKeyboard() {
       // Implementação para mostrar o teclado virtual
       // Ex: InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
       //     imm.showSoftInput(editMessage, InputMethodManager.SHOW_IMPLICIT);
   }

   private void hideKeyboard() {
       // Implementação para esconder o teclado virtual
       // Ex: InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
       //     imm.hideSoftInputFromWindow(editMessage.getWindowToken(), 0);
   }

   private boolean isKeyboardShown() {
       // Implementação para verificar se o teclado virtual está visível
       // Pode ser feito verificando a altura da janela de input
       return false; // Placeholder
   }

   // Assumindo que você tenha uma classe KeyboardUtil para calcular e armazenar a altura do teclado
   // Exemplo simplificado:
   // static class KeyboardUtil {
   //     private static final String PREFS_NAME = "keyboard_prefs";
   //     private static final String KEYBOARD_HEIGHT = "keyboard_height";
   //     private static final int DEFAULT_HEIGHT = 570;
   //
   //     public static int getKeyboardHeight(Context context) {
   //         SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
   //         return prefs.getInt(KEYBOARD_HEIGHT, DEFAULT_HEIGHT);
   //     }
   //
   //     public static void saveKeyboardHeight(Context context, int height) {
   //         SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
   //         prefs.edit().putInt(KEYBOARD_HEIGHT, height).apply();
   //     }
   //
   //     public static void setupKeyboardHeightListener(Activity activity, View rootView) {
   //         rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
   //             Rect r = new Rect();
   //             rootView.getWindowVisibleDisplayFrame(r);
   //             int screenHeight = activity.getWindowManager().getDefaultDisplay().getHeight();
   //             int keypadHeight = screenHeight - r.bottom;
   //
   //             if (keypadHeight > 100) { // Verifica se é realmente o teclado
   //                 saveKeyboardHeight(activity, keypadHeight);
   //             }
   //         });
   //     }
   // }
}
 

Classe de Utilitário para Altura do Teclado (Exemplo)

É essencial ter um mecanismo para detectar e armazenar a altura do teclado virtual. Uma classe utilitária pode gerenciar isso, salvando a altura em SharedPreferences.


import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewTreeObserver;

public class KeyboardUtil {

   private static final String PREFS_NAME = "keyboard_prefs";
   private static final String KEYBOARD_HEIGHT = "keyboard_height";
   private static final int DEFAULT_HEIGHT = 570; // Altura padrão em pixels

   /**
    * Obtém a altura do teclado armazenada.
    * @param context Contexto da aplicação.
    * @return A altura do teclado em pixels.
    */
   public static int getKeyboardHeight(Context context) {
       SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
       return prefs.getInt(KEYBOARD_HEIGHT, DEFAULT_HEIGHT);
   }

   /**
    * Salva a altura do teclado detectada.
    * @param context Contexto da aplicação.
    * @param height Altura do teclado em pixels.
    */
   public static void saveKeyboardHeight(Context context, int height) {
       SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
       prefs.edit().putInt(KEYBOARD_HEIGHT, height).apply();
   }

   /**
    * Configura um listener para detectar mudanças no layout global e calcular a altura do teclado.
    * Deve ser chamado com a view raiz da sua atividade e a própria atividade.
    * @param activity A atividade atual.
    * @param rootView A view raiz do layout da atividade.
    */
   public static void setupKeyboardHeightListener(Activity activity, View rootView) {
       rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
           @Override
           public void onGlobalLayout() {
               Rect visibleDisplayFrame = new Rect();
               rootView.getWindowVisibleDisplayFrame(visibleDisplayFrame);
               int screenHeight = activity.getWindowManager().getDefaultDisplay().getHeight();
               // Calcula a altura aparente da área não ocupada pelo teclado
               int keyboardHeight = screenHeight - visibleDisplayFrame.bottom;

               // Considera apenas valores razoáveis como altura de teclado
               if (keyboardHeight > 100 && keyboardHeight != getKeyboardHeight(activity)) {
                   saveKeyboardHeight(activity, keyboardHeight);
               }
           }
       });
   }
}
 

Considerações Adicionais

O código fornecido inclui a lógica principle para gerenciar a visibilidade do painel estendido e a transição para o teclado virtual. Partes como o gerenciamento do EditText (redimensionamento automático, etc.) e a implementação exata das funções showKeyboard(), hideKeyboard() e isKeyboardShown() dependem da arquitetura do seu projeto. Certifique-se de adaptar os nomes de recursos (drawables, strings, anim) conforme necessário.

Este código foi testado em dispositivos como Oppo R9, Huawei Honor X4 e HTC.

Tags: android UI input method keyboard panel

Publicado em 6-29 19:15