O desenvolvimento de componentes personalizados no Android permite criar interfaces mais ricas e reutilizáveis. Existem três abordagens principais:
- Extensão de um componente existente: útil quando o comportamento desejado é semelhente a um componente nativo, como um
Buttoncom efeitos visuais personalizados. - Combinação de componentes exsitentes: ideal quando a funcionalidade requer múltiplos elementos, como um campo de entrada com botão de limpeza.
- Criação a partir do zero (herdando
ViewouSurfaceView): necessário quando se deseja controle total sobre a renderização e interação, como em gráficos customizados.
Métodos-chave a serem sobrescritos
onDraw(Canvas canvas): responsável por desenhar o conteúdo visual do componente.onMeasure(int widthMeasureSpec, int heightMeasureSpec): define as dimensões do componente com base nas restrições do layout pai.onTouchEvent(MotionEvent event): trata eventos de toque para interações do usuário.onSizeChanged(int w, int h, int oldw, int oldh): chamado quando as dimensões do componente mudam.onFinishInflate(): executado após o componente ser inflado a partir de um arquivo XML.
Construtores de View personalizada
Um componente personalizado deve implementar pelo menos dois construtores para compatibilidade com uso programático e via XML:
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// Inicialização aqui
}
Definição de atributos personalizados
Atirbutos devem ser declarados em res/values/attrs.xml:
<resources>
<declare-styleable name="CustomView">
<attr name="customText" format="string" />
<attr name="customColor" format="color" />
</declare-styleable>
</resources>
No layout, usa-se o namespace res-auto:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.example.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:customText="Olá Mundo"
app:customColor="#FF5722" />
</LinearLayout>
Para ler os valores no código:
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
String text = a.getString(R.styleable.CustomView_customText);
int color = a.getColor(R.styleable.CustomView_customColor, Color.BLACK);
a.recycle();
Exemplo: Círculo interativo com texto
Um componente que desenha um círculo azul e move-se conforme o toque do usuário:
public class DraggableCircleView extends View {
private Paint paint = new Paint();
private float x = 100f, y = 100f;
private int textColor = Color.BLACK;
public DraggableCircleView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DraggableCircleView);
textColor = a.getColor(R.styleable.DraggableCircleView_textColor, Color.BLACK);
a.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(Color.BLUE);
canvas.drawCircle(x, y, 40, paint);
paint.setColor(textColor);
canvas.drawText("Arraste-me", x - 40, y + 60, paint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
x = event.getX();
y = event.getY();
invalidate(); // dispara novo onDraw()
return true;
}
}
Componente composto: Campo de entrada com botão de limpeza
Este exemplo combina EditText e ImageView em um único componente reutilizável.
Layout do componente (cleanable_edittext.xml):
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="40dp" />
<ImageView
android:id="@+id/clearButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="12dp"
android:src="@drawable/ic_clear"
android:visibility="invisible" />
</merge>
Classe do componente:
public class CleanableEditText extends FrameLayout {
private EditText editText;
private ImageView clearButton;
public CleanableEditText(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.cleanable_edittext, this, true);
editText = findViewById(R.id.editText);
clearButton = findViewById(R.id.clearButton);
editText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
clearButton.setVisibility(s.length() > 0 ? VISIBLE : INVISIBLE);
}
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void afterTextChanged(Editable s) {}
});
clearButton.setOnClickListener(v -> editText.setText(""));
}
public String getText() {
return editText.getText().toString();
}
public void setText(String text) {
editText.setText(text);
}
public EditText getEditText() {
return editText; // permite configurar inputType, etc.
}
}
Uso no layout principal:
<com.example.CleanableEditText
android:id="@+id/inputField"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Configuração adicional em código:
CleanableEditText field = findViewById(R.id.inputField);
field.getEditText().setInputType(InputType.TYPE_CLASS_PHONE);