Este codelab faz parte do curso Android avançado no Kotlin. Você vai aproveitar mais este 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 do Android avançado em Kotlin.
Introdução
No Android, você tem várias técnicas disponíveis para implementar gráficos e animações 2D personalizados em visualizações.
Além de usar recursos drawable, você pode criar desenhos 2D usando os métodos de desenho da classe Canvas. O Canvas é uma superfície de desenho 2D que oferece métodos para desenhar. Isso é útil quando o app precisa se renderizar regularmente, porque o que o usuário vê muda com o tempo. Neste codelab, você vai aprender a criar e desenhar em uma tela exibida em um View.
Os tipos de operações que você pode realizar em uma tela incluem:
- Preencha toda a tela com cor.
- Desenhe formas, como retângulos, arcos e caminhos estilizados conforme definido em um objeto
Paint. O objetoPaintcontém as informações de estilo e cor sobre como desenhar geometrias (como linhas, retângulos, ovais e caminhos) ou, por exemplo, o tipo de letra do texto. - Aplicar transformações, como tradução, escalonamento ou transformações personalizadas.
- Cortar, ou seja, aplicar uma forma ou trajetória à tela para definir as partes visíveis.

Como pensar no desenho do Android (super simplificado!)
Desenhar no Android ou em qualquer outro sistema moderno é um processo complexo que inclui camadas de abstrações e otimizações até o hardware. A forma como o Android renderiza é um assunto fascinante sobre o qual muito já foi escrito, e os detalhes estão fora do escopo deste codelab.
No contexto deste codelab e do app que desenha em uma tela para exibição em uma visualização de tela cheia, você pode pensar 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ê cria uma visualização personalizada que serve como visualização de conteúdo para seu app (
MyCanvasView). - Essa visualização, como todas as outras, vem com uma tela própria (
canvas). - Para a maneira mais básica de desenhar na tela de uma visualização, substitua o método
onDraw()e desenhe na tela. - Ao criar um desenho, é necessário armazenar em cache o que foi desenhado antes. Há várias maneiras de armazenar dados em cache. Uma delas é em um bitmap (
extraBitmap). Outra é salvar um histórico do que você desenhou como coordenadas e instruções. - Para desenhar no bitmap de cache (
extraBitmap) usando a API de desenho de tela, crie uma tela de cache (extraCanvas) para o bitmap de cache. - Em seguida, desenhe no canvas de cache (
extraCanvas), que desenha no bitmap de cache (extraBitmap). - Para mostrar tudo o que foi desenhado na tela, diga ao canvas da visualização (
canvas) para desenhar o bitmap de cache (extraBitmap).
O que você já precisa saber
- Como criar um app com uma atividade e 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
Canvase desenhar nele em resposta ao toque do usuário.
Atividades deste laboratório
- Crie um app que desenhe linhas na tela em resposta ao toque do usuário.
- Capturar eventos de movimento e, em resposta, desenhar linhas em uma tela que é mostrada em uma visualização personalizada em tela cheia.
O app MiniPaint usa uma visualização personalizada para mostrar 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.xmle adicione as duas cores a seguir.
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>- Abrir
styles.xml - No elemento pai do estilo
AppTheme, substituaDarkActionBarporNoActionBar. 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ê cria uma visualização personalizada, MyCanvasView, para desenho.
- No pacote
app/java/com.example.android.minipaint, crie um New > Kotlin File/Class chamadoMyCanvasView. - Faça a classe
MyCanvasViewestender a classeViewe transmita ocontext: Context. Aceite 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 mostrar o que você vai desenhar em MyCanvasView, defina-o como a visualização de conteúdo do MainActivity.
- Abra
strings.xmle defina uma string para usar 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 - Em
onCreate(), excluasetContentView(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 fazer isso, defina a flagSYSTEM_UI_FLAG_FULLSCREENemmyCanvasView. Assim, 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 disso, defina a visualização de conteúdo como
myCanvasView.
setContentView(myCanvasView)- Execute o app. Você vai ver uma tela completamente 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 começa sem tamanho, o método onSizeChanged() dela também é chamado depois que a atividade a cria e a aumenta. 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 deextraCanvaseextraBitmap. Esses são seu bitmap e 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
backgroundColorpara a cor de fundo da tela e inicialize-a com ocolorBackgroundque você definiu anteriormente.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)- Em
MyCanvasView, substitua o métodoonSizeChanged(). Esse método de callback é chamado pelo sistema Android com as dimensões da tela alteradas, ou seja, com uma nova largura e altura (para mudar para) e a largura e altura antigas (para mudar de).
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
super.onSizeChanged(width, height, oldWidth, oldHeight)
}- Em
onSizeChanged(), crie uma instância deBitmapcom a nova largura e altura, que são o tamanho da tela, e atribua aextraBitmap. O terceiro argumento é a configuração de cor do bitmap. OARGB_8888armazena cada cor em 4 bytes e é recomendado.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)- Crie uma instância
CanvasdeextraBitmape atribua aextraCanvas.
extraCanvas = Canvas(extraBitmap)- Especifique a cor de plano de fundo em que
extraCanvasserá preenchido.
extraCanvas.drawColor(backgroundColor)- Analisando
onSizeChanged(), um novo bitmap e uma nova tela são criados sempre que a função é executada. Você precisa de um novo bitmap porque o tamanho mudou. No entanto, isso é um vazamento de memória, deixando os bitmaps antigos por perto. Para corrigir isso, recicleextraBitmapantes de criar o próximo adicionando este código logo após a chamada parasuper.
if (::extraBitmap.isInitialized) extraBitmap.recycle()Etapa 2. Substituir o onDraw()
Todo o trabalho de exibição para MyCanvasView acontece em onDraw().
Para começar, mostre a tela, preenchendo-a com a cor de fundo definida em onSizeChanged().
- Substitua
onDraw()e desenhe o conteúdo doextraBitmapem cache na tela associada à visualização. O métododrawBitmap()Canvastem várias versões. Nesse código, você fornece o bitmap, as coordenadas x e y (em pixels) do canto superior esquerdo enullpara oPaint, que será definido mais tarde.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
A tela transmitida para onDraw() e usada pelo sistema para mostrar o bitmap é diferente da que você criou no método onSizeChanged() e usou para desenhar no bitmap.
- Execute o app. A tela inteira vai ficar preenchida com a cor de plano de fundo especificada.

Para desenhar, você precisa de um objeto Paint que especifique como as coisas são estilizadas ao serem desenhadas e um Path que especifique o que está sendo desenhado.
Etapa 1. Inicializar um objeto Paint
- Em MyCanvasView.kt, no nível superior do arquivo, 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áveldrawColorpara armazenar a cor a ser usada e inicialize-a com o recursocolorPaintdefinido anteriormente.
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)- No nível da classe, abaixo, adicione uma variável
paintpara um objetoPainte inicialize-o 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
colordopainté odrawColorque você definiu anteriormente. isAntiAliasdefine se o suavização de borda será aplicada. DefinirisAntiAliascomotruesuaviza as bordas do que é desenhado sem afetar a forma.isDither, quandotrue, afeta a forma como as cores com maior precisão do que o dispositivo são subamostradas. Por exemplo, o dithering é a maneira mais comum de reduzir a faixa de cores das imagens para 256 (ou menos) cores.styledefine o tipo de pintura a ser feita em um traço, que é essencialmente uma linha.Paint.Styleespecifica se a primitiva desenhada é preenchida, traçada ou ambas (na mesma cor). O padrão é preencher o objeto a que a tinta é aplicada. "Preenchimento" colore a parte interna da forma, enquanto "traço" segue o contorno dela.- O
strokeJoindePaint.Joinespecifica como as linhas e os segmentos de curva se unem em um caminho traçado. O padrão éMITER. strokeCapdefine a forma da extremidade da linha como um limite.Paint.Capespecifica como o início e o fim das linhas e caminhos traçados. O padrão éBUTT.strokeWidthespecifica a largura do traço em pixels. O padrão é uma largura muito fina, então ela é definida como a constanteSTROKE_WIDTHque você definiu anteriormente.
Etapa 2. Inicializar um objeto Path
O Path é o caminho do que o usuário está desenhando.
- Em
MyCanvasView, adicione uma variávelpathe inicialize-a com um objetoPathpara armazenar o caminho que está sendo desenhado ao seguir o toque do usuário na tela. Importeandroid.graphics.Pathpara oPath.
private var path = Path()Etapa 1. Responder ao movimento na tela
O método onTouchEvent() em uma visualização é chamado sempre que o usuário toca na tela.
- Em
MyCanvasView, substitua o métodoonTouchEvent()para armazenar em cache as coordenadasxeydoeventtransmitido. Em seguida, use uma expressãowhenpara processar eventos de movimento ao tocar na tela, mover na tela e soltar o toque na tela. 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 classeMotionEventpara 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
motionTouchEventXemotionTouchEventYausentes para armazenar em cache as coordenadas x e y do evento de toque atual (as coordenadasMotionEvent). Inicialize-os como0f.
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 vai ser criado e executado, mas você ainda não vai ver nada diferente do plano de fundo colorido.
Etapa 2. Implementar touchStart()
Esse método é chamado quando o usuário toca na tela pela primeira vez.
- No nível da classe, adicione variáveis para armazenar em cache os valores x e y mais recentes. Depois que o usuário para de se mover e tira o dedo da tela, esses são os pontos 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, mova para as coordenadas x-y do evento de toque (motionTouchEventXemotionTouchEventY) e atribuacurrentXecurrentYa 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
touchTolerancee defina comoViewConfiguration.get(context).scaledTouchSlop.
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlopAo usar um caminho, não é necessário desenhar todos os pixels e solicitar uma atualização da tela a cada vez. Em vez disso, você pode (e vai) interpolar um caminho entre pontos para ter uma performance muito melhor.
- Se o dedo mal se moveu, não é necessário desenhar.
- Se o dedo tiver se movido menos que a distância
touchTolerance, não desenhe. - O
scaledTouchSlopretorna 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 a contagem decurrentXecurrentYe desenhepath. Em seguida, chameinvalidate()para forçar a atualização da tela com opathatualizado.
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()
}Mais detalhes sobre esse método:
- Calcule a distância percorrida (
dx, dy). - Se o movimento for maior que a tolerância ao toque, adicione um segmento ao caminho.
- Defina o ponto inicial do próximo segmento como o endpoint do segmento atual.
- Usar
quadTo()em vez delineTo()cria uma linha suave sem cantos. Consulte Curvas de Bézier. - Chame
invalidate()para (eventualmente chamaronDraw()e) redesenhar a visualização.
Etapa 4: implementar touchUp()
Quando o usuário levanta o dedo, basta redefinir o caminho para que ele não seja desenhado novamente. Nada é desenhado, então não é necessário invalidar.
- 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 do desenho não é salvo. Para este app de exemplo, isso é proposital, para dar ao usuário uma maneira simples de limpar a tela.

Etapa 5: desenhar um frame ao redor do esboço
À medida que o usuário desenha na tela, o app cria o caminho e o salva no bitmap extraBitmap. O método onDraw() mostra o bitmap extra na tela da visualização. Você pode fazer mais desenhos no onDraw(). Por exemplo, você pode desenhar formas depois de desenhar o bitmap.
Nesta etapa, você vai desenhar uma moldura ao redor da borda da imagem.
- Em
MyCanvasView, adicione uma variável chamadaframeque contenha um objetoRect.
private lateinit var frame: Rect- No final de
onSizeChanged(), defina um encarte e adicione código para criar oRectque será usado 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 e 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 seja uma boa solução, não é a única maneira possível de armazenar informações de desenho. A forma de armazenar o histórico de desenhos depende do app e dos seus requisitos. Por exemplo, se você estiver desenhando formas, poderá salvar uma lista com a localização e as dimensões delas. No app MiniPaint, você pode salvar o caminho como um Path. Confira abaixo o processo geral para fazer isso, se quiser tentar.
- Em
MyCanvasView, remova todo o código deextraCanvaseextraBitmap. - Adicione variáveis para o caminho até agora e o caminho que está sendo desenhado no momento.
// 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. Não deve haver nenhuma diferença.
Faça o download do código do codelab concluído.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-canvas
Se preferir, você pode fazer o download do repositório como um arquivo ZIP, descompactar e abrir no Android Studio.
- Um
Canvasé uma superfície de desenho 2D que oferece métodos para desenhar. - O
Canvaspode ser associado a uma instânciaViewque o mostra. - O objeto
Paintcontém as informações de estilo e cor sobre como desenhar geometrias (como linhas, retângulos, ovais e caminhos) e texto. - Um padrão comum para trabalhar com uma tela é criar uma visualização personalizada e substituir os métodos
onDraw()eonSizeChanged(). - Substitua o método
onTouchEvent()para capturar os toques do usuário e responder a eles desenhando coisas. - Você pode usar um bitmap extra para armazenar em cache informações de desenhos que mudam com o tempo. Como alternativa, você pode armazenar formas ou um caminho.
Curso da Udacity:
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 Bézier
- Tela e elementos gráficos
- Série de artigos sobre arquitetura de gráficos (avançado)
- drawables
- onDraw()
- onSizeChanged()
MotionEventViewConfiguration.get(context).scaledTouchSlop
Esta seção lista as possíveis atividades de dever de casa para os alunos que estão fazendo este codelab como parte de um curso ministrado por um professor. Cabe ao professor fazer o seguinte:
- Atribuir o dever de casa, se necessário.
- Informar aos alunos como enviar deveres de casa.
- Atribuir nota aos deveres de casa.
Os professores podem usar essas sugestões o quanto quiserem, podendo passar os exercícios que acharem mais apropriados como dever de casa.
Se você estiver seguindo este codelab por conta própria, sinta-se à vontade para usar esses deveres de casa para testar seu conhecimento.
Responda estas perguntas
Pergunta 1
Quais dos componentes a seguir 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 ele precisa redesenhar a tela.
Pergunta 3
Qual é a função dos objetos Canvas, Bitmap e Paint?
▢ Superfície de desenho 2D, bitmap mostrado na tela, informações de estilo para desenho.
▢ Superfície de desenho 3D, bitmap para armazenar em cache o caminho, informações de estilo para desenho.
▢ Superfície de desenho 2D, bitmap exibido na tela, estilização para a visualização.
▢ Cache para informações de desenho, bitmap para desenhar e informações de estilo para desenho.
Para acessar links de outros codelabs deste curso, consulte a página inicial dos codelabs do curso Android avançado no Kotlin.