Este codelab es parte del curso Aspectos avanzados de Android en Kotlin. Aprovecharás al máximo este curso si trabajas con los codelabs de forma secuencial, aunque no es obligatorio. Todos los codelabs del curso se indican en la página de destino de los codelabs de Aspectos avanzados de Android en Kotlin.
Introducción
En Android, tienes varias técnicas disponibles para implementar gráficos y animaciones 2D personalizados en las vistas.
Además de usar recursos de diseño, puedes crear dibujos en 2D con los métodos de dibujo de la clase Canvas. El objeto Canvas es una superficie de dibujo 2D que proporciona métodos para dibujar. Esto es útil cuando tu app necesita volver a dibujarse con regularidad, ya que lo que ve el usuario cambia con el tiempo. En este codelab, aprenderás a crear un lienzo y a dibujar en él, que se mostrará en un View.
Entre los tipos de operaciones que puedes realizar en un lienzo, se incluyen las siguientes:
- Rellena todo el lienzo con color.
- Dibuja formas, como rectángulos, arcos y rutas con el estilo definido en un objeto
Paint. El objetoPaintcontiene la información de estilo y color sobre cómo dibujar geometrías (como líneas, rectángulos, óvalos y rutas) o, por ejemplo, el tipo de letra del texto. - Aplicar transformaciones, como traslación, ajuste o transformaciones personalizadas
- Recortar, es decir, aplicar una forma o una ruta al lienzo para definir sus partes visibles.

Cómo puedes pensar en el dibujo de Android (¡de forma muy simplificada!)
El dibujo en Android o en cualquier otro sistema moderno es un proceso complejo que incluye capas de abstracciones y optimizaciones hasta el hardware. La forma en que Android dibuja es un tema fascinante sobre el que se ha escrito mucho, y sus detalles están fuera del alcance de este codelab.
En el contexto de este codelab y su app que dibuja en un lienzo para mostrarlo en una vista de pantalla completa, puedes pensarlo de la siguiente manera.

- Necesitas una vista para mostrar lo que dibujas. Podría ser una de las vistas que proporciona el sistema Android. O bien, en este codelab, crearás una vista personalizada que servirá como vista de contenido para tu app (
MyCanvasView). - Esta vista, como todas las vistas, incluye su propio lienzo (
canvas). - Para dibujar en el lienzo de una vista de la manera más básica, anula su método
onDraw()y dibuja en su lienzo. - Cuando creas un dibujo, debes almacenar en caché lo que dibujaste antes. Existen varias formas de almacenar en caché tus datos. Una de ellas es en un mapa de bits (
extraBitmap). Otra es guardar un historial de lo que dibujaste como coordenadas e instrucciones. - Para dibujar en tu mapa de bits de almacenamiento en caché (
extraBitmap) con la API de dibujo de Canvas, crea un lienzo de almacenamiento en caché (extraCanvas) para tu mapa de bits de almacenamiento en caché. - Luego, dibujas en tu lienzo de almacenamiento en caché (
extraCanvas), que dibuja en tu mapa de bits de almacenamiento en caché (extraBitmap). - Para mostrar todo lo que se dibuja en la pantalla, le indicas al lienzo de la vista (
canvas) que dibuje el mapa de bits de almacenamiento en caché (extraBitmap).
Conocimientos que ya deberías tener
- Cómo crear una app con una actividad y un diseño básico, y ejecutarla con Android Studio
- Cómo asociar controladores de eventos con vistas
- Cómo crear una vista personalizada
Qué aprenderás
- Cómo crear un
Canvasy dibujar en él en respuesta al toque del usuario
Actividades
- Crear una app que dibuje líneas en la pantalla en respuesta a las acciones del usuario
- Captura eventos de movimiento y, en respuesta, dibuja líneas en un lienzo que se muestra en una vista personalizada de pantalla completa.
La app de MiniPaint usa una vista personalizada para mostrar una línea en respuesta a los toques del usuario, como se muestra en la siguiente captura de pantalla.

Paso 1: Crea el proyecto MiniPaint
- Crea un nuevo proyecto de Kotlin llamado MiniPaint que use la plantilla Empty Activity.
- Abre el archivo
app/res/values/colors.xmly agrega los siguientes dos colores.
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>- Abrir
styles.xml - En el elemento superior del estilo
AppThemedeterminado, reemplazaDarkActionBarporNoActionBar. Esto quita la barra de acciones para que puedas dibujar en pantalla completa.
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">Paso 2: Crea la clase MyCanvasView
En este paso, crearás una vista personalizada, MyCanvasView, para dibujar.
- En el paquete
app/java/com.example.android.minipaint, crea un New > Kotlin File/Class llamadoMyCanvasView. - Haz que la clase
MyCanvasViewextienda la claseViewy pasa elcontext: Context. Acepta las importaciones sugeridas.
import android.content.Context
import android.view.View
class MyCanvasView(context: Context) : View(context) {
}Paso 3: Cómo establecer MyCanvasView como la vista de contenido
Para mostrar lo que dibujarás en MyCanvasView, debes configurarlo como la vista de contenido de MainActivity.
- Abre
strings.xmly define una cadena para usarla en la descripción del contenido de la vista.
<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 - En
onCreate(), borrasetContentView(R.layout.activity_main). - Crea una instancia de
MyCanvasView.
val myCanvasView = MyCanvasView(this)- Debajo de eso, solicita la pantalla completa para el diseño de
myCanvasView. Para ello, establece la marcaSYSTEM_UI_FLAG_FULLSCREENenmyCanvasView. De esta manera, la vista ocupa toda la pantalla.
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN- Agrega una descripción del contenido.
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)- Debajo de eso, configura la vista de contenido en
myCanvasView.
setContentView(myCanvasView)- Ejecuta tu app. Verás una pantalla completamente blanca, ya que el lienzo no tiene tamaño y aún no dibujaste nada.
Paso 1: Anula onSizeChanged()
El sistema Android llama al método onSizeChanged() cada vez que cambia el tamaño de una vista. Dado que la vista comienza sin tamaño, también se llama al método onSizeChanged() de la vista después de que la actividad la crea y la infla por primera vez. Por lo tanto, este método onSizeChanged() es el lugar ideal para crear y configurar el lienzo de la vista.
- En
MyCanvasView, a nivel de la clase, define variables para un lienzo y un mapa de bits. LlamémoslosextraCanvasyextraBitmap. Estos son tu mapa de bits y tu lienzo para almacenar en caché lo que se dibujó antes.
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap- Define una variable de nivel de clase
backgroundColorpara el color de fondo del lienzo y, luego, inicialízala en elcolorBackgroundque definiste antes.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)- En
MyCanvasView, anula el métodoonSizeChanged(). El sistema Android llama a este método de devolución de llamada con las dimensiones de pantalla modificadas, es decir, con un nuevo ancho y alto (para cambiar a) y el ancho y alto anteriores (para cambiar desde).
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
super.onSizeChanged(width, height, oldWidth, oldHeight)
}- Dentro de
onSizeChanged(), crea una instancia deBitmapcon el nuevo ancho y alto, que son el tamaño de la pantalla, y asígnala aextraBitmap. El tercer argumento es la configuración de color del mapa de bits. Se recomiendaARGB_8888, ya que almacena cada color en 4 bytes.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)- Crea una instancia de
Canvasa partir deextraBitmapy asígnala aextraCanvas.
extraCanvas = Canvas(extraBitmap)- Especifica el color de fondo con el que se rellenará
extraCanvas.
extraCanvas.drawColor(backgroundColor)- Si observas
onSizeChanged(), se crea un nuevo mapa de bits y un nuevo lienzo cada vez que se ejecuta la función. Necesitas un mapa de bits nuevo porque cambió el tamaño. Sin embargo, esto es una pérdida de memoria, ya que los mapas de bits antiguos permanecen. Para corregir esto, reciclaextraBitmapantes de crear el siguiente agregando este código justo después de la llamada asuper.
if (::extraBitmap.isInitialized) extraBitmap.recycle()Paso 2: Cómo anular onDraw()
Todo el trabajo de dibujo para MyCanvasView se realiza en onDraw().
Para comenzar, muestra el lienzo y llena la pantalla con el color de fondo que estableciste en onSizeChanged().
- Anula
onDraw()y dibuja el contenido deextraBitmapalmacenado en caché en el lienzo asociado a la vista. El métododrawBitmap()Canvasviene en varias versiones. En este código, proporcionas el mapa de bits, las coordenadas X e Y (en píxeles) de la esquina superior izquierda ynullpara elPaint, ya que lo establecerás más adelante.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
Ten en cuenta que el lienzo que se pasa a onDraw() y que usa el sistema para mostrar el mapa de bits es diferente del que creaste en el método onSizeChanged() y que usaste para dibujar en el mapa de bits.
- Ejecuta la app. Deberías ver toda la pantalla con el color de fondo especificado.

Para dibujar, necesitas un objeto Paint que especifique cómo se diseñan los elementos cuando se dibujan y un Path que especifique qué se dibuja.
Paso 1: Inicializa un objeto Paint
- En MyCanvasView.kt, en el nivel superior del archivo, define una constante para el ancho del trazo.
private const val STROKE_WIDTH = 12f // has to be float- En el nivel de clase de
MyCanvasView, define una variabledrawColorpara contener el color con el que se dibujará y, luego, inicialízala con el recursocolorPaintque definiste antes.
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)- En el nivel de la clase, agrega una variable
paintpara un objetoPainty, luego, inicialízala de la siguiente manera.
// 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)
}- El
colordelpaintes eldrawColorque definiste antes. isAntiAliasdefine si se debe aplicar el suavizado de bordes. Si establecesisAntiAliasentrue, se suavizan los bordes de lo que se dibuja sin afectar la forma.isDither, cuando estrue, afecta la forma en que se reduce la resolución de los colores con mayor precisión que la del dispositivo. Por ejemplo, el tramado es el medio más común para reducir el rango de color de las imágenes a 256 colores (o menos).styleestablece el tipo de pintura que se aplicará a un trazo, que es esencialmente una línea.Paint.Styleespecifica si la primitiva que se dibuja se rellena, se traza o ambas (con el mismo color). El valor predeterminado es rellenar el objeto al que se aplica la pintura. (El "relleno" colorea el interior de la forma, mientras que el "trazo" sigue su contorno).strokeJoindePaint.Joinespecifica cómo se unen las líneas y los segmentos de curva en una ruta de acceso trazada. El valor predeterminado esMITER.strokeCapestablece la forma del final de la línea como un tope.Paint.Capespecifica cómo se ven el inicio y el final de las líneas y las rutas con trazo. El valor predeterminado esBUTT.strokeWidthespecifica el ancho del trazo en píxeles. El valor predeterminado es el ancho de línea fina, que es muy delgado, por lo que se establece en la constanteSTROKE_WIDTHque definiste antes.
Paso 2: Inicializa un objeto Path
El Path es la ruta de lo que dibuja el usuario.
- En
MyCanvasView, agrega una variablepathy, luego, inicialízala con un objetoPathpara almacenar la ruta que se dibuja cuando se sigue el toque del usuario en la pantalla. Importaandroid.graphics.PathparaPath.
private var path = Path()Paso 1: Cómo responder al movimiento en la pantalla
Se llama al método onTouchEvent() en una vista cada vez que el usuario toca la pantalla.
- En
MyCanvasView, anula el métodoonTouchEvent()para almacenar en caché las coordenadasxyydeleventpasado. Luego, usa una expresiónwhenpara controlar los eventos de movimiento cuando se toca la pantalla, se mueve el dedo por la pantalla y se suelta el toque en la pantalla. Estos son los eventos de interés para dibujar una línea en la pantalla. Para cada tipo de evento, llama a un método de utilidad, como se muestra en el siguiente código. Consulta la documentación de la claseMotionEventpara obtener una lista completa de los eventos táctiles.
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
}- A nivel de la clase, agrega las variables
motionTouchEventXymotionTouchEventYfaltantes para almacenar en caché las coordenadas X e Y del evento táctil actual (las coordenadasMotionEvent). Inicialízalos en0f.
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f- Crea stubs para las tres funciones
touchStart(),touchMove()ytouchUp().
private fun touchStart() {}
private fun touchMove() {}
private fun touchUp() {}- Tu código debería compilarse y ejecutarse, pero aún no verás nada diferente del fondo de color.
Paso 2: Implementa touchStart()
Se llama a este método cuando el usuario toca la pantalla por primera vez.
- A nivel de la clase, agrega variables para almacenar en caché los valores de X e Y más recientes. Después de que el usuario deja de moverse y levanta el dedo, estos son los puntos de partida para la siguiente ruta (el siguiente segmento de la línea que se debe dibujar).
private var currentX = 0f
private var currentY = 0f- Implementa el método
touchStart()de la siguiente manera. Restablecepath, muévete a las coordenadas X-Y del evento táctil (motionTouchEventXymotionTouchEventY) y asignacurrentXycurrentYa ese valor.
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}Paso 3: Implementa touchMove()
- A nivel de la clase, agrega una variable
touchTolerancey establécela enViewConfiguration.get(context).scaledTouchSlop.
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlopCon una ruta de acceso, no es necesario dibujar cada píxel ni solicitar una actualización de la pantalla cada vez. En cambio, puedes (y debes) interpolar una ruta entre puntos para obtener un rendimiento mucho mejor.
- Si el dedo apenas se movió, no es necesario dibujar.
- Si el dedo se movió menos que la distancia
touchTolerance, no dibuje. scaledTouchSlopdevuelve la distancia en píxeles que puede oscilar un toque antes de que el sistema considere que el usuario se está desplazando.
- Define el método
touchMove(). Calcula la distancia recorrida (dx,dy), crea una curva entre los dos puntos y la almacena enpath, actualiza el recuento acumulado decurrentXycurrentY, y dibujapath. Luego, llama ainvalidate()para forzar el redibujo de la pantalla con elpathactualizado.
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 en más detalle:
- Calcula la distancia que se movió (
dx, dy). - Si el movimiento fue mayor que la tolerancia al tacto, agrega un segmento a la ruta.
- Establece el punto de partida del siguiente segmento en el extremo de este segmento.
- Usar
quadTo()en lugar delineTo()crea una línea dibujada de forma suave y sin esquinas. Consulta Curvas de Bézier. - Llama a
invalidate()para (finalmente llamar aonDraw()y) volver a dibujar la vista.
Paso 4: Implementa touchUp()
Cuando el usuario levanta el dedo, solo es necesario restablecer la ruta para que no se vuelva a dibujar. No se dibuja nada, por lo que no es necesaria la invalidación.
- Implementa el método
touchUp().
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}- Ejecuta el código y usa el dedo para dibujar en la pantalla. Ten en cuenta que, si rotas el dispositivo, la pantalla se borrará porque no se guarda el estado del dibujo. En el caso de esta app de ejemplo, esto se diseñó de esta manera para que el usuario tenga una forma sencilla de borrar la pantalla.

Paso 5: Dibuja un marco alrededor del boceto
A medida que el usuario dibuja en la pantalla, tu app construye la ruta y la guarda en el mapa de bits extraBitmap. El método onDraw() muestra el mapa de bits adicional en el lienzo de la vista. Puedes dibujar más en onDraw(). Por ejemplo, podrías dibujar formas después de dibujar el mapa de bits.
En este paso, dibujarás un marco alrededor del borde de la foto.
- En
MyCanvasView, agrega una variable llamadaframeque contenga un objetoRect.
private lateinit var frame: Rect- Al final de
onSizeChanged(), define una inserción y agrega código para crear elRectque se usará para el fotograma, con las nuevas dimensiones y la inserción.
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)- En
onDraw(), después de dibujar el mapa de bits, dibuja un rectángulo.
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)- Ejecuta tu app y observa el fotograma.

Tarea (opcional): Almacena datos en una ruta
En la app actual, la información de dibujo se almacena en un mapa de bits. Si bien esta es una buena solución, no es la única forma posible de almacenar información de dibujo. La forma en que almacenas el historial de dibujo depende de la app y de tus diversos requisitos. Por ejemplo, si dibujas formas, puedes guardar una lista de formas con su ubicación y dimensiones. En el caso de la app de MiniPaint, podrías guardar la ruta como un Path. A continuación, se incluye un esquema general sobre cómo hacerlo, si quieres probarlo.
- En
MyCanvasView, quita todo el código deextraCanvasyextraBitmap. - Agrega variables para la ruta hasta el momento y la ruta que se dibuja actualmente.
// Path representing the drawing so far
private val drawing = Path()
// Path representing what's currently being drawn
private val curPath = Path()- En
onDraw(), en lugar de dibujar el mapa de bits, dibuja las rutas almacenadas y actuales.
// 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)- En
touchUp(), agrega la ruta actual a la ruta anterior y restablece la ruta actual.
// Add the current path to the drawing so far
drawing.addPath(curPath)
// Rewind the current path for the next touch
curPath.reset()- Ejecuta tu app. No debería haber ninguna diferencia.
Descarga el código del codelab terminado.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-canvas
También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.
- Un
Canvases una superficie de dibujo 2D que proporciona métodos para dibujar. - El
Canvasse puede asociar con una instancia deViewque lo muestre. - El objeto
Paintcontiene la información de estilo y color sobre cómo dibujar geometrías (como líneas, rectángulos, óvalos y rutas) y texto. - Un patrón común para trabajar con un lienzo es crear una vista personalizada y anular los métodos
onDraw()yonSizeChanged(). - Anula el método
onTouchEvent()para capturar los toques del usuario y responder a ellos dibujando elementos. - Puedes usar un mapa de bits adicional para almacenar en caché información de los dibujos que cambian con el tiempo. También podrías almacenar formas o una ruta.
Curso de Udacity:
Documentación para desarrolladores de Android:
- Clase
Canvas - Clase
Bitmap - Clase
View - Clase
Paint - Parámetros de configuración de
Bitmap.config - Clase
Path - Página de Wikipedia sobre curvas de Bézier
- Lienzo y elementos de diseño
- Serie de artículos sobre Arquitectura de gráficos (avanzado)
- drawables
- onDraw()
- onSizeChanged()
MotionEventViewConfiguration.get(context).scaledTouchSlop
En esta sección, se enumeran las posibles actividades para el hogar para los alumnos que trabajan en este codelab como parte de un curso dirigido por un instructor. Depende del instructor hacer lo siguiente:
- Si es necesario, asigna una tarea.
- Comunicarles a los alumnos cómo enviar las actividades para el hogar.
- Califica las actividades para el hogar.
Los instructores pueden usar estas sugerencias en la medida que quieran y deben asignar cualquier otra actividad para el hogar que consideren apropiada.
Si estás trabajando en este codelab por tu cuenta, usa estas actividades para el hogar para probar tus conocimientos.
Responde estas preguntas:
Pregunta 1
¿Cuáles de los siguientes componentes son necesarios para trabajar con un Canvas? Selecciona todas las opciones que correspondan.
▢ Bitmap
▢ Paint
▢ Path
▢ View
Pregunta 2
¿Qué hace una llamada a invalidate() (en términos generales)?
▢ Invalida y reinicia tu app.
▢ Borra el dibujo del mapa de bits.
▢ Indica que no se debe ejecutar el código anterior.
▢ Le indica al sistema que debe volver a dibujar la pantalla.
Pregunta 3
¿Cuál es la función de los objetos Canvas, Bitmap y Paint?
▢ Superficie de dibujo 2D, mapa de bits que se muestra en la pantalla, información de diseño para el dibujo.
▢ Superficie de dibujo en 3D, mapa de bits para almacenar en caché la ruta de acceso, información de diseño para el dibujo.
▢ Superficie de dibujo 2D, mapa de bits que se muestra en la pantalla, diseño de la vista
▢ Caché para la información de dibujo, mapa de bits para dibujar y la información de diseño para dibujar.
Para obtener vínculos a otros codelabs de este curso, consulta la página de destino de los codelabs de Aspectos avanzados de Android en Kotlin.