Criando Etiquetas de Canto Personalizadas no Android com Custom Views

No desenvolvimento Android, é comum a necessidade de adicionar etiquetas (badges) no canto de componentes de interface para destacar informações como "Novo", "Promoção" ou "Vendido". Embora seja possível utilizar imagens estáticas, a criação de um componente customizado oferece maior flexibilidade em termos de cores, textos e posicionamento. Neste artigo, detalharemos a construção de uma View customizada para essa finalidade.

1. Definição de Atributos Customizados

Para tornar o componente reutilizável via XML, definimos um conjunto de atributos no arquivo res/values/attrs.xml. Isso permite configurar o tamanho, a cor e a posição diretamente no layout.

<declare-styleable name="CustomBadgeView">
    <attr name="badge_size" format="dimension" />
    <attr name="label_margin" format="dimension" />
    <attr name="bg_color" format="color" />
    <attr name="label_color" format="color" />
    <attr name="label_size" format="dimension" />
    <attr name="label_text" format="string" />
    <attr name="badge_gravity">
        <enum name="top_right" value="0" />
        <enum name="bottom_right" value="1" />
        <enum name="bottom_left" value="2" />
        <enum name="top_left" value="3" />
    </attr>
</declare-styleable>

2. Implementação da Custom View

A classe principal estende View e gerencia a renderização do fundo e do texto. O segredo está na manipulação do Canvas, utilizando rotações e translações para posicionar a etiqueta corretamente nos quatro cantos possíveis.

public class CustomBadgeView extends View {
    private int centerOffset;
    private Paint bgPaint;
    private TextPaint labelPaint;
    private Path badgePath;

    private int badgeGravity; 
    private int badgeSize;
    private int labelSize;
    private int labelColor;
    private String labelText;
    private int backgroundColor;
    private int edgePadding;

    public CustomBadgeView(Context context) {
        this(context, null);
    }

    public CustomBadgeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        applyAttributes(context, attrs);
        setupTools();
    }

    private void applyAttributes(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomBadgeView);
        badgeGravity = typedArray.getInt(R.styleable.CustomBadgeView_badge_gravity, 0);
        badgeSize = typedArray.getDimensionPixelSize(R.styleable.CustomBadgeView_badge_size, 100);
        labelSize = typedArray.getDimensionPixelSize(R.styleable.CustomBadgeView_label_size, 40);
        labelColor = typedArray.getColor(R.styleable.CustomBadgeView_label_color, Color.WHITE);
        labelText = typedArray.getString(R.styleable.CustomBadgeView_label_text);
        backgroundColor = typedArray.getColor(R.styleable.CustomBadgeView_bg_color, Color.RED);
        edgePadding = typedArray.getDimensionPixelSize(R.styleable.CustomBadgeView_label_margin, -1);
        typedArray.recycle();
    }

    private void setupTools() {
        badgePath = new Path();
        bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        bgPaint.setColor(backgroundColor);

        labelPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        labelPaint.setColor(labelColor);
        labelPaint.setTextSize(labelSize);
        labelPaint.setTextAlign(Paint.Align.CENTER);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int finalSize = (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) ? width : badgeSize * 2;
        
        setMeasuredDimension(finalSize, finalSize);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerOffset = Math.min(w, h) / 2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.translate(centerOffset, centerOffset);
        canvas.rotate(badgeGravity * 90);

        float limit = centerOffset * 2;
        float actualBadgeSize = Math.min(badgeSize, limit);

        // Desenho do polígono da etiqueta
        badgePath.reset();
        badgePath.moveTo(-centerOffset, -centerOffset);
        badgePath.lineTo(actualBadgeSize - centerOffset, -centerOffset);
        badgePath.lineTo(centerOffset, centerOffset - actualBadgeSize);
        badgePath.lineTo(centerOffset, centerOffset);
        badgePath.close();
        canvas.drawPath(badgePath, bgPaint);

        // Configuração para desenho do texto
        canvas.rotate(45);
        float textX = 0;
        float textY;

        double hypotenuse = Math.sqrt(2) / 2.0 * actualBadgeSize;
        float textHeight = -(labelPaint.ascent() + labelPaint.descent());

        if (edgePadding >= 0) {
            if (badgeGravity == 1 || badgeGravity == 2) {
                textY = (float) -(hypotenuse - (edgePadding - labelPaint.ascent()));
            } else {
                textY = -edgePadding;
            }
        } else {
            textY = (float) (-hypotenuse + textHeight) / 2;
        }

        // Ajuste de inversão para cantos inferiores
        if (badgeGravity == 1 || badgeGravity == 2) {
            canvas.translate(0, (float) -hypotenuse);
            canvas.scale(-1, -1);
        }

        if (labelText != null) {
            canvas.drawText(labelText, textX, textY, labelPaint);
        }
    }

    public void updateText(String text) {
        this.labelText = text;
        invalidate();
    }

    public void setBadgeColor(int color) {
        this.backgroundColor = color;
        bgPaint.setColor(color);
        invalidate();
    }
}

3. Resumo de Propriedades

Abaixo, os principais atributos que podem ser configurados via XML para personalizar o comportamento visual da etiqueta:

Atributo Tipo Descrição
badge_size dimension Largura visível da etiqueta no eixo horizontal/vertical.
bg_color color Cor de fundo da etiqueta.
label_color color Cor da fonte do texto.
label_size dimension Tamanho da fonte.
badge_gravity enum Posição: top_right (0), bottom_right (1), bottom_left (2), top_left (3).

Esta implementação utiliza cálculos trigonométricos simples para garantir que o texto seja desehnado paralelamente à hipotenusa do triângulo (ou trapézio) formado no canto da View. Ao rotacionar o Canvas, simplificamos a lógica de desenho, tratando cada canto como uma variação de rotação de 90 graus.

Tags: android CustomView UI Canvas java

Publicado em 6-10 17:51 por Thomas