Grafiken in Compose

Viele Apps müssen in der Lage sein, genau zu steuern, was auf dem Bildschirm gezeichnet wird. Dies kann so klein sein wie die Platzierung eines Kästchens oder eines Kreises auf dem Bildschirm an der richtigen Stelle oder eine komplexe Anordnung von Grafikelementen in vielen verschiedenen Stilen.

Einfache Zeichnung mit Modifikatoren und DrawScope

Benutzerdefinierten Text lassen sich in „Compose“ hauptsächlich mit Modifizierern zeichnen, z. B. Modifier.drawWithContent, Modifier.drawBehind und Modifier.drawWithCache.

Wenn Sie beispielsweise etwas aus der zusammensetzbaren Funktion zeichnen möchten, können Sie den drawBehind-Modifikator verwenden, um mit der Ausführung von Zeichenbefehlen zu beginnen:

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

Wenn Sie nur eine zusammensetzbare Funktion benötigen, die etwas zeichnet, können Sie die zusammensetzbare Funktion Canvas verwenden. Die zusammensetzbare Funktion Canvas ist ein praktischer Wrapper um Modifier.drawBehind. Platzieren Sie das Canvas-Objekt in Ihrem Layout genauso wie andere Compose-UI-Elemente. In Canvas können Sie Elemente zeichnen und dabei ihren Stil und ihre Position genau steuern.

Alle Zeichenmodifikatoren stellen ein DrawScope bereit. Dies ist eine auf einen Bereich reduzierte Zeichenumgebung, die einen eigenen Status behält. So können Sie die Parameter für eine Gruppe grafischer Elemente festlegen. Das DrawScope bietet mehrere nützliche Felder, z. B. size, ein Size-Objekt, das die aktuellen Dimensionen von DrawScope festlegt.

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, gezeichnet auf einem weißen Hintergrund, der ein Viertel des Bildschirms einnimmt
Abbildung 1. Rechteck, das mit Canvas in „Compose“ gezeichnet wurde

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 die Position und Größe durch Standardparameterwerte bereitgestellt. 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 Zeichenbereich am oberen linken Rand des Pixels. x erhöht sich, wenn er nach rechts bewegt wird, und y steigt, wenn er nach unten bewegt 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 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 oder 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 werden scaleX beispielsweise um das 10-Fache und scaleY um das 15-Fache erhöht:

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

Ein ungleichförmig skalierter Kreis
Abbildung 3: Skalierungsvorgang auf einen Kreis auf Canvas anwenden

Übersetzen

Mit DrawScope.translate() können Sie Ihre 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.

Einsatz

Mit DrawScope.inset() können Sie die Standardparameter des aktuellen DrawScope anpassen, indem Sie die Begrenzungen der Zeichnung ändern und die Zeichnungen entsprechend übersetzen:

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 mit rundem Abstand.
Abbildung 6: Ein Einfügen 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 gemeinsam 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 Übersetzung 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 auf die Seite des Displays verschoben wurde
Abbildung 7. Mit withTransform können Sie sowohl eine Drehung als auch eine Verschiebung anwenden. Drehen Sie dazu das Rechteck und verschieben Sie es nach links.

Gängige Zeichenvorgänge

Text zeichnen

Um Text in „Schreiben“ zu zeichnen, können Sie normalerweise die zusammensetzbare Funktion Text verwenden. Wenn Sie sich jedoch in einem DrawScope befinden oder den Text manuell und 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 Measurer 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 unterscheidet sich etwas von anderen Zeichenbefehlen. Normalerweise geben Sie dem Zeichenbefehl die Größe (Breite und Höhe) zum Zeichnen der Form/des Bilds 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 „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 einen Hintergrund hinter dem Text 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()
)

Bei diesem Code-Snippet wird der Text mit einem rosa Hintergrund versehen:

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

Wenn Sie die Einschränkungen, die Schriftgröße oder andere Eigenschaften anpassen, die sich auf die gemessene Größe auswirken, wird eine neue Größe im Bericht angezeigt. 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 und 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:

Auf rosafarbenem Hintergrund gezeichneter Text mit Auslassungspunkten, die den Text abgeschnitten haben.
Abbildung 10: TextOverflow.Ellipsis mit festen Einschränkungen bei der Textmessung.

Bild zeichnen

Um ein ImageBitmap mit DrawScope 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 auf Canvas gezeichnetes Bild eines Hundes
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 wie 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()

Ziehpunkte

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 wird erstellt und gezeichnet.

Auf Canvas-Objekt zugreifen

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

Wenn Sie beispielsweise ein benutzerdefiniertes Drawable auf den Canvas zeichnen möchten, können Sie auf den Canvas zugreifen, Drawable#draw() aufrufen und das Objekt Canvas ü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 gesamte Größe einnimmt
Abbildung 13. Zugriff auf den Canvas, um ein Drawable zu zeichnen.

Weitere Informationen

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