Com o Jetpack Compose, fica mais fácil trabalhar com gráficos personalizados. Muitos apps precisam ter
controle preciso sobre o que é desenhado na tela. Isso pode
ser tão simples quanto colocar uma caixa ou um círculo no lugar certo da tela ou
pode ser uma organização elaborada de elementos gráficos em vários estilos
diferentes. Com a abordagem declarativa do Compose, toda a configuração gráfica
ocorre em um só lugar, em vez de ser dividida entre uma chamada de método e um objeto
auxiliar Paint
. O Compose cuida da criação e atualização dos objetos necessários de
forma eficiente.
Gráficos declarativos com o Compose
O Compose amplia a abordagem declarativa para a forma como ele processa gráficos. A abordagem do Compose apresenta diversas vantagens:
- O Compose minimiza o state em elementos gráficos, o que ajuda a evitar dificuldades de programação do estado.
- Quando algo é desenhado, todas as opções ficam no local esperado, dentro da função de composição.
- As APIs gráficas do Compose cuidam da criação e da liberação de objetos de forma eficiente.
Canvas
A principal função que pode ser composta para gráficos personalizados é
Canvas
.
Canvas
é posicionado no layout da mesma forma que qualquer outro
elemento de IU do Compose seria. Dentro de Canvas
, é possível desenhar elementos com controle preciso
sobre o estilo e localização.
Por exemplo, o código a seguir cria Canvas
que preenche todo o
espaço disponível no elemento pai:
Canvas(modifier = Modifier.fillMaxSize()) {
}
Canvas
expõe automaticamente um
DrawScope
,
ambiente de desenho com escopo que mantém o próprio estado. Isso permite que você defina
os parâmetros de um grupo de elementos gráficos. O DrawScope
fornece vários
campos úteis, como size
, um objeto Size
que especifica as dimensões atuais e
máximas do DrawScope
.
Por exemplo, suponha que você queira desenhar uma linha diagonal do canto superior direito
da tela até o canto inferior esquerdo. Isso poderia ser feito adicionando
uma função drawLine
que pode ser composta
Canvas(modifier = Modifier.fillMaxSize()) { val canvasWidth = size.width val canvasHeight = size.height drawLine( start = Offset(x = canvasWidth, y = 0f), end = Offset(x = 0f, y = canvasHeight), color = Color.Blue ) }
Figura 1. Uso do drawLine
para desenhar uma linha na tela. O código define
a cor da linha, mas usa a largura padrão.
É possível usar outros parâmetros para personalizar o desenho. Por exemplo, por padrão,
a linha é desenhada com uma espessura fina, que é mostrada como um pixel de largura,
independente da escala do desenho. É possível modificar o padrão definindo um
valor strokeWidth
:
Canvas(modifier = Modifier.fillMaxSize()) { val canvasWidth = size.width val canvasHeight = size.height drawLine( start = Offset(x = canvasWidth, y = 0f), end = Offset(x = 0f, y = canvasHeight), color = Color.Blue, strokeWidth = 5F ) }
Figura 2. Modificação da linha da figura 1, substituindo a largura padrão.
Existem muitas outras funções de desenho simples, como
drawRect
e
drawCircle
.
Por exemplo, o código abaixo desenha um círculo preenchido no meio da tela, com
diâmetro igual à metade da menor dimensão da tela:
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasWidth = size.width
val canvasHeight = size.height
drawCircle(
color = Color.Blue,
center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
radius = size.minDimension / 4
)
}
Figura 3. Uso de drawCircle
para exibir um círculo no centro da tela.
Por padrão, drawCircle
desenha um círculo preenchido, portanto, não é necessário especificar
essa configuração explicitamente.
As funções de desenho têm parâmetros padrão úteis. Por exemplo, por padrão,
drawRectangle()
preenche todo o escopo pai, e drawCircle()
tem um raio
igual à metade da menor dimensão do pai. Como sempre é o caso com o Kotlin,
é possível simplificar e facilitar ainda mais seu código aproveitando os valores dos
parâmetros padrão e definindo somente os
parâmetros que precisam ser modificados. Isso pode ser aproveitado fornecendo parâmetros explícitos
aos métodos de exibição do DrawScope
, já que as configurações padrão dos
elementos mostrados têm como base as configurações do escopo pai.
DrawScope
Como já observado, cada Canvas
do Compose expõe um DrawScope
, um ambiente de desenho
com escopo, em que você emite os comandos de desenho.
Por exemplo, o código a seguir desenha um retângulo no canto superior esquerdo da tela:
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasQuadrantSize = size / 2F
drawRect(
color = Color.Green,
size = canvasQuadrantSize
)
}
É possível usar a função
DrawScope.inset()
para ajustar os parâmetros padrão do escopo atual, mudar os limites de desenho
e realizar a translação dos desenhos adequadamente. Operações como inset()
são válidas para todas as operações de desenho no lambda correspondente:
val canvasQuadrantSize = size / 2F
inset(50F, 30F) {
drawRect(
color = Color.Green,
size = canvasQuadrantSize
)
}
DrawScope
oferece outras transformações simples, como
rotate()
.
Por exemplo, o código a seguir desenha um retângulo no centro que preenche um nono
da tela:
val canvasSize = size
val canvasWidth = size.width
val canvasHeight = size.height
drawRect(
color = Color.Gray,
topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
size = canvasSize / 3F
)
Figura 4. Uso de drawRect
para desenhar um retângulo preenchido no centro
da tela.
É possível girar o retângulo aplicando uma rotação ao DrawScope
:
rotate(degrees = 45F) {
drawRect(
color = Color.Gray,
topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
size = canvasSize / 3F
)
}
Figura 5. Usamos rotate()
para aplicar uma rotação ao escopo de desenho
atual, que gira o retângulo em 45 graus.
Caso você queira aplicar várias transformações aos desenhos, a melhor
abordagem é não criar ambientes DrawScope
aninhados. Em vez disso,
use a função
withTransform()
,
que cria e aplica uma única transformação que combina todas
as mudanças esperadas. Usar withTransform()
é mais eficiente que fazer
chamadas aninhadas para transformações individuais, porque, dessa forma, todas as transformações são
executadas em uma única operação, e o Compose não precisar calcular
e salvar cada uma das transformações aninhadas.
Por exemplo, o código a seguir aplica uma translação e uma rotação ao retângulo:
withTransform({
translate(left = canvasWidth / 5F)
rotate(degrees = 45F)
}) {
drawRect(
color = Color.Gray,
topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
size = canvasSize / 3F
)
}
Figura 6. Aqui, usamos withTransform
para aplicar uma rotação e uma
translação, girando e deslocando o retângulo para a esquerda.