Criar um desenho personalizado

Experimente trabalhar com o Compose
O Jetpack Compose é o kit de ferramentas de interface recomendado para o Android. Aprenda a trabalhar com layouts no Compose.

A parte mais importante de uma visualização personalizada é a aparência. O desenho personalizado pode ser fácil ou complexo, de acordo com as necessidades do seu aplicativo. Este documento aborda algumas das operações mais comuns.

Para mais informações, consulte Visão geral de drawables.

Substituir o onDraw()

A etapa mais importante no desenho de uma visualização personalizada é substituir o método onDraw(). O parâmetro para onDraw() é um objeto Canvas que a visualização pode usar para desenhar a si mesma. A classe Canvas define métodos para desenhar texto, linhas, bitmaps e muitas outras primitivas gráficas. Você pode usar esses métodos em onDraw() para criar sua interface personalizada.

Comece criando um objeto Paint. A próxima seção discute Paint com mais detalhes.

Criar objetos de desenho

O framework android.graphics divide o desenho em duas áreas:

  • O que desenhar, processado por Canvas.
  • Como desenhar, processado por Paint.

Por exemplo, Canvas oferece um método para desenhar uma linha, e Paint oferece métodos para definir a cor dessa linha. Canvas tem um método para desenhar um retângulo, e Paint define se ele será preenchido com uma cor ou ficará vazio. Canvas define formas que podem ser desenhadas na tela, e Paint define a cor, o estilo, a fonte e outros aspectos de cada forma desenhada.

Antes de desenhar qualquer coisa, crie um ou mais objetos Paint. O exemplo a seguir faz isso em um método chamado init. Esse método é chamado pelo construtor de Java, mas pode ser inicializado inline no Kotlin.

Kotlin

@ColorInt
private var textColor    // Obtained from style attributes.

@Dimension
private var textHeight   // Obtained from style attributes.

private val textPaint = Paint(ANTI_ALIAS_FLAG).apply {
    color = textColor
    if (textHeight == 0f) {
        textHeight = textSize
    } else {
        textSize = textHeight
    }
}

private val piePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    textSize = textHeight
}

private val shadowPaint = Paint(0).apply {
    color = 0x101010
    maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
}

Java

private Paint textPaint;
private Paint piePaint;
private Paint shadowPaint;

@ColorInt
private int textColor;       // Obtained from style attributes.

@Dimension
private float textHeight;    // Obtained from style attributes.

private void init() {
   textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   textPaint.setColor(textColor);
   if (textHeight == 0) {
       textHeight = textPaint.getTextSize();
   } else {
       textPaint.setTextSize(textHeight);
   }

   piePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   piePaint.setStyle(Paint.Style.FILL);
   piePaint.setTextSize(textHeight);

   shadowPaint = new Paint(0);
   shadowPaint.setColor(0xff101010);
   shadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));
   ...
}

Criar objetos com antecedência é uma otimização importante. As visualizações são redesenhadas com frequência, e muitos objetos de desenho exigem uma inicialização cara. A criação de objetos de desenho no método onDraw() reduz significativamente o desempenho e pode fazer com que a interface fique lenta.

Gerenciar eventos de layout

Para desenhar corretamente sua visualização personalizada, descubra o tamanho dela. As visualizações personalizadas complexas geralmente precisam executar vários cálculos de layout, dependendo do tamanho e da forma da área na tela. Nunca faça suposições sobre o tamanho da visualização na tela. Mesmo que apenas um app use sua visualização, ele precisará gerenciar diferentes tamanhos, densidades e proporções de tela nos modos retrato e paisagem.

Embora o View tenha muitos métodos para gerenciar medições, a maioria deles não precisa ser substituída. Se sua visualização não precisa de controle especial sobre o tamanho, substitua apenas um método: onSizeChanged().

O onSizeChanged() é chamado quando um tamanho é atribuído à sua visualização e novamente caso o tamanho da sua visualização mude por qualquer motivo. Calcule posições, dimensões e quaisquer outros valores relacionados ao tamanho da sua visualização no onSizeChanged(), em vez de recalcular esses valores toda vez que você desenhar. No exemplo a seguir, o onSizeChanged() é onde a visualização calcula o retângulo delimitador do gráfico e a posição relativa do rótulo do texto e de outros elementos visuais.

Quando um tamanho é atribuído à sua visualização, o gerenciador de layout presume que o tamanho inclua o padding da visualização. Gerencie os valores de padding ao calcular o tamanho da sua visualização. Confira um snippet de onSizeChanged() que mostra como fazer isso:

Kotlin

private val showText    // Obtained from styled attributes.
private val textWidth   // Obtained from styled attributes.

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // Account for padding.
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label.
    if (showText) xpad += textWidth.toFloat()
    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

    // Figure out how big you can make the pie.
    val diameter = Math.min(ww, hh)
}

Java

private Boolean showText;    // Obtained from styled attributes.
private int textWidth;       // Obtained from styled attributes.

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // Account for padding.
    float xpad = (float)(getPaddingLeft() + getPaddingRight());
    float ypad = (float)(getPaddingTop() + getPaddingBottom());

    // Account for the label.
    if (showText) xpad += textWidth;

    float ww = (float)w - xpad;
    float hh = (float)h - ypad;

    // Figure out how big you can make the pie.
    float diameter = Math.min(ww, hh);
}

Se você precisa de um controle mais preciso sobre os parâmetros de layout da sua visualização, implemente onMeasure(). Os parâmetros desse método são valores View.MeasureSpec que informam o tamanho que a visualização pai quer que sua visualização tenha e se esse tamanho é o máximo ou apenas uma sugestão. Como otimização, esses valores são armazenados como números inteiros compactados. Use os métodos estáticos de View.MeasureSpec para descompactar as informações armazenadas em cada número inteiro.

Veja um exemplo de implementação de onMeasure(): Nessa implementação, ela tenta tornar a área grande o suficiente para que o gráfico seja tão grande quanto o rótulo:

Kotlin

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // Try for a width based on your minimum.
    val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
    val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1)

    // Whatever the width is, ask for a height that lets the pie get as big as
    // it can.
    val minh: Int = View.MeasureSpec.getSize(w) - textWidth.toInt() + paddingBottom + paddingTop
    val h: Int = View.resolveSizeAndState(minh, heightMeasureSpec, 0)

    setMeasuredDimension(w, h)
}

Java

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on your minimum.
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   // Whatever the width is, ask for a height that lets the pie get as big as it
   // can.
   int minh = MeasureSpec.getSize(w) - (int)textWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(minh, heightMeasureSpec, 0);

   setMeasuredDimension(w, h);
}

Há três coisas importantes a serem observadas nesse código:

  • Os cálculos consideram o padding da visualização. Como mencionado anteriormente, essa é a responsabilidade da visualização.
  • O método auxiliar resolveSizeAndState() é usado para criar os valores finais de largura e altura. Esse auxiliar retorna um valor View.MeasureSpec apropriado comparando o tamanho necessário da visualização com o valor transmitido em onMeasure().
  • onMeasure() não tem valor de retorno. Em vez disso, o método comunica os resultados chamando setMeasuredDimension(). É obrigatório chamar esse método. Se você omitir a chamada, a View classe lançará uma exceção de tempo de execução.

Desenhar

Depois de definir o código de criação e medição de objetos, implemente onDraw(). Cada visualização implementa onDraw() de maneira diferente, mas há algumas operações comuns que a maioria das visualizações compartilha:

  • Desenhe texto usando drawText(). Especifique o tipo de letra chamando setTypeface() e a cor do texto chamando setColor().
  • Desenhe formas primitivas usando drawRect(), drawOval(), e drawArc(). Mude se as formas são preenchidas, contornadas ou ambas chamando setStyle().
  • Desenhe formas mais complexas usando a Path classe. Defina uma forma adicionando linhas e curvas a um Path objeto e, em seguida, desenhe a forma usando drawPath(). Assim como as formas primitivas, os caminhos podem ser contornados, preenchidos ou ambos, dependendo de setStyle().
  • Defina preenchimentos de gradiente criando LinearGradient objetos. Chame setShader() para usar seu LinearGradient em formas sólidas.
  • Desenhe bitmaps usando drawBitmap().

O código a seguir desenha uma combinação de texto, linhas e formas:

Kotlin

private val data = mutableListOf<Item>() // A list of items that are displayed.

private var shadowBounds = RectF()       // Calculated in onSizeChanged.
private var pointerRadius: Float = 2f    // Obtained from styled attributes.
private var pointerX: Float = 0f         // Calculated in onSizeChanged.
private var pointerY: Float = 0f         // Calculated in onSizeChanged.
private var textX: Float = 0f            // Calculated in onSizeChanged.
private var textY: Float = 0f            // Calculated in onSizeChanged.
private var bounds = RectF()             // Calculated in onSizeChanged.
private var currentItem: Int = 0         // The index of the currently selected item.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    canvas.apply {
        // Draw the shadow.
        drawOval(shadowBounds, shadowPaint)

        // Draw the label text.
        drawText(data[currentItem].label, textX, textY, textPaint)

        // Draw the pie slices.
        data.forEach {item ->
            piePaint.shader = item.shader
            drawArc(
                bounds,
                360 - item.endAngle,
                item.endAngle - item.startAngle,
                true,
                piePaint
            )
        }

        // Draw the pointer.
        drawLine(textX, pointerY, pointerX, pointerY, textPaint)
        drawCircle(pointerX, pointerY, pointerRadius, textPaint)
    }
}

// Maintains the state for a data item.
private data class Item(
    var label: String,      
    var value: Float = 0f,

    @ColorInt
    var color: Int = 0,

    // Computed values.
    var startAngle: Float = 0f,
    var endAngle: Float = 0f,

    var shader: Shader
)

Java

private List<Item> data = new ArrayList<Item>();  // A list of items that are displayed.

private RectF shadowBounds;                       // Calculated in onSizeChanged.
private float pointerRadius;                      // Obtained from styled attributes.
private float pointerX;                           // Calculated in onSizeChanged.
private float pointerY;                           // Calculated in onSizeChanged.
private float textX;                              // Calculated in onSizeChanged.
private float textY;                              // Calculated in onSizeChanged.
private RectF bounds;                             // Calculated in onSizeChanged.
private int currentItem = 0;                      // The index of the currently selected item.

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

    // Draw the shadow.
    canvas.drawOval(
            shadowBounds,
            shadowPaint
    );

    // Draw the label text.
    canvas.drawText(data.get(currentItem).label, textX, textY, textPaint);

    // Draw the pie slices.
    for (int i = 0; i < data.size(); ++i) {
        Item it = data.get(i);
        piePaint.setShader(it.shader);
        canvas.drawArc(
                bounds,
                360 - it.endAngle,
                it.endAngle - it.startAngle,
                true, 
                piePaint
        );
    }

    // Draw the pointer.
    canvas.drawLine(textX, pointerY, pointerX, pointerY, textPaint);
    canvas.drawCircle(pointerX, pointerY, pointerRadius, textPaint);
}

// Maintains the state for a data item.
private class Item {
    public String label;
    public float value;
    @ColorInt
    public int color;

    // Computed values.
    public int startAngle;
    public int endAngle;

    public Shader shader;
}    

Aplicar efeitos gráficos

O Android 12 (nível 31 da API) adiciona a classe RenderEffect, que aplica efeitos gráficos comuns, como desfoques, filtros de cores, efeitos de sombreador do Android e muito mais, a objetos View e hierarquias de renderização. É possível combinar efeitos como efeitos em cadeia, que consistem em um efeito interno e externo, ou efeitos combinados. O suporte a esse recurso varia de acordo com a capacidade de processamento do dispositivo.

Também é possível aplicar efeitos ao subjacente RenderNode para a View chamando View.setRenderEffect(RenderEffect).

Para implementar um objeto RenderEffect, faça o seguinte:

view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))

É possível criar a visualização de maneira programática ou inflá-la em um layout XML e recuperá-la usando a vinculação de visualizações ou findViewById().