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 valorView.MeasureSpec
apropriado comparando o o tamanho necessário da visualização para o valor transmitido paraonMeasure()
. onMeasure()
não tem valor de retorno. Em vez disso, o método comunica seus resultados chamandosetMeasuredDimension()
: É obrigatório chamar esse método. Se você omitir essa chamada, o A classeView
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 chamandosetTypeface()
e a cor do texto chamandosetColor()
. - Desenhar formas primitivas usando
drawRect()
,drawOval()
, edrawArc()
Defina se as formas são preenchidas, circunscritos ou ambos chamandosetStyle()
: - Desenhe formas mais complexas usando o
Path
. Definir uma forma adicionando linhas e curvas a umaPath
e desenhe a forma usandodrawPath()
. Assim como nas formas primitivas, os caminhos podem ser contornados, preenchidos ou ambos, dependendo desetStyle()
. -
Definir preenchimentos de gradiente criando
LinearGradient
objetos. LigaçãosetShader()
para usar oLinearGradient
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()