Cómo recortar objetos Canvas

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

Para los fines de este codelab, el recorte es una forma de definir regiones de una imagen, un lienzo o un mapa de bits que se dibujan o no en la pantalla de forma selectiva. Uno de los propósitos del recorte es reducir el sobregiro. La superposición se produce cuando un píxel de la pantalla se dibuja más de una vez para mostrar la imagen final. Cuando reduces el sobrepintado, minimizas la cantidad de veces que se dibuja un píxel o una región de la pantalla para maximizar el rendimiento del dibujo. También puedes usar el recorte para crear efectos interesantes en el diseño y la animación de la interfaz de usuario.

Por ejemplo, cuando dibujas una pila de tarjetas superpuestas como se muestra a continuación, en lugar de dibujar completamente cada tarjeta de abajo hacia arriba, suele ser más eficiente dibujar solo las partes visibles. "Por lo general", porque las operaciones de recorte también tienen un costo y, en general, el sistema Android realiza mucha optimización de dibujo.

Para dibujar solo las partes visibles de las tarjetas, debes especificar una región de recorte para cada tarjeta. Por ejemplo, en el siguiente diagrama, cuando se aplica un rectángulo de recorte a una imagen, solo se muestra la parte que se encuentra dentro de ese rectángulo.

Por lo general, la región de recorte es un rectángulo, pero puede tener cualquier forma o combinación de formas, incluso texto. También puedes especificar si deseas incluir o excluir la región dentro de la región de recorte. Por ejemplo, puedes crear una región de recorte circular y mostrar solo lo que está fuera del círculo.

En este codelab, experimentarás con varias formas de recortar.

Conocimientos que ya deberías tener

Debes estar familiarizado con lo siguiente:

  • Cómo crear una app con un Activity y ejecutarla con Android Studio
  • Cómo crear un Canvas y dibujar en él
  • Cómo crear un View personalizado y anular onDraw() y onSizeChanged()

Qué aprenderás

  • Cómo recortar objetos para dibujar en un Canvas
  • Cómo guardar y restablecer los estados de dibujo de un lienzo
  • Cómo aplicar transformaciones a un lienzo y a un texto

Actividades

  • Crea una app que dibuje formas recortadas en la pantalla y muestre diferentes formas de recortar y el resultado en la visibilidad de esas formas.
  • También dibujarás texto traducido y sesgado.

La app de ClippingExample muestra cómo puedes usar y combinar formas para especificar qué partes de un lienzo se muestran en una vista. Tu app final se verá como la siguiente captura de pantalla.

Compilarás esta app desde cero, por lo que deberás configurar un proyecto, definir dimensiones y cadenas, y declarar algunas variables.

Paso 1: Crea el proyecto ClippingExample

  1. Crea un proyecto de Kotlin llamado ClippingExample con la plantilla Empty Activity. Usa com.example.android para el prefijo del nombre del paquete.
  2. Abre MainActivity.kt.
  3. En el método onCreate(), reemplaza la vista de contenido predeterminada y configúrala en una nueva instancia de ClippedView. Esta será tu vista personalizada para los ejemplos de recorte que crearás a continuación.
setContentView(ClippedView(this))
  1. En el mismo nivel que MainActivity.kt, crea un nuevo archivo y una nueva clase de Kotlin para una vista personalizada llamada ClippedView que extienda View. Asigna la firma que se muestra a continuación. El resto de tu trabajo se realizará dentro de este ClippedView. La anotación @JvmOverloads indica al compilador de Kotlin que genere sobrecargas para esta función que sustituyan los valores predeterminados de los parámetros.
class ClippedView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}

Paso 2: Agrega dimensiones y recursos de cadenas

  1. Define las dimensiones que usarás para las vistas recortadas en un archivo de recursos nuevo en res/values/dimens.xml. Estas dimensiones predeterminadas están codificadas y dimensionadas para adaptarse a una pantalla bastante pequeña.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">90dp</dimen>
   <dimen name="clipRectBottom">90dp</dimen>
   <dimen name="clipRectTop">0dp</dimen>
   <dimen name="clipRectLeft">0dp</dimen>

   <dimen name="rectInset">8dp</dimen>
   <dimen name="smallRectOffset">40dp</dimen>

   <dimen name="circleRadius">30dp</dimen>
   <dimen name="textOffset">20dp</dimen>
   <dimen name="strokeWidth">4dp</dimen>

   <dimen name="textSize">18sp</dimen>
</resources>

Para que la app se vea bien en una pantalla más grande (y para ver los detalles con mayor facilidad), puedes crear un archivo dimens con valores más grandes que solo se apliquen a pantallas más grandes.

  1. En Android Studio, haz clic con el botón derecho en la carpeta values y elige New > Values resource file.
  2. En el diálogo New Resource File, llama al archivo dimens. En Available qualifiers, selecciona Smallest Screen Width y haz clic en el botón >> para agregarlo a Chosen qualifiers. Ingresa 480 en el cuadro Ancho de pantalla más pequeño y haz clic en Aceptar.

  1. El archivo debería aparecer en tu carpeta de valores como se muestra a continuación.

  1. Si no ves el archivo, cambia a la vista Project Files de la app. La ruta completa del nuevo archivo es la que se muestra a continuación: ClippingExample/app/src/main/res/values-sw480dp/dimens.xml.

  1. Reemplaza el contenido predeterminado del archivo values-sw480dp/dimens.xml por las dimensiones que se indican a continuación.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">120dp</dimen>
   <dimen name="clipRectBottom">120dp</dimen>

   <dimen name="rectInset">10dp</dimen>
   <dimen name="smallRectOffset">50dp</dimen>

   <dimen name="circleRadius">40dp</dimen>
   <dimen name="textOffset">25dp</dimen>
   <dimen name="strokeWidth">6dp</dimen>
</resources>
  1. En strings.xml, agrega las siguientes cadenas. Se usarán para mostrar texto en el lienzo.
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>

Paso 3: Crea e inicializa un objeto Paint y un objeto Path

  1. Vuelve a la vista Android de tu proyecto.
  2. En ClippedView, define una variable Paint para dibujar. Habilita el suavizado y usa el ancho de trazo y el tamaño de texto definidos en las dimensiones, como se muestra a continuación.
private val paint = Paint().apply {
   // Smooth out edges of what is drawn without affecting shape.
   isAntiAlias = true
   strokeWidth = resources.getDimension(R.dimen.strokeWidth)
   textSize = resources.getDimension(R.dimen.textSize)
}
  1. En ClippedView, crea e inicializa un Path para almacenar de forma local la ruta de lo que se dibujó. Importa android.graphics.Path.
private val path = Path()

Paso 4: Configura las formas

En esta app, se muestran varias filas y dos columnas de formas recortadas de diversas maneras.

Todos tienen en común lo siguiente:

  • Un rectángulo grande (cuadrado) que actúa como contenedor
  • Una línea diagonal que atraviesa el rectángulo grande
  • Un círculo
  • Una cadena de texto corta

En este paso, configurarás las dimensiones de esas formas a partir de los recursos, de modo que solo tengas que obtener las dimensiones una vez cuando las uses más adelante.

  1. En ClippedView, debajo de path, agrega variables para las dimensiones de un rectángulo de recorte alrededor de todo el conjunto de formas.
private val clipRectRight = resources.getDimension(R.dimen.clipRectRight)
private val clipRectBottom = resources.getDimension(R.dimen.clipRectBottom)
private val clipRectTop = resources.getDimension(R.dimen.clipRectTop)
private val clipRectLeft = resources.getDimension(R.dimen.clipRectLeft)
  1. Agrega variables para la inserción de un rectángulo y el desplazamiento de un rectángulo pequeño.
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)
  1. Agrega una variable para el radio de un círculo. Este es el radio del círculo que se dibuja dentro del rectángulo.
private val circleRadius = resources.getDimension(R.dimen.circleRadius)
  1. Agrega un desplazamiento y un tamaño de texto para el texto que se dibuja dentro del rectángulo.
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)

Paso 4: Configura las ubicaciones de filas y columnas

Las formas de esta app se muestran en dos columnas y cuatro filas, según los valores de las dimensiones configuradas anteriormente. Las ecuaciones matemáticas para esto no forman parte de este codelab, pero puedes consultarlas mientras copias el código que se proporciona en este paso.

  1. Configura las coordenadas para dos columnas.
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight
  1. Agrega las coordenadas de cada fila, incluida la fila final del texto transformado.
private val rowOne = rectInset
private val rowTwo = rowOne + rectInset + clipRectBottom
private val rowThree = rowTwo + rectInset + clipRectBottom
private val rowFour = rowThree + rectInset + clipRectBottom
private val textRow = rowFour + (1.5f * clipRectBottom)
  1. Ejecuta la app. Esta se abrirá con una pantalla blanca en blanco debajo del nombre de la app.

En onDraw(), llamas a métodos para dibujar siete rectángulos recortados diferentes, como se muestra en la captura de pantalla de la app a continuación. Todos los rectángulos se dibujan de la misma manera; la única diferencia son las regiones de recorte definidas y la ubicación en la pantalla.

El algoritmo que se usa para dibujar los rectángulos funciona como se muestra en el siguiente diagrama y explicación. En resumen, dibujas una serie de rectángulos moviendo el origen de Canvas. Conceptualmente, esto consta de los siguientes pasos:

(1) Primero, traslada Canvas al lugar donde quieres que se dibuje el rectángulo. Es decir, en lugar de calcular dónde se debe dibujar el siguiente rectángulo y todas las demás formas, mueves el origen Canvas, es decir, su sistema de coordenadas.

(2) Luego, dibuja el rectángulo en el nuevo origen del lienzo. Es decir, dibujas las formas en la misma ubicación en el sistema de coordenadas traducido. Esto es mucho más simple y un poco más eficiente.

(3) Por último, restablece el Canvas a su Origin original.

Este es el algoritmo que implementarás:

  1. En onDraw(), llama a una función para completar el Canvas con el color de fondo gris y dibuja las formas originales.
  2. Llama a una función para cada rectángulo recortado y el texto que se dibujará.

Para cada rectángulo o texto, haz lo siguiente:

  1. Guarda el estado actual de Canvas para que puedas restablecerlo a ese estado inicial.
  2. Traslada el Origin del lienzo a la ubicación en la que deseas dibujar.
  3. Aplica formas y rutas de recorte.
  4. Dibuja el rectángulo o el texto.
  5. Restablece el estado de Canvas.

Paso: Anula onDraw()

  1. Anula onDraw() como se muestra en el siguiente código. Llamarás a una función para cada forma que dibujes, la cual implementarás más adelante.
 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawBackAndUnclippedRectangle(canvas)
        drawDifferenceClippingExample(canvas)
        drawCircularClippingExample(canvas)
        drawIntersectionClippingExample(canvas)
        drawCombinedClippingExample(canvas)
        drawRoundedRectangleClippingExample(canvas)
        drawOutsideClippingExample(canvas)
        drawSkewedTextExample(canvas)
        drawTranslatedTextExample(canvas)
        // drawQuickRejectExample(canvas)
    }
  1. Crea stubs para cada una de las funciones de dibujo de modo que el código siga compilándose. Puedes copiar el siguiente código.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
}
private fun drawDifferenceClippingExample(canvas: Canvas){
}
private fun drawCircularClippingExample(canvas: Canvas){
}
private fun drawIntersectionClippingExample(canvas: Canvas){
}
private fun drawCombinedClippingExample(canvas: Canvas){
}
private fun drawRoundedRectangleClippingExample(canvas: Canvas){
}
private fun drawOutsideClippingExample(canvas: Canvas){
}
private fun drawTranslatedTextExample(canvas: Canvas){
}
private fun drawSkewedTextExample(canvas: Canvas){
}
private fun drawQuickRejectExample(canvas: Canvas){
}

La app dibuja el mismo rectángulo y las mismas formas siete veces: primero sin recorte y, luego, seis veces con varias rutas de recorte aplicadas. El método drawClippedRectangle() factoriza el código para dibujar un rectángulo, como se muestra a continuación.

Paso 1: Crea el método drawClippedRectangle()

  1. Crea un método drawClippedRectangle() que tome un argumento canvas del tipo Canvas.
private fun drawClippedRectangle(canvas: Canvas) {
}
  1. Dentro del método drawClippedRectangle(), establece los límites del rectángulo de recorte para toda la forma. Aplica un rectángulo de recorte que limite el dibujo solo al cuadrado.
canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
)

El método Canvas.clipRect(...) reduce la región de la pantalla en la que se pueden escribir las operaciones de dibujo futuras. Establece los límites de recorte como la intersección espacial del rectángulo de recorte actual y el rectángulo que se pasó a clipRect(). Existen muchas variantes del método clipRect() que aceptan diferentes formas para las regiones y permiten diferentes operaciones en el rectángulo de recorte.

  1. Rellena el canvas con color blanco. Sí. Todo el lienzo, porque no estás dibujando rectángulos, sino recortando. Debido al rectángulo de recorte, solo se rellena la región definida por el rectángulo de recorte, lo que crea un rectángulo blanco. El resto de la superficie permanece en gris.
canvas.drawColor(Color.WHITE)
  1. Cambia el color a rojo y dibuja una línea diagonal dentro del rectángulo de recorte.
paint.color = Color.RED
canvas.drawLine(
   clipRectLeft,clipRectTop,
   clipRectRight,clipRectBottom,paint
)
  1. Establece el color en verde y dibuja un círculo dentro del rectángulo de recorte.
paint.color = Color.GREEN
canvas.drawCircle(
   circleRadius,clipRectBottom - circleRadius,
   circleRadius,paint
)
  1. Establece el color en azul y dibuja texto alineado con el borde derecho del rectángulo de recorte. Usa canvas.drawText() para dibujar texto.
paint.color = Color.BLUE
// Align the RIGHT side of the text with the origin.
paint.textSize = textSize
paint.textAlign = Paint.Align.RIGHT
canvas.drawText(
   context.getString(R.string.clipping),
   clipRectRight,textOffset,paint
)

Paso 2: Implementa el método drawBackAndUnclippedRectangle()

  1. Para ver el método drawClippedRectangle() en acción, dibuja el primer rectángulo sin recortar implementando el método drawBackAndUnclippedRectangle() como se muestra a continuación. Guarda el canvas, tradúcelo a la primera posición de fila y columna, dibuja llamando a drawClippedRectangle() y, luego, restablece el canvas a su estado anterior.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
   canvas.drawColor(Color.GRAY)
   canvas.save()
   canvas.translate(columnOne,rowOne)
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. Ejecuta la app. Deberías ver el primer rectángulo blanco con su círculo, la línea roja y el texto sobre un fondo gris.

En los siguientes métodos de ejemplo de recorte, aplicarás varias combinaciones de regiones de recorte para lograr efectos gráficos y aprenderás a combinar regiones de recorte para crear cualquier forma que necesites.

Cada uno de estos métodos sigue el mismo patrón.

  1. Guarda el estado actual del lienzo: canvas.save()

El contexto de la actividad mantiene una pila de estados de dibujo. Los estados de dibujo constan de la matriz de transformación actual y la región de recorte actual. Puedes guardar el estado actual, realizar acciones que cambien el estado del dibujo (como traducir o rotar el lienzo) y, luego, restablecer el estado del dibujo guardado. (Nota: Esto es similar al comando "stash" en Git).

Cuando tu dibujo incluye transformaciones, encadenar y deshacer transformaciones invirtiéndolas es propenso a errores. Por ejemplo, si primero traduces, luego estiras y, por último, rotas, se vuelve complejo rápidamente. En su lugar, guarda el estado del lienzo, aplica las transformaciones, dibuja y, luego, restablece el estado anterior.

Por ejemplo, podrías definir una región de recorte y guardar ese estado. Luego, traduce el lienzo, agrega una región de recorte y rota. Después de dibujar, puedes restablecer el estado de recorte original y continuar con una transformación de sesgo y traducción diferente, como se muestra en el diagrama.

  1. Traduce el origen del lienzo a las coordenadas de fila y columna: canvas.translate()

Es mucho más sencillo mover el origen del lienzo y dibujar lo mismo en un nuevo sistema de coordenadas que mover todos los elementos para dibujar. (Sugerencia: Puedes usar la misma técnica para rotar elementos).

  1. Aplica transformaciones al path, si corresponde.
  2. Aplicar recorte: canvas.clipPath(path)
  3. Dibuja las formas: drawClippedRectangle() or drawText()
  4. Restablece el estado anterior del lienzo: canvas.restore()

Paso 1: Implementa drawDifferenceClippingExample(canvas)

Agrega código para dibujar el segundo rectángulo, que usa la diferencia entre dos rectángulos de recorte para crear un efecto de marco de fotos.

Usa el siguiente código, que hace lo siguiente:

  1. Guarda el lienzo.
  2. Traslada el origen del lienzo al espacio abierto de la primera fila, segunda columna, a la derecha del primer rectángulo.
  3. Aplica dos rectángulos de recorte. El operador DIFFERENCE resta el segundo rectángulo del primero.
  1. Llama al método drawClippedRectangle() para dibujar el lienzo modificado.
  2. Restablece el estado del lienzo.
private fun drawDifferenceClippingExample(canvas: Canvas) {
   canvas.save()
   // Move the origin to the right for the next rectangle.
   canvas.translate(columnTwo,rowOne)
   // Use the subtraction of two clipping rectangles to create a frame.
   canvas.clipRect(
       2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .DIFFERENCE) was deprecated in API level 26. The recommended
   // alternative method is clipOutRect(float, float, float, float),
   // which is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
       canvas.clipRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset,
            Region.Op.DIFFERENCE
       )
   } else {
       canvas.clipOutRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. Ejecuta la app, que debería verse así.

Paso 2: Implementa drawCircularClippingExample(canvas)

A continuación, agrega código para dibujar un rectángulo que use una región de recorte circular creada a partir de una ruta de acceso circular, lo que quita (no dibuja) el círculo y, por lo tanto, muestra el fondo gris.

private fun drawCircularClippingExample(canvas: Canvas) {

   canvas.save()
   canvas.translate(columnOne, rowTwo)
   // Clears any lines and curves from the path but unlike reset(),
   // keeps the internal data structure for faster reuse.
   path.rewind()
   path.addCircle(
       circleRadius,clipRectBottom - circleRadius,
       circleRadius,Path.Direction.CCW
   )
   // The method clipPath(path, Region.Op.DIFFERENCE) was deprecated in
   // API level 26. The recommended alternative method is
   // clipOutPath(Path), which is currently available in
   // API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipPath(path, Region.Op.DIFFERENCE)
   } else {
       canvas.clipOutPath(path)
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

Paso 3: Implementa drawIntersectionClippingExample(canvas)

A continuación, agrega código para dibujar la intersección de dos rectángulos de recorte en la segunda fila y columna.

Ten en cuenta que, según la resolución de la pantalla, el aspecto de esta región variará. Experimenta con la dimensión smallRectOffset para cambiar el tamaño de la región visible. Un valor de smallRectOffset más pequeño genera una región más grande en la pantalla.

private fun drawIntersectionClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowTwo)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight - smallRectOffset,
       clipRectBottom - smallRectOffset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .INTERSECT) was deprecated in API level 26. The recommended
   // alternative method is clipRect(float, float, float, float), which
   // is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom,
           Region.Op.INTERSECT
       )
   } else {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

Paso 4: Implementa drawCombinedClippingExample(canvas)

A continuación, combina formas, un círculo y un rectángulo, y dibuja cualquier ruta para definir una región de recorte.

private fun drawCombinedClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne, rowThree)
   path.rewind()
   path.addCircle(
       clipRectLeft + rectInset + circleRadius,
       clipRectTop + circleRadius + rectInset,
       circleRadius,Path.Direction.CCW
   )
   path.addRect(
       clipRectRight / 2 - circleRadius,
       clipRectTop + circleRadius + rectInset,
       clipRectRight / 2 + circleRadius,
       clipRectBottom - rectInset,Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

Paso 5: Implementa drawRoundedRectangleClippingExample(canvas)

A continuación, agrega un rectángulo redondeado, que es una forma de recorte que se usa con frecuencia.

  1. En el nivel superior, crea e inicializa una variable de rectángulo. RectF es una clase que contiene coordenadas de rectángulos en números de punto flotante.
private var rectF = RectF(
   rectInset,
   rectInset,
   clipRectRight - rectInset,
   clipRectBottom - rectInset
)
  1. Implementa la función drawRoundedRectangleClippingExample(). La función addRoundRect() toma un rectángulo, valores para los valores x e y del radio de la esquina y la dirección para enrollar el contorno del rectángulo redondeado. Path.Direction especifica cómo se orientan las formas cerradas (p.ej., rectángulos, óvalos) cuando se agregan a una ruta. CCW significa en sentido contrario a las manecillas del reloj.
private fun drawRoundedRectangleClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowThree)
   path.rewind()
   path.addRoundRect(
       rectF,clipRectRight / 4,
       clipRectRight / 4, Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

Paso 6: Implementa drawOutsideClippingExample(canvas)

Recorta la parte exterior alrededor del rectángulo duplicando las inserciones del rectángulo de recorte.

private fun drawOutsideClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne,rowFour)
   canvas.clipRect(2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset)
   drawClippedRectangle(canvas)
   canvas.restore()
}

Paso 7: Implementa drawTranslatedTextExample(canvas)

El diseño del texto no es muy diferente de otras formas, y puedes aplicar transformaciones al texto. Por ejemplo, puedes traducir texto traduciendo el lienzo y dibujando el texto.

  1. Implementa la siguiente función.
private fun drawTranslatedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.GREEN
   // Align the RIGHT side of the text with the origin.
   paint.textAlign = Paint.Align.LEFT
   // Apply transformation to canvas.
   canvas.translate(columnTwo,textRow)
   // Draw text.
   canvas.drawText(context.getString(R.string.translated),
       clipRectLeft,clipRectTop,paint)
   canvas.restore()
}
  1. Ejecuta la app para ver el texto traducido.

Paso 8: Implementa drawSkewedTextExample(canvas)

También puedes sesgar el texto. Es decir, distorsionarla de varias maneras.

  1. Crea la siguiente función en ClippedView.
private fun drawSkewedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.YELLOW
   paint.textAlign = Paint.Align.RIGHT
   // Position text.
   canvas.translate(columnTwo, textRow)
   // Apply skew transformation.
   canvas.skew(0.2f, 0.3f)
   canvas.drawText(context.getString(R.string.skewed),
       clipRectLeft, clipRectTop, paint)
   canvas.restore()
}
  1. Ejecuta la app para ver el texto sesgado dibujado antes del texto traducido.

El método Canvas de quickReject() te permite verificar si un rectángulo o una ruta especificados quedarían completamente fuera de las regiones visibles actualmente, después de que se hayan aplicado todas las transformaciones.

El método quickReject() es muy útil cuando construyes dibujos más complejos y necesitas hacerlo lo más rápido posible. Con quickReject(), puedes decidir de manera eficiente qué objetos no tienes que dibujar en absoluto, y no es necesario que escribas tu propia lógica de intersección.

  • El método quickReject() devuelve true si el rectángulo o la ruta no serían visibles en la pantalla. En el caso de las superposiciones parciales, aún debes realizar tu propia verificación.
  • El EdgeType puede ser AA (suavizado: Trata los bordes redondeándolos, ya que pueden estar suavizados) o BW (blanco y negro: Trata los bordes redondeándolos al límite de píxel más cercano) para redondearlos al píxel más cercano.

Hay varias versiones de quickReject(), y también puedes encontrarlas en la documentación.

boolean

quickReject(float left, float top, float right, float bottom, Canvas.EdgeType type)

boolean

quickReject(RectF rect, Canvas.EdgeType type)

boolean

quickReject(Path path, Canvas.EdgeType type)

En este ejercicio, dibujarás en una fila nueva, debajo del texto y dentro de clipRect, como antes.

  • Primero, llamas a quickReject() con un rectángulo inClipRectangle que se superpone con clipRect. Por lo tanto, quickReject() devuelve falso, clipRect se completa con BLACK y se dibuja el rectángulo inClipRectangle.

  • Luego, cambia el código y llama a quickReject() con notInClipRectangle. quickReject() ahora devuelve verdadero, clipRect se completa con WHITE y notInClipRectangle no se dibuja.

Cuando tienes dibujos complejos, esto puede indicarte rápidamente qué formas están completamente fuera de la región de recorte y para cuáles es posible que debas realizar cálculos y dibujos adicionales, ya que están parcial o totalmente dentro de la región de recorte.

Paso: Experimenta con quickReject()

  1. En el nivel superior, crea una variable para las coordenadas Y de una fila adicional.
   private val rejectRow = rowFour + rectInset + 2*clipRectBottom
  1. Agrega la siguiente función drawQuickRejectExample() a ClippedView. Lee el código, ya que contiene todo lo que necesitas saber para usar quickReject().
private fun drawQuickRejectExample(canvas: Canvas) {
   val inClipRectangle = RectF(clipRectRight / 2,
       clipRectBottom / 2,
       clipRectRight * 2,
       clipRectBottom * 2)

   val notInClipRectangle = RectF(RectF(clipRectRight+1,
       clipRectBottom+1,
       clipRectRight * 2,
       clipRectBottom * 2))

   canvas.save()
   canvas.translate(columnOne, rejectRow)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
   )
   if (canvas.quickReject(
           inClipRectangle, Canvas.EdgeType.AA)) {
       canvas.drawColor(Color.WHITE)
   }
   else {
       canvas.drawColor(Color.BLACK)
       canvas.drawRect(inClipRectangle, paint
       )
   }
       canvas.restore()
}
  1. En onDraw(), quita el comentario de la invocación de drawQuickRejectExample().
  2. Ejecuta tu app y verás un rectángulo negro, que es la región de recorte rellenada, y partes de inClipRectangle, ya que los dos rectángulos se superponen, por lo que quickReject() devuelve false y se dibuja inClipRectangle.

  1. En drawQuickRejectExample(), cambia el código para ejecutar quickReject() en notInClipRectangle.. Ahora quickReject() devuelve true y la región de recorte se completa con blanco.

Descarga el código del codelab terminado.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping


También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Download Zip

  • El Context de una actividad mantiene un estado que conserva las transformaciones y las regiones de recorte para el Canvas.
  • Usa canvas.save() y canvas.restore() para dibujar y volver al estado original del lienzo.
  • Para dibujar varias formas en un lienzo, puedes calcular su ubicación o mover (trasladar) el origen de la superficie de dibujo. Esto último puede facilitar la creación de métodos de utilidad para secuencias de dibujo repetidas.
  • Las regiones de recorte pueden tener cualquier forma, combinación de formas o ruta.
  • Puedes agregar, restar y cruzar regiones de recorte para obtener exactamente la región que necesitas.
  • Puedes aplicar transformaciones al texto transformando el lienzo.
  • El método Canvas de quickReject() te permite verificar si un rectángulo o una ruta especificados se encuentran completamente fuera de las regiones visibles actualmente.

Curso de Udacity:

Documentación para desarrolladores de Android:

Consulta también la serie de artículos sobre la arquitectura de gráficos para obtener una explicación detallada de cómo el framework de Android dibuja en la pantalla.

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

¿Qué método llamas para excluir de manera eficiente las formas del dibujo?

excludeFromDrawing()

quickReject()

onDraw()

clipRect()

Pregunta 2

Canvas.save() y Canvas.restore(), ¿qué información guardan y restablecen?

▢ Color, ancho de línea, etcétera

▢ Solo transformaciones actuales

▢ Transformaciones y región de recorte actuales

▢ Solo la región de recorte actual

Pregunta 3

Paint.Align especifica lo siguiente:

▢ Cómo alinear las siguientes formas de dibujo

▢ De qué lado del origen se extrae el texto

▢ Dónde se alinea en la región de recorte

▢ Qué lado del texto se debe alinear con el origen

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.