Grafiken in Compose

Viele Apps müssen genau steuern können, was auf dem Bildschirm gezeichnet wird. Das kann so einfach sein wie das Platzieren eines Rechtecks oder Kreises an der richtigen Stelle auf dem Bildschirm oder eine aufwendige Anordnung von Grafikelementen in vielen verschiedenen Stilen.

Einfache Zeichnung mit Modifikatoren und DrawScope

Die wichtigste Möglichkeit, etwas Benutzerdefiniertes in Compose zu zeichnen, sind Modifikatoren wie Modifier.drawWithContent, Modifier.drawBehind und Modifier.drawWithCache.

Wenn Sie beispielsweise etwas hinter Ihrem Composable zeichnen möchten, können Sie den Modifier drawBehind verwenden, um mit der Ausführung von Zeichenbefehlen zu beginnen:

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

Wenn Sie nur eine Composable-Funktion benötigen, die etwas zeichnet, können Sie die Composable-Funktion Canvas verwenden. Die Canvas-Composable ist ein praktischer Wrapper für Modifier.drawBehind. Sie platzieren das Canvas in Ihrem Layout genauso wie jedes andere Compose-UI-Element. Mit Canvas können Sie Elemente zeichnen und dabei Stil und Position genau festlegen.

Alle Zeichenmodifikatoren machen eine DrawScope verfügbar, eine eingeschränkte Zeichenumgebung, die ihren eigenen Status beibehält. So können Sie die Parameter für eine Gruppe von grafischen Elementen festlegen. Das DrawScope bietet mehrere nützliche Felder, z. B. size, ein Size-Objekt, das die aktuellen Abmessungen des DrawScope angibt.

Um etwas zu zeichnen, können Sie eine der vielen Zeichenfunktionen auf 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
    )
}

Rosa 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, benötigen Sie den Offset (x und y) und die Größe des Elements. Bei vielen der Zeichenmethoden für DrawScope werden Position und Größe durch Standardparameterwerte angegeben. Mit den Standardparametern wird das Element in der Regel am [0, 0]-Punkt auf dem Canvas positioniert und es wird eine Standard-size bereitgestellt, die den gesamten Zeichenbereich ausfüllt. Im Beispiel oben sehen Sie, dass das Rechteck oben links positioniert ist. Um die Größe und Position Ihres Elements anzupassen, müssen Sie das Koordinatensystem in Compose verstehen.

Der Ursprung des Koordinatensystems ([0,0]) befindet sich am Pixel ganz oben links im Zeichenbereich. x nimmt zu, wenn es sich nach rechts bewegt, und y nimmt zu, wenn es sich nach unten bewegt.

Ein Raster, das das Koordinatensystem mit der oberen linken Ecke [0, 0] und der unteren rechten Ecke [Breite, Höhe] zeigt
Abbildung 2: Koordinatensystem / Zeichenraster

Wenn Sie beispielsweise eine diagonale Linie von der oberen rechten Ecke des Zeichenbereichs zur unteren linken Ecke zeichnen möchten, können Sie die Funktion DrawScope.drawLine() verwenden und einen Start- und End-Offset 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, um zu ändern, wo oder wie die Zeichenbefehle ausgeführt werden.

Reichweite

Verwenden Sie DrawScope.scale(), um die Größe Ihrer Zeichenvorgänge um einen Faktor zu erhöhen. Vorgänge wie scale() werden auf alle Zeichenvorgänge innerhalb des entsprechenden Lambda angewendet. 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 Kreis, der nicht gleichmäßig skaliert wurde
Abbildung 3. Eine Skalierungsoperation 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 um 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. Anwenden eines Übersetzungsvorgangs auf einen Kreis auf dem Canvas.

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 Displays
Abbildung 5. Mit rotate() wird eine Drehung auf den aktuellen Zeichenbereich angewendet, wodurch das Rechteck um 45 Grad gedreht wird.

Eingebettet

Verwenden Sie DrawScope.inset(), um die Standardparameter des aktuellen DrawScope anzupassen. Dadurch werden die Zeichenbegrenzungen geändert und die Zeichnungen entsprechend verschoben:

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 effektiv ein Innenabstand hinzugefügt:

Ein Rechteck, das rundherum gepolstert wurde
Abbildung 6. Ein Inset auf Zeichenbefehle 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 das Ausführen von verschachtelten Aufrufen einzelner Transformationen, da alle Transformationen zusammen in einem einzigen Vorgang ausgeführt werden. Bei verschachtelten Transformationen muss Compose jede Transformation berechnen und speichern.

Mit dem folgenden Code werden beispielsweise sowohl eine Translation als auch eine Drehung auf das Rechteck angewendet:

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 an den Rand des Displays verschoben wurde
Abbildung 7. Mit withTransform werden sowohl eine Drehung als auch eine Translation angewendet. Das Rechteck wird also gedreht und nach links verschoben.

Häufige Zeichenvorgänge

Text zeichnen

Um Text in Compose zu zeichnen, können Sie in der Regel die zusammensetzbare Funktion Text verwenden. Wenn Sie sich jedoch in einem DrawScope befinden oder Ihren Text manuell mit Anpassungen zeichnen möchten, können Sie die Methode DrawScope.drawText() verwenden.

Um Text zu zeichnen, erstellen Sie mit rememberTextMeasurer ein TextMeasurer und rufen Sie drawText mit dem Messgerät auf:

val textMeasurer = rememberTextMeasurer()

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

Ein in Canvas gezeichnetes „Hallo“
Abbildung 8. Text auf Canvas zeichnen.

Text messen

Das Zeichnen von Text funktioniert etwas anders als andere Zeichenbefehle. Normalerweise geben Sie dem Zeichenbefehl die Größe (Breite und Höhe) an, mit der die Form oder das Bild gezeichnet werden soll. Bei Text gibt es einige Parameter, die die Größe des gerenderten Texts steuern, z. B. Schriftgröße, Schriftart, Ligaturen und Buchstabenabstand.

Mit Compose können Sie ein TextMeasurer verwenden, um je nach den oben genannten Faktoren auf die gemessene Größe von Text zuzugreifen. Wenn Sie einen Hintergrund hinter dem Text zeichnen möchten, können Sie die gemessenen Informationen verwenden, um die Größe des Bereichs zu ermitteln, 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 erstellt:

Mehrzeiliger Text, der ⅔ der Fläche einnimmt, mit einem Hintergrundrechteck
Abbildung 9. Mehrzeiliger Text, der ⅔ der Fläche einnimmt, mit einem Hintergrundrechteck.

Wenn Sie die Einschränkungen, die Schriftgröße oder eine andere Eigenschaft anpassen, die sich auf die gemessene Größe auswirkt, wird eine neue Größe gemeldet. Sie können eine feste Größe für width und height festlegen. Der Text folgt dann der festgelegten TextOverflow. Mit dem folgenden Code wird beispielsweise Text in einem Drittel der Höhe und einem Drittel der Breite des zusammensetzbaren Bereichs gerendert und TextOverflow auf TextOverflow.Ellipsis festgelegt:

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 mit Auslassungspunkten am Ende in den Einschränkungen dargestellt:

Text auf rosa Hintergrund, der durch Auslassungspunkte abgeschnitten wird.
Abbildung 10. TextOverflow.Ellipsis mit festen Einschränkungen für die Messung von Text.

Bild zeichnen

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

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

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

Ein mit Canvas gezeichnetes Bild eines Hundes
Abbildung 11: Zeichnen Sie ein ImageBitmap auf dem Canvas.

Grundformen zeichnen

Auf DrawScope gibt es viele Funktionen zum Zeichnen von Formen. Verwenden Sie zum Zeichnen einer Form eine der vordefinierten Zeichenfunktionen wie drawCircle:

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

API

Ausgabe

drawCircle()

Kreis zeichnen

drawRect()

draw_rect

drawRoundedRect()

Abgerundetes Rechteck zeichnen

drawLine()

Linie zeichnen

drawOval()

Oval zeichnen

drawArc()

Bogen zeichnen

drawPoints()

Punkte zeichnen

Pfad zeichnen

Ein Pfad ist eine Reihe von mathematischen 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. Sie können einen Pfad mit Funktionen wie lineTo() und moveTo() anhand der Größe des Zeichenbereichs generieren. 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 auf dem Kopf stehendes lila Pfaddreieck in Compose
Abbildung 12. Path in Compose erstellen und zeichnen

Auf das Canvas-Objekt zugreifen

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

Wenn Sie beispielsweise ein benutzerdefiniertes Drawable auf das Canvas zeichnen möchten, können Sie auf das Canvas zugreifen und Drawable#draw() aufrufen, wobei Sie 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 ovales schwarzes ShapeDrawable, das die volle Größe einnimmt
Abbildung 13. Auf die Zeichenfläche zugreifen, um ein Drawable zu zeichnen.

Weitere Informationen

Weitere Informationen zum Zeichnen in Compose finden Sie in den folgenden Ressourcen: