Desenhar em objetos na tela

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

  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ê criará uma visualização personalizada que serve como a visualização de conteúdo para seu app (MyCanvasView).
  2. Essa visualização, como todas as visualizações, vem com uma tela própria (canvas).
  3. A forma mais básica de desenhar na tela de uma visualização é modificar o método onDraw() e desenhar nela.
  4. 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.
  5. 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.
  6. Em seguida, desenhe na tela de armazenamento em cache (extraCanvas), que é exibida no bitmap de armazenamento em cache (extraBitmap).
  7. 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

  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 pai do estilo AppTheme fornecido, 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ê criará uma visualização personalizada, MyCanvasView, para desenhar.

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

  1. 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>
  1. Abrir MainActivity.kt
  2. No onCreate(), exclua o 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 isso, defina a sinalização SYSTEM_UI_FLAG_FULLSCREEN na myCanvasView. Dessa forma, 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 dela, defina a visualização do conteúdo como myCanvasView.
setContentView(myCanvasView)
  1. 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.

  1. Em MyCanvasView, no nível da classe, defina variáveis para uma tela e um bitmap. Chame-os como extraCanvas e extraBitmap. 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
  1. Defina uma variável de nível de classe backgroundColor para a cor do plano de fundo da tela e inicialize-a como o colorBackground definido anteriormente.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
  1. No MyCanvasView, substitua o método onSizeChanged(). 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)
}
  1. Dentro de onSizeChanged(), crie uma instância da Bitmap 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)
  1. Crie uma instância Canvas a partir de extraBitmap e atribua-a a extraCanvas.
 extraCanvas = Canvas(extraBitmap)
  1. Especifique a cor do plano de fundo para preencher extraCanvas.
extraCanvas.drawColor(backgroundColor)
  1. 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, recicle extraBitmap antes de criar a próxima adicionando esse código logo após a chamada para super
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().

  1. Substitua onDraw() e desenhe o conteúdo da extraBitmap armazenada em cache na tela associada à visualização. O método drawBitmap() 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 e null para o Paint, 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.

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

  1. 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
  1. No nível da classe MyCanvasView, defina uma variável drawColor para manter a cor usada para desenhar 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-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 do paint é o drawColor definido anteriormente.
  • isAntiAlias define se a 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 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 de Paint.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 constante STROKE_WIDTH definida anteriormente.

Etapa 2: Inicializar um objeto de Caminho

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

  1. Em MyCanvasView, adicione uma variável path e a inicialize 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 a movimentos na tela

O método onTouchEvent() em uma visualização é chamado sempre que o usuário toca na tela.

  1. No MyCanvasView, substitua o método onTouchEvent() para armazenar em cache as coordenadas x e y do transmitido em event. Em seguida, use uma expressão when 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 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 em 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 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.

  1. 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
  1. Implemente o método touchStart() da seguinte maneira. Redefina o path, vá 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-a como ViewConfiguration.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.
  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 o cálculo de currentX e currentY em execução e desenhe o path. Em seguida, chame invalidate() para forçar o novo desenho 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()
}

Esse método é mais detalhado:

  1. Calcule a distância que foi movida (dx, dy).
  2. Se o movimento era além da tolerância ao toque, adicione um segmento ao caminho.
  3. Defina o ponto de partida do próximo segmento como o endpoint desse segmento.
  4. Usar quadTo() em vez de lineTo() para criar uma linha desenhada de forma suave sem cantos. Consulte Curvas do Bezier.
  5. Chame invalidate() para (depois chamar onDraw() 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.

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

  1. No MyCanvasView, adicione uma variável com o nome frame que contenha um objeto Rect.
private lateinit var frame: Rect
  1. No final do método onSizeChanged(), insira um encarte e adicione um código para criar a Rect 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)
  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. 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.

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

Fazer o download do ZIP

  • Uma Canvas é uma superfície de desenho 2D que fornece métodos para desenhar.
  • O Canvas pode ser associado a uma instância do View 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() e onSizeChanged().
  • 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:

Documentação do desenvolvedor Android:

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.