Este tutorial faz parte do curso Android avançado em Kotlin. Você obterá o máximo valor deste curso se trabalhar com os tutoriais em sequência, mas isso não é obrigatório. Todos os tutoriais do curso estão listados na página de destino Android avançado em Kotlin.

Introdução

No Android, você tem várias técnicas disponíveis para implementar gráficos 2D personalizados e animações em vistas.

Além de usar drawables, você pode criar desenhos 2D usando os métodos de desenho da classe Canvas. O Canvas é uma superfície de desenho 2D que fornece métodos de desenho. Isso é útil quando seu aplicativo precisa se redesenhar regularmente, porque o que o usuário vê muda com o tempo. Neste tutorial, você aprende como criar e desenhar em uma tela que é exibida em uma View.

Os tipos de operações que você pode realizar em uma tela incluem:

Como você pode pensar no desenho 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. Como o Android desenha é um tópico fascinante sobre o qual muito já foi escrito e seus detalhes estão além do escopo deste tutorial.

No contexto deste tutorial e de seu aplicativo que desenha em uma tela para vista em tela inteira, você pode pensar nisso da seguinte maneira.

  1. Você precisa de uma vista para exibir o que está desenhando. Essa pode ser uma das vistas fornecidas pelo sistema Android. Ou, neste tutorial, você cria uma vista personalizada que serve como vista do conteúdo para seu aplicativo (MyCanvasView).
  2. Esta vista, como todas as vistas, vem com sua própria tela (canvas).
  3. Para obter a maneira mais básica de desenhar na tela de uma vista, você sobrescreve seu método onDraw() e desenha em sua tela.
  4. Ao construir o desenho, você precisa armazenar em cache o que desenhou antes. Existem várias maneiras de armazenar seus 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 em seu bitmap de cache (extraBitmap) usando a API de desenho de tela, você cria uma tela de cache (extraCanvas) para seu bitmap de cache.
  6. Você então desenha em sua tela de cache (extraCanvas), que desenha em seu bitmap de cache (extraBitmap).
  7. Para exibir tudo desenhado na tela, você diz ao canvas da vista (canvas) para desenhar o bitmap de cache (extraBitmap).

O que você já deveria saber

O que você aprenderá

O que você vai fazer

O aplicativo MiniPaint usa uma vista personalizada para exibir uma linha em resposta aos toques do usuário, conforme mostrado na captura de tela abaixo.

Etapa 1. Crie o projeto MiniPaint

  1. Crie um novo projeto Kotlin chamado MiniPaint que usa 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. Abra styles.xml
  2. No pai do estilo AppTheme fornecido, substitua DarkActionBar por NoActionBar. Isso remove a barra de ação, para que você possa desenhar em tela cheia.
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">

Etapa 2. Crie a classe MyCanvasView

Nesta etapa, você cria uma vista personalizada, MyCanvasView, para desenhar.

  1. No pacote app/java/com.example.android.minipaint, crie um New > Kotlin File/Class chamado MyCanvasView.
  2. Faça com que a classe MyCanvasView estenda a classe View e passe no contexto context: Context. Aceite as importações sugeridas.
import android.content.Context
import android.view.View

class MyCanvasView(context: Context) : View(context) {
}

Etapa 3. Defina MyCanvasView como a vista do conteúdo

Para exibir o que você desenhará em MyCanvasView, você deve defini-lo como a vista do conteúdo da MainActivity.

  1. Abra strings.xml e defina uma string a ser usada para a descrição do conteúdo da vista.
<string name="canvasContentDescription">Mini Paint is a simple line drawing app.
   Drag your fingers to draw. Rotate the phone to clear.</string>
  1. Abra 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 inteira para o layout de myCanvasView. Faça isso definindo o sinalizador SYSTEM_UI_FLAG_FULLSCREEN em myCanvasView. Dessa forma, a vista 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 vista do conteúdo para myCanvasView.
setContentView(myCanvasView)
  1. Execute seu aplicativo. 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 vista muda de tamanho. Como a vista começa sem tamanho, o método onSizeChanged() da vista também é chamado depois que a Activity primeiro a cria e aumenta. Este método onSizeChanged() é, portanto, o local ideal para criar e configurar a tela da vista.

  1. Em MyCanvasView, no nível de classe, defina variáveis ​​para uma tela e um bitmap. Chame-os de extraCanvas e extraBitmap. Estes 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 definido anteriormente.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
  1. Em MyCanvasView, substitua o método onSizeChanged(). Este método de retorno de chamada é 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. Dentro de onSizeChanged(), crie uma instância de Bitmap com a nova largura e altura, que são o tamanho da tela, e atribua-a a extraBitmap. O terceiro argumento é a configuração de cores de bitmap. ARGB_8888 armazena cada cor em 4 bytes e é recomendado.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
  1. Crie uma instância de Canvas de extraBitmap e atribua-a a extraCanvas.
 extraCanvas = Canvas(extraBitmap)
  1. Especifique a cor de fundo na qual preencher extraCanvas.
extraCanvas.drawColor(backgroundColor)
  1. Olhando para onSizeChanged(), um novo bitmap e tela são criados toda vez que a função é executada. Você precisa de um novo bitmap, porque o tamanho foi alterado. No entanto, esse é um vazamento de memória, deixando os bitmaps antigos por aí. 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 onDraw()

Todo o trabalho de desenho para MyCanvasView acontece em onDraw().

Para começar, exiba a tela, preenchendo-a com a cor de fundo que você definiu em onSizeChanged().

  1. Substitua onDraw() e desenhe o conteúdo do extraBitmap armazenado em cache na tela associada à vista. O método drawBitmap() Canvas vem em várias versões. Neste código, você fornece o bitmap, as coordenadas x e y (em pixels) do canto superior esquerdo e null para o Paint, conforme você definirá isso mais tarde.
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}


Observe que a tela que é passada para onDraw() e usada pelo sistema para exibir o bitmap é diferente daquela que você criou no método onSizeChanged() e usado por você para desenhar no bitmap.

  1. Execute seu aplicativo. Você deve ver a tela inteira preenchida com a cor de fundo especificada.

Para desenhar, você precisa de um objeto Paint que especifica como as coisas são estilizadas quando desenhadas, e um Path que especifica o que está sendo desenhado.

Etapa 1. Inicializar um objeto do 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 de classe de MyCanvasView, defina uma variável drawColor para manter a cor com a qual desenhar e inicialize-a com o recurso colorPaint que você definiu 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)
}

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. Responda ao movimento na tela

O método onTouchEvent() em uma vista é chamado sempre que o usuário toca a tela.

  1. Em MyCanvasView, substitua o método onTouchEvent() para armazenar em cache as coordenadas x e y do passado em event. Em seguida, use uma expressão when para manipular eventos de movimento para tocar para baixo na tela, mover na tela e liberar 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 de utilitário, conforme mostrado no código a seguir. Consulte a documentação da classe MotionEvent para 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 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 esboços para as três funções touchStart(), touchMove() e touchUp().
private fun touchStart() {}

private fun touchMove() {}

private fun touchUp() {}
  1. Seu código deve ser compilado e executado, mas você ainda não verá nada diferente do fundo colorido.

Etapa 2. Implementar touchStart()

Este método é chamado quando o usuário toca a 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 levanta o toque, esses são o ponto de partida para o próximo caminho (o próximo segmento da linha a ser traçado).
private var currentX = 0f
private var currentY = 0f
  1. Implemente o método touchStart() da seguinte maneira. Redefina o path, mova para as coordenadas xy do evento de toque (motionTouchEventX e motionTouchEventY) e atribua currentX e currentY para 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 há necessidade de desenhar cada pixel e sempre solicitar uma atualização da tela. Em vez disso, você pode (e irá) interpolar um caminho entre os pontos para um desempenho muito melhor.

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

Este método em mais detalhes:

  1. Calcule a distância que foi movida (dx, dy).
  2. Se o movimento foi além da tolerância ao toque, adicione um segmento ao caminho.
  3. Defina o ponto de partida para o próximo segmento até o ponto final deste segmento.
  4. Usando quadTo() em vez de lineTo(), crie uma linha desenhada suavemente sem cantos. Veja Curvas de Bézier.
  5. Chame invalidate() para (eventualmente chamar onDraw() e) redesenhar a vista.

Etapa 4: Implementar o touchUp()

Quando o usuário levanta o toque, tudo o que é necessário é redefinir o caminho para que não seja desenhado novamente. Nada é desenhado, então 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 seu código e use o dedo para desenhar na tela. Observe que se você girar o dispositivo, a tela será limpa, pois o estado do desenho não será salvo. Para este aplicativo de amostra, isso ocorre por design, para fornecer ao usuário uma maneira simples de limpar a tela.

Etapa 5: desenhe uma moldura ao redor do esboço

Conforme o usuário desenha na tela, seu aplicativo constrói o caminho e o salva no bitmap extraBitmap. O método onDraw() exibe o bitmap extra na tela da vista. Você pode fazer mais desenhos em onDraw(). Por exemplo, você pode desenhar formas depois de desenhar o bitmap.

Nesta etapa, você desenha um quadro ao redor da borda da imagem.

  1. Em MyCanvasView, adicione uma variável chamada frame que contém um objeto Rect.
private lateinit var frame: Rect
  1. No final de onSizeChanged() defina um inset e adicione o código para criar o Rect que será usado para o quadro, usando as novas dimensões e o inset.
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)
  1. Em onDraw(), após desenhar o bitmap, desenhe um retângulo.
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)
  1. Execute seu aplicativo. Observe a moldura.

Tarefa (opcional): Armazenamento de dados em um caminho

No aplicativo atual, as informações do 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. Como você armazena seu histórico de desenho depende do aplicativo e de seus vários requisitos. Por exemplo, se você estiver desenhando formas, poderá salvar uma lista de formas com sua localização e dimensões. Para o aplicativo MiniPaint, você pode salvar o caminho como um Path. Abaixo está o esboço geral de como fazer isso, se você 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 atualmente.
// 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 seu aplicativo e, sim, não deve haver nenhuma diferença.

Baixe o código para o tutorial concluído.

$ git clone https://github.com/googletutoriais/android-kotlin-drawing-canvas


Alternativamente, você pode baixar o repositório como um arquivo Zip, descompactá-lo e abri-lo no Android Studio.

Baixe o Zip

Documentação do desenvolvedor Android:

Se você estiver trabalhando neste tutorial por conta própria, sinta-se à vontade para usar essas tarefas de lição de casa para testar seus conhecimentos

Responda a essas perguntas

Pergunta 1

Quais dos seguintes componentes são necessários para trabalhar com um Canvas? Selecione tudo que se aplica.

Bitmap

Paint

Path

View

Pergunta 2

O que uma chamada para invalidate() faz (em termos gerais)?

▢ Invalida e reinicia seu aplicativo.

▢ Apaga o desenho do bitmap.

▢ Indica que o código anterior não deve ser executado.

▢ Informa ao sistema que ele deve redesenhar a tela.

Pergunta 3

Qual é a função dos objetos Canvas, Bitmap e Paint?

▢ Superfície de desenho 2D, bitmap exibido 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, estilo da vista.

▢ Cache para informações de desenho, bitmap para desenhar, informações de estilo para desenho.

Para obter links para outros tutoriais neste curso, consulte a página inicial de tutoriais do Android avançado em Kotlin.