Desenvolvimento de Componentes Personalizados no Android

O desenvolvimento de componentes personalizados no Android permite criar interfaces mais ricas e reutilizáveis. Existem três abordagens principais:

  1. Extensão de um componente existente: útil quando o comportamento desejado é semelhente a um componente nativo, como um Button com efeitos visuais personalizados.
  2. Combinação de componentes exsitentes: ideal quando a funcionalidade requer múltiplos elementos, como um campo de entrada com botão de limpeza.
  3. Criação a partir do zero (herdando View ou SurfaceView): 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);

Tags: android CustomView viewgroup Canvas TypedArray

Publicado em 10-8 11:48