30 - Canvas - captura de eventos de toque y movimiento

Con la función componible Canvas podemos capturar cuando el usuario toca con el dedo y comienza el desplazamiento dentro de la pantalla.

Por el momento estas funcionalidades en Compose son experimentales y como tal pueden cambiar en futuras versiones del API, veremos que cada vez que se utilizan funciones experimentales debemos agregar una anotación a la función respectiva.

Problema

Disponer una barra de selección de color en la parte superior y en la parte inferior un Canvas que ocupe el resto de la pantalla. Permitir dibujar a mano alzada utilizando el color seleccionado.

Crearemos un proyecto llamado: 'Compose32'

La interfaz visual a implementar debe ser similar a:

captura eventos touch y move Canvas Jetpack Compose

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

package com.tutorialesprogramacionya.compose32

import android.os.Bundle
import android.view.MotionEvent
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    @ExperimentalComposeUiApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setContent {
            PantallaPrincipal()
        }
    }
}

data class Punto(val x: Float, val y: Float, val color: Color)

@ExperimentalComposeUiApi
@Composable
fun PantallaPrincipal() {
    val puntos = remember { mutableStateListOf<Punto>() }
    var colorSeleccionado by remember { mutableStateOf(Color.Red) }
    Column(
    ) {
        SelectorColor(seleccion = {
            colorSeleccionado = it
        })
        Canvas(modifier = Modifier
            .fillMaxSize()
            .pointerInteropFilter {
                when (it.actionMasked) {
                    MotionEvent.ACTION_UP -> {
                        puntos.add(Punto(-1f, -1f, colorSeleccionado))
                        true
                    }
                    MotionEvent.ACTION_MOVE -> {

                        puntos.add(Punto(it.x, it.y, colorSeleccionado))
                        true
                    }
                    MotionEvent.ACTION_DOWN -> {
                        puntos.add(Punto(it.x, it.y, colorSeleccionado))
                        true
                    }
                    else -> false
                }
            }) {
            var primera = true
            var iniciox = 0f
            var inicioy = 0f
            for (punto in puntos) {
                if (punto.x == -1f && punto.y == -1f) {
                    primera = true
                } else
                    if (primera) {
                        iniciox = punto.x
                        inicioy = punto.y
                        primera = false
                    } else {
                        drawLine(
                            color = punto.color,
                            start = Offset(x = iniciox, y = inicioy),
                            end = Offset(x = punto.x, y = punto.y),
                            strokeWidth = 12f
                        )
                        iniciox = punto.x
                        inicioy = punto.y
                    }
            }
        }
    }
}

@Composable
fun SelectorColor(seleccion: (color: Color) -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .horizontalScroll(rememberScrollState()),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        for (rojo in 0..255 step 30)
            for (verde in 0..255 step 30)
                for (azul in 0..255 step 30)
                    Button(
                        modifier=Modifier.height(60.dp),
                        onClick = { seleccion(Color(rojo,verde,azul)) },
                        colors = ButtonDefaults.textButtonColors(
                            backgroundColor = Color(rojo,verde,azul)
                        )
                    ) {
                    }
    }
}

Para borrar la barra de status de la parte superior implementamos el siguiente código antes que se llame a la función componible setContent:

class MainActivity : ComponentActivity() {
    @ExperimentalComposeUiApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setContent {
            PantallaPrincipal()
        }
    }
}

Para mostrar la barra de selección de color implementamos la función componible 'SelectorColor', en la misma mediante tres for anidados procedemos a crear una serie de botones con distintos colores:

@Composable
fun SelectorColor(seleccion: (color: Color) -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .horizontalScroll(rememberScrollState()),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        for (rojo in 0..255 step 30)
            for (verde in 0..255 step 30)
                for (azul in 0..255 step 30)
                    Button(
                        modifier=Modifier.height(60.dp),
                        onClick = { seleccion(Color(rojo,verde,azul)) },
                        colors = ButtonDefaults.textButtonColors(
                            backgroundColor = Color(rojo,verde,azul)
                        )
                    ) {
                    }
    }
}

Cuando se presiona alguno de los botones se llama a la función lambda de la función principal para que almacene el color seleccionado.

Definimos un data class que representa un punto de la pantalla y su color, luego crearemos una lista de este tipo de data class para almacenar cada punto donde el usuario dibuja:

data class Punto(val x: Float, val y: Float, val color: Color)

Como la captura de eventos de touch (toque) todavía no está finlalizada, debemos agregar la anotación @ExperimentalComposeUiApi previa a la declaración de la función (esto puede haber cambiado para el momento que esté haciendo el curso)

La función canvar mediante el parámetro modifier procedemos a llamar a la función pointerInteropFilter y pasar una función lambda que recibe como parámetro la acción que ha efectuado el operador (presión con el dedo, movimiento, levantado del dedo):

@ExperimentalComposeUiApi
@Composable
fun PantallaPrincipal() {
    val puntos = remember { mutableStateListOf<Punto>() }
    var colorSeleccionado by remember { mutableStateOf(Color.Red) }
    Column(
    ) {
        SelectorColor(seleccion = {
            colorSeleccionado = it
        })
        Canvas(modifier = Modifier
            .fillMaxSize()
            .pointerInteropFilter {
                when (it.actionMasked) {
                    MotionEvent.ACTION_UP -> {
                        puntos.add(Punto(-1f, -1f, colorSeleccionado))
                        true
                    }
                    MotionEvent.ACTION_MOVE -> {

                        puntos.add(Punto(it.x, it.y, colorSeleccionado))
                        true
                    }
                    MotionEvent.ACTION_DOWN -> {
                        puntos.add(Punto(it.x, it.y, colorSeleccionado))
                        true
                    }
                    else -> false
                }

Por ejemplo cuando sucede el evento ACTION_DOWN procedemos a guardar en la lista 'puntos' un objeto de la clase Punto donde acaba de presionar en pantalla. Luego sucede lo mismo cada vez que mueve el dedo estando en la superficie de la pantalla, finalmente cuando levanta el dedo de la pantalla guardamos los valores -1,-1 para tener en cuenta que ahí finaliza el trazo de la línea.

Para dibujar propiamente dicho utilizamos la función drawLine y trazamos líneas de un punto a otro, con la salvedad donde encontremos un valor (-1,-1) significa que ha finalizado el trazo de la línea:

            }) {
            var primera = true
            var iniciox = 0f
            var inicioy = 0f
            for (punto in puntos) {
                if (punto.x == -1f && punto.y == -1f) {
                    primera = true
                } else
                    if (primera) {
                        iniciox = punto.x
                        inicioy = punto.y
                        primera = false
                    } else {
                        drawLine(
                            color = punto.color,
                            start = Offset(x = iniciox, y = inicioy),
                            end = Offset(x = punto.x, y = punto.y),
                            strokeWidth = 12f
                        )
                        iniciox = punto.x
                        inicioy = punto.y
                    }
            }
        }
    }
}

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