Como desenhar em objetos na tela

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 objeto Paint conté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.

  1. 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).
  2. Essa visualização, como todas as outras, vem com uma tela própria (canvas).
  3. Para a maneira mais básica de desenhar na tela de uma visualização, substitua o método onDraw() e desenhe na tela.
  4. 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.
  5. 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.
  6. Em seguida, desenhe no canvas de cache (extraCanvas), que desenha no bitmap de cache (extraBitmap).
  7. 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 Canvas e 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

  1. Crie um novo projeto Kotlin chamado MiniPaint que use o modelo Empty Activity.
  2. 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>
  1. Abrir styles.xml
  2. No elemento pai do estilo AppTheme, substitua DarkActionBar por NoActionBar. 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.

  1. No pacote app/java/com.example.android.minipaint, crie um New > Kotlin File/Class chamado MyCanvasView.
  2. Faça a classe MyCanvasView estender a classe View e transmita o context: 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.

  1. Abra strings.xml e 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>
  1. Abrir MainActivity.kt
  2. Em onCreate(), exclua setContentView(R.layout.activity_main).
  3. Crie uma instância de MyCanvasView.
val myCanvasView = MyCanvasView(this)
  1. Abaixo disso, solicite a tela cheia para o layout de myCanvasView. Para fazer isso, defina a flag SYSTEM_UI_FLAG_FULLSCREEN em myCanvasView. Assim, a visualização preenche completamente a tela.
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN
  1. Adicione uma descrição do conteúdo.
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)
  1. Abaixo disso, defina a visualização de conteúdo como myCanvasView.
setContentView(myCanvasView)
  1. 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.

  1. Em MyCanvasView, no nível da classe, defina variáveis para uma tela e um bitmap. Chame-os de extraCanvas e extraBitmap. 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
  1. Defina uma variável de nível de classe backgroundColor para a cor de fundo da tela e inicialize-a com o colorBackground que você definiu anteriormente.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
  1. Em MyCanvasView, substitua o método onSizeChanged(). 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)
}
  1. Em onSizeChanged(), crie uma instância de Bitmap com a nova largura e altura, que são o tamanho da tela, e atribua a extraBitmap. O terceiro argumento é a configuração de cor do bitmap. O ARGB_8888 armazena cada cor em 4 bytes e é recomendado.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
  1. Crie uma instância Canvas de extraBitmap e atribua a extraCanvas.
 extraCanvas = Canvas(extraBitmap)
  1. Especifique a cor de plano de fundo em que extraCanvas será preenchido.
extraCanvas.drawColor(backgroundColor)
  1. 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, recicle extraBitmap antes de criar o próximo adicionando este código logo após a chamada para super.
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().

  1. Substitua onDraw() e desenhe o conteúdo do extraBitmap em cache na tela associada à visualização. O método drawBitmap() Canvas tem várias versões. Nesse código, você fornece o bitmap, as coordenadas x e y (em pixels) do canto superior esquerdo e null para o Paint, 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.

  1. 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

  1. 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
  1. No nível da classe MyCanvasView, defina uma variável drawColor para armazenar a cor a ser usada e inicialize-a com o recurso colorPaint definido anteriormente.
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)
  1. No nível da classe, abaixo, adicione uma variável paint para um objeto Paint e 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 color do paint é o drawColor que você definiu anteriormente.
  • isAntiAlias define se o suavização de borda será aplicada. Definir isAntiAlias como true suaviza as bordas do que é desenhado sem afetar a forma.
  • isDither, quando true, 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.
  • style define o tipo de pintura a ser feita em um traço, que é essencialmente uma linha. Paint.Style especifica 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 strokeJoin de Paint.Join especifica como as linhas e os segmentos de curva se unem em um caminho traçado. O padrão é MITER.
  • strokeCap define a forma da extremidade 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 é uma largura muito fina, então ela é definida como a constante STROKE_WIDTH que você definiu anteriormente.

Etapa 2. Inicializar um objeto Path

O Path é o caminho do que o usuário está desenhando.

  1. Em MyCanvasView, adicione uma variável path e inicialize-a com um objeto Path para armazenar o caminho que está sendo desenhado ao seguir o toque do usuário na tela. Importe android.graphics.Path para o Path.
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.

  1. Em MyCanvasView, substitua o método onTouchEvent() para armazenar em cache as coordenadas x e y do event transmitido. Em seguida, use uma expressão when para 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 classe MotionEvent 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
}
  1. No nível da classe, adicione as variáveis motionTouchEventX e motionTouchEventY ausentes para armazenar em cache as coordenadas x e y do evento de toque atual (as coordenadas MotionEvent). Inicialize-os como 0f.
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
  1. Crie stubs para as três funções touchStart(), touchMove() e touchUp().
private fun touchStart() {}

private fun touchMove() {}

private fun touchUp() {}
  1. 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.

  1. 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
  1. Implemente o método touchStart() da seguinte maneira. Redefina o path, mova para as coordenadas x-y do evento de toque (motionTouchEventX e motionTouchEventY) e atribua currentX e currentY a esse valor.
private fun touchStart() {
   path.reset()
   path.moveTo(motionTouchEventX, motionTouchEventY)
   currentX = motionTouchEventX
   currentY = motionTouchEventY
}

Etapa 3. Implementar touchMove()

  1. No nível da classe, adicione uma variável touchTolerance e defina como ViewConfiguration.get(context).scaledTouchSlop.
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop

Ao 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 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.
  1. Defina o método touchMove(). Calcule a distância percorrida (dx, dy), crie uma curva entre os dois pontos e armazene-a em path, atualize a contagem de currentX e currentY e desenhe path. Em seguida, chame invalidate() para forçar a atualização da tela com o path 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()
}

Mais detalhes sobre esse método:

  1. Calcule a distância percorrida (dx, dy).
  2. Se o movimento for maior que a tolerância ao toque, adicione um segmento ao caminho.
  3. Defina o ponto inicial do próximo segmento como o endpoint do segmento atual.
  4. Usar quadTo() em vez de lineTo() cria uma linha suave sem cantos. Consulte Curvas de Bézier.
  5. Chame invalidate() para (eventualmente chamar onDraw() 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.

  1. Implemente o método touchUp().
private fun touchUp() {
   // Reset the path so it doesn't get drawn again.
   path.reset()
}
  1. 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.

  1. Em MyCanvasView, adicione uma variável chamada frame que contenha um objeto Rect.
private lateinit var frame: Rect
  1. No final de onSizeChanged(), defina um encarte e adicione código para criar o Rect que 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)
  1. Em onDraw(), depois de desenhar o bitmap, desenhe um retângulo.
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)
  1. 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.

  1. Em MyCanvasView, remova todo o código de extraCanvas e extraBitmap.
  2. 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()
  1. 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)
  1. 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()
  1. 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.

Fazer o download do ZIP

  • Um Canvas é uma superfície de desenho 2D que oferece métodos para desenhar.
  • O Canvas pode ser associado a uma instância View que o mostra.
  • O objeto Paint conté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() e onSizeChanged().
  • 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:

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.