Grafiken in Compose

Bei vielen Apps muss genau gesteuert werden können, was auf dem Bildschirm gezeichnet wird. Das kann so einfach sein wie das Platzieren eines Quadrats oder Kreises an der richtigen Stelle auf dem Bildschirm oder eine ausgefeilte Anordnung von grafischen Elementen in vielen verschiedenen Stilen.

Einfache Zeichnung mit Modifikatoren und DrawScope

Die Hauptmethode zum Zeichnen benutzerdefinierter Elemente in Compose sind Modifikatoren wie Modifier.drawWithContent, Modifier.drawBehind und Modifier.drawWithCache.

Wenn Sie beispielsweise etwas hinter Ihrem Composeable zeichnen möchten, können Sie mit der Tastenkombination drawBehind Zeichenbefehle ausführen:

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

Wenn Sie nur ein grafisches Element benötigen, können Sie das Canvas-Element verwenden. Das Canvas-Komposit ist eine praktische Ummantelung für Modifier.drawBehind. Sie platzieren das Canvas in Ihrem Layout genauso wie jedes andere Compose-UI-Element. In der Canvas können Sie Elemente zeichnen und dabei Stil und Position genau festlegen.

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

Um etwas zu 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
    )
}

Rosa Rechteck auf weißem Hintergrund, das ein Viertel des Bildschirms einnimmt
Abbildung 1: Rechteck, das mit Canvas in „Compose“ gezeichnet wurde

Weitere Informationen zu den 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 durch Standardparameterwerte angegeben. Die Standardparameter positionieren das Element im Allgemeinen am Punkt [0, 0] des Canvas und stellen einen Standard-size bereit, der den gesamten Zeichenbereich ausfüllt (siehe Beispiel oben). Das Rechteck befindet sich oben links. Um die Größe und Position eines Elements anzupassen, müssen Sie das Koordinatensystem in Compose verstehen.

Der Ursprung des Koordinatensystems ([0,0]) befindet sich im linken oberen Pixel des Zeichenbereichs. x nimmt zu, wenn der Wert nach rechts und y zunimmt, wenn der Wert nach unten verschoben wird.

Ein Raster mit dem Koordinatensystem oben links [0, 0] und unten rechts [Breite, Höhe].
Abbildung 2: Koordinatensystem zeichnen / Raster zeichnen.

Wenn Sie beispielsweise eine diagonale Linie von der oberen rechten Ecke des Canvas-Bereichs bis zur unteren linken 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 oder wie die Zeichenbefehle ausgeführt werden.

Reichweite

Mit DrawScope.scale() können Sie die Größe Ihrer Zeichenvorgänge um einen bestimmten Faktor erhöhen. Vorgänge wie scale() werden auf alle Zeichenvorgänge innerhalb des entsprechenden Lambdas angewendet. Im folgenden Code wird scaleX beispielsweise zehnmal und scaleY fünfzehnmal erhöht:

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

Ein nicht einheitlich skalierter Kreis
Abbildung 3. Skalierungsvorgang auf einen Kreis in Canvas anwenden

Übersetzen

Mit DrawScope.translate() können Sie die Zeichenvorgänge nach oben, unten, links oder rechts verschieben. Durch den 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 aus der Mitte verschoben wurde
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 Displays
Abbildung 5: Wir verwenden rotate(), um eine Drehung auf den aktuellen Zeichenbereich anzuwenden, wodurch das Rechteck um 45 Grad gedreht wird.

Eingebettet

Mit DrawScope.inset() können Sie die Standardparameter des aktuellen DrawScope anpassen, die Zeichnungsgrenzen ändern und die Zeichnungen entsprechend verschieben:

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 Padding hinzugefügt:

Ein Rechteck, das rundum mit einem Abstand versehen ist
Abbildung 6. Eingefügte Elemente auf Zeichenbefehle anwenden

Mehrere Transformationen

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

Mit dem folgenden Code werden beispielsweise sowohl eine Verschiebung 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 zur Seite des Displays verschoben ist
Abbildung 7. Mit withTransform können Sie sowohl eine Drehung als auch eine Verschiebung vornehmen, also das Rechteck drehen und nach links verschieben.

Gängige Zeichenvorgänge

Text zeichnen

Wenn Sie in Compose Text zeichnen möchten, können Sie in der Regel das Text-Element verwenden. Wenn Sie sich jedoch in einem DrawScope befinden oder den Text manuell mit Anpassungen zeichnen möchten, können Sie die Methode DrawScope.drawText() verwenden.

Wenn Sie Text zeichnen möchten, erstellen Sie mit rememberTextMeasurer eine TextMeasurer und rufen Sie drawText mit dem Messwert auf:

val textMeasurer = rememberTextMeasurer()

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

Auf Canvas gezeichnete 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, in der die Form/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 der Funktion „Schreiben“ können Sie einen TextMeasurer verwenden, um abhängig von den oben genannten Faktoren Zugriff auf die gemessene Textgröße zu erhalten. Wenn Sie hinter dem Text einen Hintergrund zeichnen möchten, können Sie anhand der gemessenen Informationen die Größe des Bereichs 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 erzeugt:

Mehrzeiliger Text, der ⅔ des gesamten Bereichs einnimmt, mit einem Hintergrund-Rechteck
Abbildung 9. Mehrzeiliger Text, der ⅔ des gesamten Bereichs einnimmt, mit einem Hintergrund-Rechteck.

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 erfasst. Sie können sowohl für width als auch für height eine feste Größe festlegen. Der Text folgt dann der festgelegten TextOverflow. Mit dem folgenden Code wird beispielsweise Text in ⅓ der Höhe und ⅓ 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 innerhalb der Begrenzungen mit Auslassungspunkten am Ende dargestellt:

Text auf rosa Hintergrund, der durch Auslassungspunkte abgeschnitten wird.
Abbildung 10. TextOverflow.Ellipsis mit festen Einschränkungen bei der Textmessung.

Bild zeichnen

Wenn Sie mit DrawScope ein ImageBitmap zeichnen möchten, 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 auf Canvas gezeichnetes Bild eines Hundes
Abbildung 11. Zeichnen eines ImageBitmap auf Canvas

Grundformen zeichnen

In 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()

draw rounded rect

drawLine()

draw line

drawOval()

Oval zeichnen

drawArc()

Bogen zeichnen

drawPoints()

Punkte zeichnen

Pfad zeichnen

Ein Pfad ist eine 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. 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 lilafarbenes Dreieck mit dem Pfad „Schreiben“
Abbildung 12. Path in Compose erstellen und zeichnen

Zugriff auf Canvas-Objekt

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 haben, das Sie auf dem Canvas zeichnen möchten, können Sie auf den 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. Zugriff auf die Zeichenfläche, um ein Drawable zu zeichnen

Weitere Informationen

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