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.
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:
Paint
. O objeto Paint
contém as informações de estilo e cor sobre como desenhar geometrias (como linha, retângulo, oval e caminhos) ou, por exemplo, a fonte do texto. 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.
MyCanvasView
).canvas
).onDraw()
e desenha em sua tela. extraBitmap
). Outra é salvar um histórico do que você desenhou como coordenadas e instruções.extraBitmap
) usando a API de desenho de tela, você cria uma tela de cache (extraCanvas
) para seu bitmap de cache. extraCanvas
), que desenha em seu bitmap de cache (extraBitmap
). canvas
) para desenhar o bitmap de cache (extraBitmap
).Canvas
e desenhar nele em resposta ao toque do usuário.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.
app/res/values/colors.xml
e adicione as duas cores a seguir.<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>
styles.xml
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">
Nesta etapa, você cria uma vista personalizada, MyCanvasView
, para desenhar.
app/java/com.example.android.minipaint
, crie um New > Kotlin File/Class chamado MyCanvasView
.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) {
}
Para exibir o que você desenhará em MyCanvasView
, você deve defini-lo como a vista do conteúdo da MainActivity
.
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>
MainActivity.kt
onCreate()
, exclua setContentView(R.layout.activity_main)
.MyCanvasView
. val myCanvasView = MyCanvasView(this)
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
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)
myCanvasView
. setContentView(myCanvasView)
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.
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
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)
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)
}
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)
Canvas
de extraBitmap
e atribua-a a extraCanvas
. extraCanvas = Canvas(extraBitmap)
extraCanvas
. extraCanvas.drawColor(backgroundColor)
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()
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()
.
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.
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.
private const val STROKE_WIDTH = 12f // has to be float
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)
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)
}
color
da paint
é a drawColor
que você definiu anteriormente. isAntiAlias
define se deve ser aplicada suavização de aresta. Definir isAntiAlias
como true
, suaviza as bordas do que é desenhado sem afetar a forma.isDither
, quando true
, afeta como as cores com maior precisão do que o dispositivo são amostradas. Por exemplo, o pontilhamento é o meio mais comum de reduzir o intervalo de cores das imagens para 256 (ou menos) cores.style
define o tipo de pintura a ser feito para um traço, que é essencialmente uma linha. Paint.Style
especifica se a primitiva que está sendo desenhada é preenchida, traçada ou ambos (da mesma cor). O padrão é preencher o objeto ao qual a tinta é aplicada. ("Preencher" colore o interior da forma, enquanto "traço" segue seu contorno.)strokeJoin
de Paint.Join
especifica como linhas e segmentos de curva se unem em um caminho traçado. O padrão é MITER
.strokeCap
define a forma do final da linha como um limite. Paint.Cap
especifica como o início e o fim das linhas traçadas e caminhos. O padrão é BUTT
.strokeWidth
especifica a largura do traço em pixels. O padrão é a largura da linha fina, que é realmente fina, então é definida para a constante STROKE_WIDTH
que você definiu anteriormente.O Path
é o caminho do que o usuário está desenhando.
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()
O método onTouchEvent()
em uma vista é chamado sempre que o usuário toca a tela.
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
}
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
touchStart()
, touchMove()
e touchUp()
.private fun touchStart() {}
private fun touchMove() {}
private fun touchUp() {}
Este método é chamado quando o usuário toca a tela pela primeira vez.
private var currentX = 0f
private var currentY = 0f
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
}
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.
touchTolerance
, não desenhe.scaledTouchSlop
retorna a distância em pixels que um toque pode vagar antes que o sistema pense que o usuário está rolando.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:
dx, dy
).quadTo()
em vez de lineTo()
, crie uma linha desenhada suavemente sem cantos. Veja Curvas de Bézier.invalidate()
para (eventualmente chamar onDraw()
e) redesenhar a vista. 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.
touchUp()
. private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}
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.
MyCanvasView
, adicione uma variável chamada frame
que contém um objeto Rect
.private lateinit var frame: Rect
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)
onDraw()
, após desenhar o bitmap, desenhe um retângulo.// Draw a frame around the canvas.
canvas.drawRect(frame, paint)
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.
MyCanvasView
, remova todo o código de extraCanvas
e extraBitmap
.
// Path representing the drawing so far
private val drawing = Path()
// Path representing what's currently being drawn
private val curPath = Path()
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)
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()
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.
Canvas
é uma superfície de desenho 2D que fornece métodos de desenho. Canvas
pode ser associado a uma instância View
que o exibe. Paint
contém as informações de estilo e cor sobre como desenhar geometrias (como linha, retângulo, oval e caminhos) e texto. onDraw()
e onSizeChanged()
. onTouchEvent()
para capturar os toques do usuário e responder a eles desenhando coisas.
Documentação do desenvolvedor Android:
Canvas
Bitmap
View
classePaint
Bitmap.config
Path
MotionEvent
ViewConfiguration.get(context).scaledTouchSlop
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
Quais dos seguintes componentes são necessários para trabalhar com um Canvas
? Selecione tudo que se aplica.
▢ Bitmap
▢ Paint
▢ Path
▢ View
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.
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.