28 - Canvas - transformaciones geométricas de rotación, escalado y translación

El API de Canvas nos facilita con tres funciones independientes efectuar las transformaciones de rotación, escalado y translación.

Problema

Dibujar en la parte superior un rectángulo sin aplicar transformaciones, luego dibujar el mismo rectángulo en medio de la pantalla y aplicar una rotación de 45 grados. En la parte inferior de pantalla mostrar otro rectángulo del mismo tamaño pero aplicarle un escalado que lo muestre con un tamaño del 50% con respecto al original.
Finalmente dibujar un círculo con centro en la coordenada (0,0) y radio de 50, seguidamente aplicar una translación de 50 píxeles a la derecha y 50 píxeles hacia abajo.

Crearemos un proyecto llamado: 'Compose30'

La interfaz visual a implementar debe ser similar a:

Canvas transformaciones geométricas de rotación, escalado y translación Jetpack Compose

El código a implementar en Kotlin para obtener dicha funcionalidad es:

package com.tutorialesprogramacionya.compose30

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PantallaPrincipal()
        }
    }
}

@Composable
fun PantallaPrincipal() {
    Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
        val ancho = size.width
        val alto = size.height
        drawRect(
            topLeft = Offset(ancho / 2 - ancho * 0.10f, 0f),
            size = Size(ancho * 0.20f, alto * 0.20f),
            color = Color.Red
        )
        rotate(degrees = 45f) {
            drawRect(
                topLeft = Offset(ancho / 2 - ancho * 0.10f, alto / 2 - alto * 0.10f),
                size = Size(ancho * 0.20f, alto * 0.20f),
                color = Color.Green
            )
        }

        scale(scale = 0.5f) {
            drawRect(
                topLeft = Offset(ancho / 2 - ancho * 0.10f, alto - alto * 0.10f),
                size = Size(ancho * 0.20f, alto * 0.20f),
                color = Color.Blue
            )
        }
        translate(left=50f,top = 50f) {
            drawCircle(
                center = Offset(x = 0f, y = 0f),
                radius = 50f,
                color=Color.Cyan
            )
        }
    })
}

El primer rectángulo no se le aplican transformaciones, lo ubicamos en medio de la pantalla y ocupa un ancho y alto de 20% (damos dichos valores llamando a la función size):

@Composable
fun PantallaPrincipal() {
    Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
        val ancho = size.width
        val alto = size.height
        drawRect(
            topLeft = Offset(ancho / 2 - ancho * 0.10f, 0f),
            size = Size(ancho * 0.20f, alto * 0.20f),
            color = Color.Red
        )

Llamamos a la función rotate y le pasamos en el parámetro degrees el valor 45f, luego en la función lambda que le pasamos a rotate graficamos un rectángulo, igual que el anterior. Veremos que el mismo aparece rotado 45 grados:

        rotate(degrees = 45f) {
            drawRect(
                topLeft = Offset(ancho / 2 - ancho * 0.10f, alto / 2 - alto * 0.10f),
                size = Size(ancho * 0.20f, alto * 0.20f),
                color = Color.Green
            )
        }

Al rectángulo de la parte inferior de la pantalla le aplicamos un escalado de 0.5 (valor mayor a 1 obtenemos una imagen más grande):

        scale(scale = 0.5f) {
            drawRect(
                topLeft = Offset(ancho / 2 - ancho * 0.10f, alto - alto * 0.10f),
                size = Size(ancho * 0.20f, alto * 0.20f),
                color = Color.Blue
            )
        }

El círculo para evitar que quede parte fuera de la pantalla le aplicamos una translación de 50 píxeles en x e y:

        translate(left=50f,top = 50f) {
            drawCircle(
                center = Offset(x = 0f, y = 0f),
                radius = 50f,
                color=Color.Cyan
            )
        }

Este proyecto lo puede descargar en un zip desde este enlace: Compose30.zip

Acotaciones

Podemos hacer varias transformaciones sucesivas:

@Composable
fun PantallaPrincipal() {
    val imagen=ImageBitmap.imageResource(id = R.drawable.fondo)
    Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
        val ancho = size.width
        val alto = size.height
        rotate(degrees = 45f) {
            scale(scale=0.6f) {
                drawImage(
                    image = imagen,
                    dstOffset = IntOffset(0, 0),
                    dstSize = IntSize(ancho.toInt(), alto.toInt())
                )
            }
        }
    })
}

En el ejemplo estamos escalando la imagen que ocupa toda la pantalla y rotando en 45 grados.

Teniendo como resultado:

Canvas transformaciones geométricas de rotación, escalado y translación Jetpack Compose

Hay una solución más eficiente cuando tenemos que hacer transformaciones sucesivas. Debemos llamar a la función withTransform y pasar en la primera función lambda las transformaciones y en la segunda función lambda las funciones a las que se debe aplicar dichas transformaciones:

@Composable
fun PantallaPrincipal() {
    val imagen=ImageBitmap.imageResource(id = R.drawable.fondo)
    Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
        val ancho = size.width
        val alto = size.height

        withTransform({
            rotate(degrees = 45f)
            scale(scale = 0.6f)
        }) {
            drawImage(
                image = imagen,
                dstOffset = IntOffset(0, 0),
                dstSize = IntSize(ancho.toInt(), alto.toInt())
            )
        }
    })
}