Criar um desenho personalizado

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

A parte mais importante de uma visualização personalizada é a aparência. Desenho personalizado pode ser fácil ou complexo, de acordo com as necessidades do seu aplicativo. Este documento abrange 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 onDraw() . O parâmetro para onDraw() é uma Canvas objeto que a visualização pode usar para se desenhar. A classe Canvas define métodos para desenhar texto, linhas, bitmaps e muitos outros gráficos primitivas. Use esses métodos no onDraw() para criar o interface do usuário (UI) personalizada.

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

Criar objetos de desenho

A android.graphics divide o desenho em duas áreas:

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

Por exemplo, Canvas oferece um método para desenhar uma linha e O Paint fornece métodos para definir a cor dessa linha. Canvas tem um método para desenhar um retângulo, e Paint define se o retângulo será preenchido com uma cor ou ficará vazio. Canvas define formas que você pode desenhar na tela e Paint define a cor, o estilo, a fonte e assim por diante de cada forma que você desenha.

Antes de desenhar algo, crie um ou mais objetos Paint. A o exemplo a seguir faz isso em um método chamado init. Esse método é chamado a partir do construtor de Java, mas pode ser inicializado em linha 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 redesenhados com frequência, e muitos objetos de desenho exigem inicialização dispendiosa. Criar significativamente objetos de desenho no método onDraw() reduz o desempenho e pode deixar a interface lenta.

Processar eventos de layout

Para desenhar corretamente sua visualização personalizada, descubra qual é o tamanho dela. Personalizado complexo visualizações muitas vezes precisam realizar vários cálculos de layout dependendo do tamanho e a forma da área deles na tela. Nunca faça suposições sobre o tamanho na tela. Mesmo que apenas um app use sua visualização, ele precisa: lidar com diferentes tamanhos de tela, várias densidades de tela e vários aspectos proporções nos modos retrato e paisagem.

Embora View tem muitos métodos para lidar com a medição, a maioria deles não precisa ser substituído. Se sua visualização não precisar de controle especial sobre o tamanho, somente substituir um método: onSizeChanged():

onSizeChanged() é chamado quando sua visualização recebe pela primeira vez um e também se o tamanho da visualização mudar por qualquer motivo. Calcular posições, dimensões e outros valores relacionados ao tamanho da visualização onSizeChanged(), em vez de recalcular esses valores toda vez que você desenhar. No exemplo a seguir, onSizeChanged() é onde a visualização calcula o retângulo delimitador do gráfico e a posição relativa do rótulo de texto e outros elementos visuais.

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

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ê precisar de um controle mais preciso sobre os parâmetros de layout de sua visualização, implemente onMeasure(): Os parâmetros desse método são View.MeasureSpec valores que indicam o tamanho que a visualização pai quer ter e seja o tamanho máximo ou apenas uma sugestão. Como otimização, esses valores são armazenados como números inteiros compactados, e você usa os métodos estáticos do View.MeasureSpec para descompactar as informações armazenadas em cada número inteiro.

Veja um exemplo de implementação de onMeasure(): Neste ela tenta aumentar a área dela o suficiente para deixar o gráfico do mesmo tamanho como 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. Conforme mencionado antes, essa é responsabilidade da visualização.
  • Método auxiliar resolveSizeAndState() é usada para criar os valores finais de largura e altura. Este assistente retorna um valor View.MeasureSpec apropriado comparando o o tamanho necessário da visualização para o valor transmitido para onMeasure().
  • onMeasure() não tem valor de retorno. Em vez disso, o método comunica seus resultados chamando setMeasuredDimension(): É obrigatório chamar esse método. Se você omitir essa chamada, o A classe View gera uma exceção de execução.

Desenho

Depois de definir o código de criação e medição de objetos, é possível implementar onDraw(): Cada visualização implementa o onDraw() de forma diferente, mas existem algumas operações comuns que a maioria das visualizações compartilha:

  • Desenhar texto usando drawText(): Especifique a família tipográfica chamando setTypeface() e a cor do texto chamando setColor().
  • Desenhar formas primitivas usando drawRect(), drawOval(), e drawArc() Defina se as formas são preenchidas, circunscritos ou ambos chamando setStyle():
  • Desenhe formas mais complexas usando o Path . Definir uma forma adicionando linhas e curvas a uma Path e desenhe a forma usando drawPath(). Assim como nas formas primitivas, os caminhos podem ser contornados, preenchidos ou ambos, dependendo de setStyle().
  • Definir preenchimentos de gradiente criando LinearGradient objetos. Ligação setShader() para usar o LinearGradient em formas preenchidas.
  • Desenhe bitmaps usando drawBitmap():

O código a seguir desenha uma mistura 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 RenderEffect , que aplica efeitos gráficos comuns, como desfoques, filtros de cor, efeitos de sombreador do Android e muito mais para objetos View e hierarquias de renderização. É possível combinar efeitos como efeitos de cadeia, que consistem de um efeito interno e externo ou efeitos mistos. Suporte para este recurso varia de acordo com a capacidade de processamento do dispositivo.

Também é possível aplicar efeitos aos RenderNode para um 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 forma programática ou inflá-la usando um layout XML e recupere-as usando a vinculação de visualizações ou findViewById()