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.