Participe do evento ⁠#Android11: apresentação de lançamento da versão Beta no dia 3 de junho.

Desenho personalizado

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 app. Esta lição aborda algumas das operações mais comuns.

Além desta lição, você pode encontrar mais informações sobre o assunto em Canvas e Drawables.

Substituir o onDraw()

A etapa mais importante no desenho de uma visualização personalizada é modificar o método onDraw(). O parâmetro para onDraw() é um objeto Canvas que a visualização pode usar para se desenhar. A classe Canvas define métodos para desenhar texto, linhas, bitmaps e muitos outros elementos gráficos primitivos. Você pode usar esses métodos em onDraw() para criar sua interface do usuário (IU) personalizada.

Antes de chamar qualquer método de desenho, é necessário criar um objeto Paint. A próxima seção discute Paint em 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, enquanto Paint oferece métodos para definir a cor dessa linha. Canvas tem um método para desenhar um retângulo, enquanto Paint define se ele será preenchido com uma cor ou ficará vazio. Simplificando, Canvas define formas que podem ser desenhadas na tela, enquanto Paint define a cor, o estilo, a fonte e assim por diante de cada forma desenhada.

Portanto, antes de desenhar algo, crie um ou mais objetos Paint. O exemplo de PieChart faz isso em um método denominado init, que é chamado a partir do construtor do Java, mas é possível inicializar in-line no Kotlin:

Kotlin

    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 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 muita 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 IU pareça lenta.

Gerenciar eventos de layout

Para desenhar corretamente sua visualização personalizada, é preciso saber 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. Evite fazer 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 do PieChart, o onSizeChanged() é onde a visualização PieChart calcula o retângulo delimitador do gráfico de pizza 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 todo o padding da visualização. Gerencie os valores de padding ao calcular o tamanho da sua visualização. Veja um snippet de PieChart.onSizeChanged() que mostra como fazer isso:

Kotlin

    // Account for padding
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

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

    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

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

Java

    // 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 we 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(): Nesta implementação, o PieChart tenta tornar a área grande o suficiente para que o gráfico de pizza tenha o tamanho do rótulo:

Kotlin

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

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

        setMeasuredDimension(w, h)
    }
    

Java

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

       // Whatever the width ends up being, ask for a height that would let the pie
       // get as big as it can
       int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
       int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, 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, isso é de 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 desejado da visualização com a especificação transmitida 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 essa chamada, a classe View gerará 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 na maioria das visualizações:

Por exemplo, veja o código que desenha PieChart. Ele usa uma mistura de texto, linhas e formas.

Kotlin

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

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

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

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

            // Draw the pointer
            drawLine(textX, pointerY, pointerX, pointerY, textPaint)
            drawCircle(pointerX, pointerY, pointerSize, mTextPaint)
        }
    }
    

Java

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

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

       // Draw the label text
       canvas.drawText(data.get(currentItem).mLabel, 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, pointerSize, mTextPaint);
    }