Compose: Canvas mit Pixelgitter an Datentyp binden?

Mat

Aktives Mitglied
Das passt hier so halb in AppEntwicklung rein. Es ist eigentlich eine Desktopanwendung aber nutzt die Jetpack Compose API, die meistens für Android-Apps verwendet wird. Deswegen ist das gut übertragbar.

Kennt sich jemand Jetpack Compose aus (im Speziellen die Canvas-Komponente und States)? Vielleicht hat ja jemand Anregungen und Tipps für mich.

Ich möchte auf einem in Blöcke unterteilten Canvas zeichnen (Kalender-Matrix mit 7x52 Blöcken, jeder Block repräsentiert einen Tag).
2021-12-12_matrix.png


Dieses Gitter sollte idealerweise an einen 2D-Array or eine Hashmap gebunden sein. Ich habe gelesen, das solle sich mit Compose ganz gut machen lassen und ich hatte es schon mit Textfeldern getestet. Aber ich bin noch so'n Bisschen am überlegen, wie sich das am sinnvollsten umsetzen lässt.

Bin am rumfummeln und hab noch ein paar Offset-Fehler. Grundsätzlich funktioniert das Aktivieren und Deaktivieren von Blöcken durch Klicks (ich möchte später auch noch detectDragGestures() benutzen, damit man zusätzlich zum Klicken auch mit der Maus zeichnen kann):
2021-12-12_demo.gif

Probleme

  1. bin nicht zufrieden mit meiner Matrix und wie ich sie abspeichere / binde .. mache ich das überhaupt richtig?
  2. im Moment ändere ich bei einem OnClick-Event indirekt die Eigenschaften eines Rechtecks (transparent oder grün) und zeichne bei onDraw alle Rechtecke. Eigentlich müsste es ja reichen, nur die veränderten Rechtecke zu zeichnen. Kann man da Observer drauf packen?
  3. DrawScope habe ich noch nicht so ganz verstanden, denn es war mir nicht möglich Zeichnungen in Funktionen auszulagern (ich kann den Canvas nicht als Referenz weitergeben)


Hier der aktuelle Code (es sind einige redundante Funktionen dabei, weil ich noch am rumprobieren bin):
Main.kt:
// clang-format off
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import org.jetbrains.skia.Point
import kotlin.math.floor

private const val ROWS = 7
private const val COLS = 52
private const val BLOCK_SIZE = 25
private const val CANVAS_HEIGHT = ROWS * BLOCK_SIZE
private const val CANVAS_WIDTH = COLS * BLOCK_SIZE
@Composable
fun derCanvasErleben(gridBlocks: SnapshotStateMap<Int, Boolean>, blocks: SnapshotStateList<Rect>) {
    Canvas(
        modifier = Modifier
            .size(width = CANVAS_WIDTH.dp, height = CANVAS_HEIGHT.dp)
            .scale(1f)
            .pointerInput(Unit) {
                detectTapGestures { offset ->
                    val xy = translateOffsetToCoordinate(offset)
                    val day = translateCoordinateToDay(xy)
                    println("offset=$offset; xy=$xy; day=$day")
                    gridBlocks[day] = !gridBlocks.getOrDefault(day, false)
                }
            },
        onDraw = {
            // Horizontales Gitter
            for (i in 0 until (CANVAS_HEIGHT + 1) step BLOCK_SIZE) {
                drawLine(
                    color = Color.LightGray,
                    start = Offset(0f, i.toFloat()),
                    end = Offset(CANVAS_WIDTH.toFloat(), i.toFloat()),
                    strokeWidth = 1.25f,
                    pathEffect = PathEffect.dashPathEffect(floatArrayOf(2f, 2f), 0f)
                )
            }

            // Vertikales Gitter
            for (i in 0 until (CANVAS_WIDTH + 1) step BLOCK_SIZE) {
                drawLine(
                    color = Color.LightGray,
                    start = Offset(i.toFloat(), 0f),
                    end = Offset(i.toFloat(), CANVAS_HEIGHT.toFloat()),
                    strokeWidth = 1.25f,
                    pathEffect = PathEffect.dashPathEffect(floatArrayOf(2f, 2f), 0f)
                )
            }

            for (i in IntRange(1, ROWS * COLS)) {
                var blockColour = Color.Transparent
                if (gridBlocks.getOrDefault(i, false)) {
                    blockColour = Color.Green
                }
                val rect = getRect(i, blocks)
                drawRect(color = blockColour, topLeft = rect.topLeft, size = rect.size)
            }

        },
    )
}

fun getRect(index: Int, blocks: SnapshotStateList<Rect>): Rect {
    return blocks[index]
}

fun translateOffsetToCoordinate(pos: Offset): Point {
    return Point(floor(pos.x / 25), floor(pos.y / 25))
}

fun translateCoordinateToDay(xy: Point): Int {
    return ((xy.x * ROWS) + xy.y + 1).toInt()
}

fun translateDayToCoordinate(day: Int): Point {
    val row = day % ROWS
    val col = day % COLS
    return Point(col * 1f, row * 1f)
}

fun main() = application {
    val gridBlocks: SnapshotStateMap<Int, Boolean> = remember { mutableStateMapOf() }
    val blocks: SnapshotStateList<Rect> = remember { mutableStateListOf() }
    blocks.add(Rect(Offset(-1f, -1f), Offset(-1f, -1f)))
    for (i in IntRange(1, ROWS * COLS)) {
        gridBlocks[i] = false
        val xy = translateDayToCoordinate(i)
        val topLeft = Offset(xy.x * BLOCK_SIZE, xy.y * BLOCK_SIZE)
        val bottomRight = Offset(topLeft.x + BLOCK_SIZE, topLeft.y + BLOCK_SIZE)
        blocks.add(Rect(topLeft = topLeft, bottomRight = bottomRight))
    }

    Window(
        onCloseRequest = ::exitApplication,
        title = "ContribArt",
        state = rememberWindowState(
            size = DpSize(
                (CANVAS_WIDTH + 2 * BLOCK_SIZE).dp,
                (CANVAS_HEIGHT + 3 * BLOCK_SIZE).dp
            )
        )
    ) {
        derCanvasErleben(gridBlocks, blocks)
    }
}


build.gradle.kts:
// clang-format off
import org.jetbrains.compose.compose

plugins {
    kotlin("jvm") version "1.5.31"
    id("org.jetbrains.compose") version "1.0.0"
}

repositories {
    mavenCentral()
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    google()
}

dependencies {
    implementation(compose.desktop.currentOs)
}

compose.desktop {
    application {
        mainClass = "MainKt"
    }
}

Edit: Codeformatierung deaktivert, hat rumgespackt
 
Zuletzt bearbeitet:
Zurück
Oben Unten