Grafiken in Compose

Viele Apps müssen genau steuern können, was auf dem Bildschirm dargestellt wird. Dabei kann es sich beispielsweise um eine aufwendige Anordnung von Grafikelementen in vielen verschiedenen Stilen handeln, z. B. um eine Box oder einen Kreis auf dem Bildschirm an der richtigen Stelle zu platzieren.

Einfache Zeichnung mit Modifikatoren und DrawScope

Um benutzerdefinierte Elemente in der Funktion „Compose“ zu zeichnen, verwenden Sie Modifikatoren wie Modifier.drawWithContent, Modifier.drawBehind und Modifier.drawWithCache.

Wenn Sie beispielsweise etwas hinter der zusammensetzbaren Funktion zeichnen möchten, können Sie mit dem Modifikator drawBehind Zeichenbefehle ausführen:

Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind {
            // this = DrawScope
        }
)

Wenn Sie lediglich eine zusammensetzbare Funktion benötigen, die zeichnet, können Sie die zusammensetzbare Funktion Canvas verwenden. Die zusammensetzbare Funktion Canvas ist ein praktischer Wrapper um Modifier.drawBehind. Das Canvas-Element wird im Layout auf dieselbe Weise platziert wie jedes andere UI-Element „Compose“. In der Canvas können Sie Elemente zeichnen und deren Stil und Position präzise steuern.

Alle Zeichenmodifikatoren stellen eine DrawScope bereit, eine auf einen Bereich reduzierte Zeichenumgebung, die ihren eigenen Zustand beibehält. So können Sie die Parameter für eine Gruppe grafischer Elemente festlegen. DrawScope bietet mehrere nützliche Felder, z. B. size, ein Size-Objekt, das die aktuellen Abmessungen von DrawScope angibt.

Zum Zeichnen können Sie eine der vielen Zeichenfunktionen in DrawScope verwenden. Mit dem folgenden Code wird beispielsweise ein Rechteck in der oberen linken Ecke des Bildschirms gezeichnet:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}

Pinkes Rechteck auf weißem Hintergrund, das ein Viertel des Bildschirms einnimmt
Abbildung 1: Mit „Canvas“ in Compose gezeichnetes Rechteck.

Weitere Informationen zu verschiedenen Zeichenmodifikatoren finden Sie in der Dokumentation zu Grafikmodifikatoren.

Koordinatensystem

Wenn Sie etwas auf dem Bildschirm zeichnen möchten, müssen Sie den Versatz (x und y) und die Größe des Elements kennen. Bei vielen der Zeichenmethoden für DrawScope werden Position und Größe über Standardparameterwerte bereitgestellt. Mit den Standardparametern wird das Element in der Regel am [0, 0]-Punkt auf dem Canvas positioniert. Außerdem wird wie im obigen Beispiel ein Standard-size bereitgestellt, das den gesamten Zeichenbereich ausfüllt. Das Rechteck befindet sich oben links. Wenn Sie die Größe und Position Ihres Elements anpassen möchten, müssen Sie mit dem Koordinatensystem in Compose vertraut sein.

Der Ursprung des Koordinatensystems ([0,0]) liegt beim Pixel ganz oben links im Zeichnungsbereich. x nimmt zu, wenn sie nach rechts verschoben wird, und y, wenn sie nach unten bewegt wird.

Ein Raster mit dem Koordinatensystem für links oben [0, 0] und rechts unten [Breite, Höhe]
Abbildung 2: Zeichnungskoordinatensystem / Zeichnungsraster.

Wenn Sie beispielsweise eine diagonale Linie von der oberen rechten Ecke des Canvas-Bereichs bis zur linken unteren Ecke zeichnen möchten, können Sie die Funktion DrawScope.drawLine() verwenden und einen Start- und Endversatz mit den entsprechenden x- und y-Positionen angeben:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue
    )
}

Grundlegende Transformationen

DrawScope bietet Transformationen, mit denen Sie ändern können, wo und wie die Zeichenbefehle ausgeführt werden.

Skalieren

Mit DrawScope.scale() können Sie die Größe Ihrer Zeichenvorgänge um einen Faktor erhöhen. Vorgänge wie scale() gelten für alle Zeichenvorgänge innerhalb der entsprechenden Lambda-Funktion. Mit dem folgenden Code wird beispielsweise scaleX 10-mal und scaleY 15-mal erhöht:

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

Ein ungleichförmiger Kreis
Abbildung 3: Skalierungsvorgang auf einen Kreis in Canvas anwenden.

Übersetzen

Mit DrawScope.translate() können Sie Ihre Zeichenvorgänge nach oben, unten, links oder rechts verschieben. Mit dem folgenden Code wird die Zeichnung beispielsweise um 100 Pixel nach rechts und 300 Pixel nach oben verschoben:

Canvas(modifier = Modifier.fillMaxSize()) {
    translate(left = 100f, top = -300f) {
        drawCircle(Color.Blue, radius = 200.dp.toPx())
    }
}

Ein Kreis, der sich nicht mehr in der Mitte befindet
Abbildung 4: Übersetzungsvorgang auf einen Kreis in Canvas anwenden

Drehen

Mit DrawScope.rotate() können Sie Ihre Zeichenvorgänge um einen Drehpunkt drehen. Mit dem folgenden Code wird beispielsweise ein Rechteck um 45 Grad gedreht:

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Ein Smartphone mit einem um 45 Grad gedrehten Rechteck in der Mitte des Bildschirms
Abbildung 5: Mit rotate() wird eine Drehung auf den aktuellen Zeichenbereich angewendet, bei dem das Rechteck um 45 Grad gedreht wird.

Eingesetzt

Verwenden Sie DrawScope.inset(), um die Standardparameter der aktuellen DrawScope anzupassen. Ändern Sie dabei die Zeichengrenzen und übersetzen Sie die Zeichnungen entsprechend:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

Mit diesem Code wird den Zeichenbefehlen ein Innenrand hinzugefügt:

Ein Rechteck, das um das Rechteck herum aufgefüllt wurde
Abbildung 6: Eine Einfügung zu Zeichenbefehlen anwenden.

Mehrere Transformationen

Wenn Sie mehrere Transformationen auf Ihre Zeichnungen anwenden möchten, verwenden Sie die Funktion DrawScope.withTransform(). Damit wird eine einzelne Transformation erstellt und angewendet, die alle gewünschten Änderungen kombiniert. Die Verwendung von withTransform() ist effizienter als verschachtelte Aufrufe einzelner Transformationen, da alle Transformationen gemeinsam in einem einzigen Vorgang ausgeführt werden, anstatt dass Compose jede der verschachtelten Transformationen berechnen und speichern muss.

Der folgende Code wendet beispielsweise sowohl eine Verschiebung als auch eine Drehung auf das Rechteck an:

Canvas(modifier = Modifier.fillMaxSize()) {
    withTransform({
        translate(left = size.width / 5F)
        rotate(degrees = 45F)
    }) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Ein Smartphone mit einem gedrehten Rechteck, das zur Seite des Bildschirms verschoben ist
Abbildung 7: Mit withTransform können Sie eine Drehung und eine Verschiebung anwenden. Dabei wird das Rechteck gedreht und nach links verschoben.

Gängige Zeichenvorgänge

Text zeichnen

Wenn Sie in der Funktion „Compose“ Text zeichnen möchten, können Sie normalerweise die zusammensetzbare Funktion Text verwenden. Wenn Sie sich jedoch in einer DrawScope befinden oder Ihren Text manuell anpassen möchten, können Sie die Methode DrawScope.drawText() verwenden.

Erstellen Sie zum Zeichnen von Text ein TextMeasurer mit rememberTextMeasurer und rufen Sie drawText mit dem Messgerät auf:

val textMeasurer = rememberTextMeasurer()

Canvas(modifier = Modifier.fillMaxSize()) {
    drawText(textMeasurer, "Hello")
}

Hello gezeichnet auf Leinwand
Abbildung 8: Text auf Canvas zeichnen

Text analysieren

Das Zeichnen von Text funktioniert etwas anders als andere Zeichenbefehle. Normalerweise geben Sie dem Zeichenbefehl die Größe (Breite und Höhe) zum Zeichnen der Form bzw. des Bildes an. Bei Text gibt es einige Parameter, die die Größe des gerenderten Textes steuern, z. B. Schriftgröße, Schriftart, Ligaturen und Buchstabenabstand.

Mit der Funktion „Compose“ können Sie abhängig von den oben genannten Faktoren mit einem TextMeasurer auf die gemessene Textgröße zugreifen. Wenn Sie einen Hintergrund hinter dem Text zeichnen möchten, können Sie anhand der gemessenen Informationen die Größe des Bereichs abrufen, den der Text einnimmt:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()),
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

Mit diesem Code-Snippet wird ein rosa Hintergrund für den Text erzeugt:

Mehrzeiliger Text, der 2⁄3 der gesamten Fläche einnimmt, mit einem Rechteck im Hintergrund
Abbildung 9: Mehrzeiliger Text, der 2⁄3 der gesamten Fläche einnimmt, mit einem Rechteck im Hintergrund.

Das Anpassen der Einschränkungen, der Schriftgröße oder einer anderen Eigenschaft, die sich auf die gemessene Größe auswirkt, führt zu einer neuen Größe im Bericht. Sie können sowohl für width als auch für height eine feste Größe festlegen. Der Text folgt dann dem festgelegten TextOverflow. Mit dem folgenden Code wird beispielsweise Text in 1⁄3 der Höhe und 1⁄3 der Breite des zusammensetzbaren Bereichs gerendert. Dabei wird TextOverflow auf TextOverflow.Ellipsis gesetzt:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixed(
                        width = (size.width / 3f).toInt(),
                        height = (size.height / 3f).toInt()
                    ),
                    overflow = TextOverflow.Ellipsis,
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

Der Text wird jetzt in den Einschränkungen mit Auslassungspunkten am Ende gezeichnet:

Text auf rosafarbenem Hintergrund, wobei Auslassungspunkte den Text abgeschnitten haben.
Abbildung 10: TextOverflow.Ellipsis mit festen Einschränkungen beim Messen von Text.

Bild zeichnen

Um ein ImageBitmap mit DrawScope zu zeichnen, laden Sie das Bild mit ImageBitmap.imageResource() und rufen dann drawImage auf:

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

Ein Bild eines Hundes, der auf Canvas gezeichnet wurde
Abbildung 11: ImageBitmap auf Canvas zeichnen

Grundformen zeichnen

In DrawScope gibt es viele Funktionen zum Zeichnen von Formen. Verwenden Sie zum Zeichnen einer Form eine der vordefinierten Zeichenfunktionen, z. B. drawCircle:

val purpleColor = Color(0xFFBA68C8)
Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    onDraw = {
        drawCircle(purpleColor)
    }
)

API

Ausgang

drawCircle()

Kreis zeichnen

drawRect()

Rechteck zeichnen

drawRoundedRect()

Abgerundetes Rechteck zeichnen

drawLine()

Linie zeichnen

drawOval()

Oval zeichnen

drawArc()

Bogen zeichnen

drawPoints()

Punkte zeichnen

Pfad zeichnen

Ein Pfad besteht aus einer Reihe mathematischer Anweisungen, die nach der Ausführung zu einer Zeichnung führen. DrawScope kann mit der Methode DrawScope.drawPath() einen Pfad zeichnen.

Angenommen, Sie möchten ein Dreieck zeichnen. Mit Funktionen wie lineTo() und moveTo() können Sie einen Pfad generieren, indem Sie die Größe des Zeichenbereichs verwenden. Rufen Sie dann drawPath() mit diesem neu erstellten Pfad auf, um ein Dreieck zu erhalten.

Spacer(
    modifier = Modifier
        .drawWithCache {
            val path = Path()
            path.moveTo(0f, 0f)
            path.lineTo(size.width / 2f, size.height / 2f)
            path.lineTo(size.width, 0f)
            path.close()
            onDrawBehind {
                drawPath(path, Color.Magenta, style = Stroke(width = 10f))
            }
        }
        .fillMaxSize()
)

Ein umgedrehtes lilafarbenes Pfaddreieck, das auf „Schreiben“ gezeichnet wurde
Abbildung 12: Path in Compose erstellen und zeichnen

Auf Canvas-Objekt zugreifen

Mit DrawScope haben Sie keinen direkten Zugriff auf ein Canvas-Objekt. Mit DrawScope.drawIntoCanvas() können Sie Zugriff auf das Canvas-Objekt selbst erhalten, für das Sie Funktionen aufrufen können.

Wenn Sie beispielsweise ein benutzerdefiniertes Drawable haben, das Sie auf dem Canvas zeichnen möchten, können Sie auf den Canvas zugreifen und Drawable#draw() aufrufen und dabei das Canvas-Objekt übergeben:

val drawable = ShapeDrawable(OvalShape())
Spacer(
    modifier = Modifier
        .drawWithContent {
            drawIntoCanvas { canvas ->
                drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
                drawable.draw(canvas.nativeCanvas)
            }
        }
        .fillMaxSize()
)

Ein schwarzes ShapeDrawable, das in voller Größe oval ist
Abbildung 13: Zugriff auf den Canvas zum Zeichnen von Drawable.

Weitere Informationen

Weitere Informationen zum Zeichnen in der Funktion „Compose“ finden Sie in den folgenden Ressourcen: