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.