Este codelab faz parte do curso Android avançado no Kotlin. Você aproveitará mais o curso se fizer os codelabs em sequência, mas isso não é obrigatório. Todos os codelabs do curso estão listados na página de destino dos codelabs avançados do Android em Kotlin (link em inglês).
Introdução
No Android, existem várias técnicas disponíveis para implementar animações e gráficos 2D personalizados em visualizações.
Além de usar drawables, você pode criar desenhos 2D usando os métodos de desenho da classe Canvas
. Canvas
é uma superfície de desenho 2D que fornece métodos para desenhar. Isso é útil quando o app precisa ser redesenhado regularmente, porque o que o usuário vê muda com o tempo. Neste codelab, você aprenderá a criar e desenhar em uma tela exibida em uma View
.
Os tipos de operações que você pode realizar em uma tela incluem o seguinte:
- Preencha toda a tela com cores.
- Desenhe formas, como retângulos, arcos e caminhos, com o estilo definido em um objeto
Paint
. O objetoPaint
contém as informações de estilo e cor sobre como desenhar geometrias (como linha, retângulo, forma oval e caminhos) ou, por exemplo, a fonte do texto. - Aplique transformações, como de conversão, escalonamento ou personalizadas.
- Faça um clipe, ou seja, aplique uma forma ou um caminho à tela para definir as partes visíveis.
Como você pode pensar no desenho do Android (supersimplificado)
Desenhar no Android ou em qualquer outro sistema moderno é um processo complexo que inclui camadas de abstrações e otimizações no hardware. A forma como o Android desenha é um tema fascinante sobre o que foi escrito, e os detalhes dele estão além do escopo deste codelab.
No contexto deste codelab e do app que é desenhado em uma tela para exibição em tela cheia, você pode pensar nele da seguinte maneira.
- Você precisa de uma visualização para mostrar o que está desenhando. Essa pode ser uma das visualizações fornecidas pelo sistema Android. Ou, neste codelab, você criará uma visualização personalizada que serve como a visualização de conteúdo para seu app (
MyCanvasView
). - Essa visualização, como todas as visualizações, vem com uma tela própria (
canvas
). - A forma mais básica de desenhar na tela de uma visualização é modificar o método
onDraw()
e desenhar nela. - Ao criar um desenho, você precisa armazenar em cache o que já desenhou. Há várias maneiras de armazenar seus dados em cache, uma em um bitmap (
extraBitmap
). Outra é salvar um histórico do que você desenhou como coordenadas e instruções. - Para desenhar no bitmap de armazenamento em cache (
extraBitmap
) usando a API de desenho do Canvas, crie uma tela de armazenamento em cache (extraCanvas
) para ele. - Em seguida, desenhe na tela de armazenamento em cache (
extraCanvas
), que é exibida no bitmap de armazenamento em cache (extraBitmap
). - Para exibir todos os desenhos na tela, diga à tela da visualização (
canvas
) para desenhar o bitmap de armazenamento em cache (extraBitmap
).
O que você já precisa saber
- Como criar um app com uma atividade, um layout básico e executá-lo usando o Android Studio.
- Como associar manipuladores de eventos a visualizações.
- Como criar uma visualização personalizada.
O que você vai aprender
- Como criar um
Canvas
e desenhá-lo em resposta ao toque do usuário.
Atividades do laboratório
- Crie um app que desenha linhas na resposta para o usuário tocar nela.
- Capture eventos de movimento e, em resposta, desenhe linhas em uma tela exibida em uma visualização personalizada em tela cheia.
O app MiniPaint usa uma visualização personalizada para exibir uma linha em resposta aos toques do usuário, conforme mostrado na captura de tela abaixo.
Etapa 1. Criar o projeto MiniPaint
- Crie um novo projeto Kotlin chamado MiniPaint que use o modelo Empty Activity.
- Abra o arquivo
app/res/values/colors.xml
e adicione as duas cores a seguir.
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>
- Abrir
styles.xml
- No pai do estilo
AppTheme
fornecido, substituaDarkActionBar
porNoActionBar
. Isso remove a barra de ações para que você possa desenhar em tela cheia.
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
Etapa 2: Criar a classe MyCanvasView
Nesta etapa, você criará uma visualização personalizada, MyCanvasView
, para desenhar.
- No pacote
app/java/com.example.android.minipaint
, crie um novo arquivo/classe do Kotlin chamadoMyCanvasView
. - Faça a classe
MyCanvasView
estender a classeView
e transmita acontext: Context
. Aceitar as importações sugeridas.
import android.content.Context
import android.view.View
class MyCanvasView(context: Context) : View(context) {
}
Etapa 3. Definir MyCanvasView como a visualização de conteúdo
Para exibir o que você desenhará no MyCanvasView
, é preciso defini-lo como a visualização de conteúdo da MainActivity
.
- Abra
strings.xml
e defina uma string para ser usada na descrição do conteúdo da visualização.
<string name="canvasContentDescription">Mini Paint is a simple line drawing app.
Drag your fingers to draw. Rotate the phone to clear.</string>
- Abrir
MainActivity.kt
- No
onCreate()
, exclua osetContentView(R.layout.activity_main)
. - Crie uma instância de
MyCanvasView
.
val myCanvasView = MyCanvasView(this)
- Abaixo disso, solicite a tela cheia para o layout de
myCanvasView
. Para isso, defina a sinalizaçãoSYSTEM_UI_FLAG_FULLSCREEN
namyCanvasView
. Dessa forma, a visualização preenche completamente a tela.
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN
- Adicione uma descrição do conteúdo.
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)
- Abaixo dela, defina a visualização do conteúdo como
myCanvasView
.
setContentView(myCanvasView)
- Execute o app. Você verá uma tela totalmente branca, porque a tela não tem tamanho e você ainda não desenhou nada.
Etapa 1. Substituir onSizeChanged()
O método onSizeChanged()
é chamado pelo sistema Android sempre que uma visualização muda de tamanho. Como a visualização é iniciada sem tamanho, o método onSizeChanged()
da visualização também é chamado depois que a atividade é criada e inflada. Portanto, esse método onSizeChanged()
é o lugar ideal para criar e configurar a tela da visualização.
- Em
MyCanvasView
, no nível da classe, defina variáveis para uma tela e um bitmap. Chame-os comoextraCanvas
eextraBitmap
. Eles são o bitmap e a tela para armazenar em cache o que foi desenhado antes.
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap
- Defina uma variável de nível de classe
backgroundColor
para a cor do plano de fundo da tela e inicialize-a como ocolorBackground
definido anteriormente.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
- No
MyCanvasView
, substitua o métodoonSizeChanged()
. Esse método de callback é chamado pelo sistema Android com as dimensões de tela alteradas, ou seja, com uma nova largura e altura (para alterar) e a largura e altura antigas (para mudar).
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
super.onSizeChanged(width, height, oldWidth, oldHeight)
}
- Dentro de
onSizeChanged()
, crie uma instância daBitmap
com a nova largura e altura, que são o tamanho da tela, e atribua-a àextraBitmap
. O terceiro argumento é a configuração do cor bitmap.ARGB_8888
armazena cada cor em quatro bytes e é recomendada.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
- Crie uma instância
Canvas
a partir deextraBitmap
e atribua-a aextraCanvas
.
extraCanvas = Canvas(extraBitmap)
- Especifique a cor do plano de fundo para preencher
extraCanvas
.
extraCanvas.drawColor(backgroundColor)
- Observando
onSizeChanged()
, um novo bitmap e tela será criado sempre que a função for executada. Você precisa de um novo bitmap porque o tamanho mudou. No entanto, isso é um vazamento de memória, deixando os bitmaps antigos por aí. Para corrigir isso, recicleextraBitmap
antes de criar a próxima adicionando esse código logo após a chamada parasuper
if (::extraBitmap.isInitialized) extraBitmap.recycle()
Etapa 2: Substituir o onDraw()
Todo o trabalho de desenho de MyCanvasView
acontece em onDraw()
.
Para começar, exiba a tela, preenchendo-a com a cor de fundo definida em onSizeChanged()
.
- Substitua
onDraw()
e desenhe o conteúdo daextraBitmap
armazenada em cache na tela associada à visualização. O métododrawBitmap()
Canvas
vem em várias versões. Nesse código, você fornece o bitmap, as coordenadas x e y (em pixels) do canto superior esquerdo enull
para oPaint
, como você definirá mais tarde.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
A tela que é transmitida para o onDraw()
e usada pelo sistema para exibir o bitmap é diferente daquela que você criou no método onSizeChanged()
e que você usou para desenhar no bitmap.
- Execute o app. Você verá toda a tela preenchida com a cor de fundo especificada.
Para desenhar, você precisa de um objeto Paint
que especifique como os itens serão estilizados quando desenhados e um Path
que especifica o que será exibido.
Etapa 1. Inicializar um objeto Paint
- Em MyCanvasView.kt, no nível do arquivo superior, defina uma constante para a largura do traço.
private const val STROKE_WIDTH = 12f // has to be float
- No nível da classe
MyCanvasView
, defina uma variáveldrawColor
para manter a cor usada para desenhar e inicialize-a com o recursocolorPaint
definido anteriormente.
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)
- No nível da classe, abaixo, adicione uma variável
paint
para um objetoPaint
e inicialize-a da seguinte maneira.
// Set up the paint with which to draw.
private val paint = Paint().apply {
color = drawColor
// Smooths out edges of what is drawn without affecting shape.
isAntiAlias = true
// Dithering affects how colors with higher-precision than the device are down-sampled.
isDither = true
style = Paint.Style.STROKE // default: FILL
strokeJoin = Paint.Join.ROUND // default: MITER
strokeCap = Paint.Cap.ROUND // default: BUTT
strokeWidth = STROKE_WIDTH // default: Hairline-width (really thin)
}
- O
color
dopaint
é odrawColor
definido anteriormente. isAntiAlias
define se a suavização de borda será aplicada. DefinirisAntiAlias
comotrue
, suaviza as bordas do que é desenhado sem afetar a forma.isDither
, quandotrue
, afeta a forma como as cores são reduzidas em relação à precisão do dispositivo. Por exemplo, o pontilhamento é a forma mais comum de reduzir o intervalo de cores de imagens para 256 cores, ou menos.style
define o tipo de pintura a ser feito em um traço, que é basicamente uma linha.Paint.Style
especifica se o primitivo que está sendo desenhado é preenchido, traçado ou ambos (na mesma cor). O padrão é preencher o objeto ao qual a pintura é aplicada. ("Preencher") colore o interior da forma, enquanto "quot;stroke" segue o contorno dela".strokeJoin
dePaint.Join
especifica como linhas e segmentos de curva se juntam em um caminho traçado. O padrão éMITER
.strokeCap
define o formato do fim da linha como um limite.Paint.Cap
especifica como o início e o fim das linhas e caminhos traçados. O padrão éBUTT
.strokeWidth
especifica a largura do traço em pixels. O padrão é a largura da linha do cabelo, que é realmente fina, então ela é definida como a constanteSTROKE_WIDTH
definida anteriormente.
Etapa 2: Inicializar um objeto de Caminho
O Path
é o caminho que o usuário está desenhando.
- Em
MyCanvasView
, adicione uma variávelpath
e a inicialize com um objetoPath
para armazenar o caminho que está sendo desenhado ao seguir o toque do usuário na tela. Importeandroid.graphics.Path
para oPath
.
private var path = Path()
Etapa 1. Responder a movimentos na tela
O método onTouchEvent()
em uma visualização é chamado sempre que o usuário toca na tela.
- No
MyCanvasView
, substitua o métodoonTouchEvent()
para armazenar em cache as coordenadasx
ey
do transmitido emevent
. Em seguida, use uma expressãowhen
para processar eventos de movimento. Para isso, toque na tela, mova-a e solte o toque. Esses são os eventos de interesse para desenhar uma linha na tela. Para cada tipo de evento, chame um método utilitário, conforme mostrado no código abaixo. Consulte a documentação da classeMotionEvent
para ver uma lista completa de eventos de toque.
override fun onTouchEvent(event: MotionEvent): Boolean {
motionTouchEventX = event.x
motionTouchEventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> touchStart()
MotionEvent.ACTION_MOVE -> touchMove()
MotionEvent.ACTION_UP -> touchUp()
}
return true
}
- No nível da classe, adicione as variáveis
motionTouchEventX
emotionTouchEventY
ausentes para armazenar em cache as coordenadas x e y do evento de toque atual (as coordenadasMotionEvent
). Inicialize-os em0f
.
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
- Crie stubs para as três funções
touchStart()
,touchMove()
etouchUp()
.
private fun touchStart() {}
private fun touchMove() {}
private fun touchUp() {}
- O código será criado e executado, mas você ainda não verá nada diferente do plano de fundo colorido.
Etapa 2: Implementar touchStart()
Esse método é chamado quando o usuário toca pela primeira vez na tela.
- No nível da classe, adicione variáveis para armazenar em cache os valores mais recentes de x e y. Depois que o usuário para de se mover e levanta o toque, estes são o ponto de partida para o próximo caminho (o próximo segmento da linha a ser desenhada).
private var currentX = 0f
private var currentY = 0f
- Implemente o método
touchStart()
da seguinte maneira. Redefina opath
, vá para as coordenadas x-y do evento de toque (motionTouchEventX
emotionTouchEventY
) e atribuacurrentX
ecurrentY
a esse valor.
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}
Etapa 3. Implementar touchMove()
- No nível da classe, adicione uma variável
touchTolerance
e defina-a comoViewConfiguration.get(context).scaledTouchSlop
.
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop
Usando um caminho, não é necessário desenhar cada pixel e sempre solicitar uma atualização da tela. Em vez disso, você pode interpolar um caminho entre os pontos para ter um desempenho muito melhor.
- Se o dedo mal se mover, não será necessário desenhar.
- Se o dedo tiver se movido menos que a distância do
touchTolerance
, não desenhe. scaledTouchSlop
retorna a distância em pixels que um toque pode percorrer antes que o sistema pense que o usuário está rolando a tela.
- Defina o método
touchMove()
. Calcule a distância percorrida (dx
,dy
), crie uma curva entre os dois pontos e armazene-a empath
, atualize o cálculo decurrentX
ecurrentY
em execução e desenhe opath
. Em seguida, chameinvalidate()
para forçar o novo desenho da tela com opath
atualizado.
private fun touchMove() {
val dx = Math.abs(motionTouchEventX - currentX)
val dy = Math.abs(motionTouchEventY - currentY)
if (dx >= touchTolerance || dy >= touchTolerance) {
// QuadTo() adds a quadratic bezier from the last point,
// approaching control point (x1,y1), and ending at (x2,y2).
path.quadTo(currentX, currentY, (motionTouchEventX + currentX) / 2, (motionTouchEventY + currentY) / 2)
currentX = motionTouchEventX
currentY = motionTouchEventY
// Draw the path in the extra bitmap to cache it.
extraCanvas.drawPath(path, paint)
}
invalidate()
}
Esse método é mais detalhado:
- Calcule a distância que foi movida (
dx, dy
). - Se o movimento era além da tolerância ao toque, adicione um segmento ao caminho.
- Defina o ponto de partida do próximo segmento como o endpoint desse segmento.
- Usar
quadTo()
em vez delineTo()
para criar uma linha desenhada de forma suave sem cantos. Consulte Curvas do Bezier. - Chame
invalidate()
para (depois chamaronDraw()
e reexibir a visualização).
Etapa 4: implementar touchUp()
Quando o usuário levanta o toque, basta redefinir o caminho para que ele não seja desenhado novamente. Como nenhum elemento é desenhado, nenhuma invalidação é necessária.
- Implemente o método
touchUp()
.
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}
- Execute o código e use o dedo para desenhar na tela. Se você girar o dispositivo, a tela será limpa porque o estado de desenho não será salvo. Neste app de exemplo, isso foi projetado para oferecer ao usuário uma maneira simples de limpar a tela.
Etapa 5: desenhar um frame em volta do esboço
Conforme o usuário desenha na tela, o app cria o caminho e o salva no bitmap extraBitmap
. O método onDraw()
exibe o bitmap extra na tela da visualização. Você pode fazer mais desenhos em onDraw()
. Por exemplo, você pode desenhar formas depois de desenhar o bitmap.
Nesta etapa, você desenha um frame ao redor da borda da imagem.
- No
MyCanvasView
, adicione uma variável com o nomeframe
que contenha um objetoRect
.
private lateinit var frame: Rect
- No final do método
onSizeChanged()
, insira um encarte e adicione um código para criar aRect
que será usada para o frame, usando as novas dimensões e o encarte.
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)
- Em
onDraw()
, depois de desenhar o bitmap, desenhe um retângulo.
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)
- Execute o app. Observe o frame.
Tarefa (opcional): armazenar dados em um caminho
No app atual, as informações de desenho são armazenadas em um bitmap. Embora essa seja uma boa solução, ela não é a única forma possível de armazenar informações de desenho. A forma como você armazena seu histórico de desenhos depende do app e de vários requisitos. Por exemplo, se você estiver desenhando formas, salve uma lista delas com local e dimensões. Para o app MiniPaint, salve o caminho como um Path
. Veja abaixo um resumo geral sobre como fazer isso.
- Em
MyCanvasView
, remova todo o código paraextraCanvas
eextraBitmap
. - Adicione variáveis para o caminho até agora e para ele que está sendo desenhado.
// Path representing the drawing so far
private val drawing = Path()
// Path representing what's currently being drawn
private val curPath = Path()
- Em
onDraw()
, em vez de desenhar o bitmap, desenhe os caminhos armazenados e atuais.
// Draw the drawing so far
canvas.drawPath(drawing, paint)
// Draw any current squiggle
canvas.drawPath(curPath, paint)
// Draw a frame around the canvas
canvas.drawRect(frame, paint)
- Em
touchUp()
, adicione o caminho atual ao caminho anterior e redefina o caminho atual.
// Add the current path to the drawing so far
drawing.addPath(curPath)
// Rewind the current path for the next touch
curPath.reset()
- Execute o app. Sim, não haverá diferença.
Faça o download do código do codelab concluído.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-canvas
Como alternativa, é possível fazer o download do repositório como um arquivo ZIP, descompactá-lo e abri-lo no Android Studio.
- Uma
Canvas
é uma superfície de desenho 2D que fornece métodos para desenhar. - O
Canvas
pode ser associado a uma instância doView
que o exibe. - O objeto
Paint
contém as informações de estilo e cor sobre como desenhar geometrias (como linha, retângulo, forma oval e caminhos) e texto. - Um padrão comum para trabalhar com uma tela é criar uma visualização personalizada e modificar os métodos
onDraw()
eonSizeChanged()
. - Modifique o método
onTouchEvent()
para capturar toques do usuário e responder a eles desenhando itens. - Você pode usar um bitmap extra para armazenar em cache informações de desenhos que mudam com o tempo. Como alternativa, é possível armazenar formas ou um caminho.
Curso da Udacity:
- Como desenvolver apps Android com Kotlin (link em inglês)
Documentação do desenvolvedor Android:
- Classe
Canvas
- Classe
Bitmap
- Classe
View
- Classe
Paint
- Configurações do
Bitmap.config
- Classe
Path
- Página da Wikipédia sobre curvas de Bezier
- Canvas e drawables
- Série de artigos sobre arquitetura gráfica (avançado)
- drawables.
- onDraw().
- onSizeChanged().
MotionEvent
ViewConfiguration.get(context).scaledTouchSlop
Esta seção lista as possíveis atividades para os alunos que estão trabalhando neste codelab como parte de um curso ministrado por um instrutor. Cabe ao instrutor fazer o seguinte:
- Se necessário, atribua o dever de casa.
- Informe aos alunos como enviar o dever de casa.
- Atribua nota aos trabalhos de casa.
Os professores podem usar essas sugestões o quanto quiserem, e eles devem se sentir à vontade para passar o dever de casa como achar adequado.
Se você estiver fazendo este codelab por conta própria, use essas atividades para testar seu conhecimento.
Responda a estas perguntas
Pergunta 1
Quais dos seguintes componentes são necessários para trabalhar com um Canvas
? Selecione todas as opções aplicáveis.
▢ Bitmap
▢ Paint
▢ Path
▢ View
Pergunta 2
O que uma chamada para invalidate()
faz (em termos gerais)?
▢ Invalida e reinicia o app.
▢ Apaga o desenho do bitmap.
▢ Indica que o código anterior não deve ser executado.
▢ Informa ao sistema que é necessário redesenhar a tela.
Pergunta 3
Qual é a função dos objetos Canvas
, Bitmap
e Paint
?
▢ Plataforma de desenho 2D, bitmap exibido na tela, estilo das informações para desenho.
▢ Plataforma de desenho 3D, bitmap para armazenamento em cache do caminho e estilo das informações para desenho.
▢ Plataforma de desenho 2D, bitmap exibido na tela, estilo para a visualização.
▢ Cache para desenhar informações, bitmap para desenhar e informações de estilo para desenhos.
Para ver links de outros codelabs neste curso, consulte a página de destino dos codelabs avançados no Android.