Formen in Compose

Mit der Funktion „Schreiben“ können Sie Formen erstellen, die aus Polygonen bestehen. Beispiel: können Sie folgende Formen erstellen:

Blaues Sechseck in der Mitte des Zeichenbereichs
Abbildung 1: Beispiele für verschiedene Formen, die Sie mit Grafikformen erstellen können Mediathek

Um in „Schreiben“ ein benutzerdefiniertes abgerundetes Polygon zu erstellen, fügen Sie den graphics-shapes-Abhängigkeit zu Ihrem app/build.gradle:

implementation "androidx.graphics:graphics-shapes:1.0.1"

Mit dieser Bibliothek können Sie Formen aus Polygonen erstellen. Polygonale Formen haben nur gerade Kanten und scharfe Ecken, aber optional auch abgerundete Ecken. So lässt sich ganz einfach zwischen zwei verschiedenen Formen wechseln. Die Veränderung zwischen willkürlichen Formen ist schwierig Design-Zeit. Diese Bibliothek macht es einfach, indem zwischen diesen mit ähnlichen polygonalen Strukturen.

Polygone erstellen

Mit dem folgenden Snippet wird ein grundlegendes Polygon mit sechs Punkten in der Mitte erstellt. des Zeichenbereichs:

Box(
    modifier = Modifier
        .drawWithCache {
            val roundedPolygon = RoundedPolygon(
                numVertices = 6,
                radius = size.minDimension / 2,
                centerX = size.width / 2,
                centerY = size.height / 2
            )
            val roundedPolygonPath = roundedPolygon.toPath().asComposePath()
            onDrawBehind {
                drawPath(roundedPolygonPath, color = Color.Blue)
            }
        }
        .fillMaxSize()
)

Blaues Sechseck in der Mitte des Zeichenbereichs
Abbildung 2: Blaues Sechseck in der Mitte des Zeichenbereichs.

In diesem Beispiel erstellt die Bibliothek ein RoundedPolygon, das die Geometrie enthält. die die angeforderte Form darstellt. Um diese Form in einer Schreib-App zu zeichnen, müssen Sie ein Path-Objekt daraus abrufen, um die Form wie man zeichnet.

Ecken eines Polygons abrunden

Verwenden Sie den Parameter CornerRounding, um die Ecken eines Polygons abzurunden. Dieses Für sind zwei Parameter erforderlich: radius und smoothing. Jede abgerundete Ecke besteht aus 1–3 kubischen Kurven, deren Mitte einen Kreisbogen hat, während die beiden seitlichen („flankierenden“) Kurven von der Kante der Form zur Mittelkurve übergehen.

Radius

radius ist der Radius des Kreises, mit dem ein Eckpunkt gerundet wird.

Das folgende Beispiel zeigt das folgende abgerundete Eckdreieck:

Dreieck mit abgerundeten Ecken
Abbildung 3: Dreieck mit abgerundeten Ecken.
Der Radius „r“ bestimmt die Größe der abgerundeten Ecken.
Abbildung 4. Der Rundungsradius r bestimmt die Rundungsgröße von abgerundete Ecken.

Glättung

Die Glättung ist ein Faktor, der bestimmt, wie lange es dauert, vom kreisförmigen Rundungsbereich der Ecke bis zum Rand zu gelangen. einen Glättungsfaktor von 0 (ungeglättet, der Standardwert für CornerRounding) ergibt eine rein kreisförmige Anzeige. Ecken abgerundet werden. Ein Glättungsfaktor ungleich null (bis zur maximalen Anzahl von 1,0) führt zu wird die Ecke durch drei separate Kurven abgerundet.

Ein Glättungsfaktor von 0 (ungeglättet) erzeugt eine einzelne kubische Kurve, die
die einem Kreis um die Ecke mit dem angegebenen Rundungsradius folgen, wie im
früheres Beispiel
Abbildung 5. Bei einem Glättungsfaktor von 0 (nicht geglättet) wird eine einzelne kubische Kurve erzeugt, die wie im vorherigen Beispiel einem Kreis um die Ecke mit dem angegebenen Rundungsradius folgt.
Bei einem nicht nullwertigen Glättungsfaktor werden drei kubische Kurven verwendet, um den Eckpunkt zu runden: die innere kreisförmige Kurve (wie zuvor) sowie zwei flankierende Kurven, die den Übergang zwischen der inneren Kurve und den Polygonkanten bilden.
Abbildung 6. Ein Glättungsfaktor ungleich null erzeugt drei kubische Kurven zum Runden. den Scheitelpunkt: die innere kreisförmige Kurve (wie zuvor) plus zwei flankierende Kurven, die zwischen der inneren Kurve und den Polygonkanten.

Das folgende Snippet veranschaulicht beispielsweise den feinen Unterschied bei der Einstellung Glättung auf 0 gegen 1:

Box(
    modifier = Modifier
        .drawWithCache {
            val roundedPolygon = RoundedPolygon(
                numVertices = 3,
                radius = size.minDimension / 2,
                centerX = size.width / 2,
                centerY = size.height / 2,
                rounding = CornerRounding(
                    size.minDimension / 10f,
                    smoothing = 0.1f
                )
            )
            val roundedPolygonPath = roundedPolygon.toPath().asComposePath()
            onDrawBehind {
                drawPath(roundedPolygonPath, color = Color.Black)
            }
        }
        .size(100.dp)
)

Zwei schwarze Dreiecke, die den Unterschied im Glättungsparameter zeigen
Abbildung 7. Zwei schwarze Dreiecke, die den Unterschied in den Glättungsparametern darstellen.

Größe und Position

Standardmäßig wird eine Form mit einem Radius von 1 um den Mittelpunkt (0, 0) herum erstellt. Dieser Radius entspricht der Entfernung zwischen dem Mittelpunkt und den äußeren Eckpunkten des Polygons, auf dem die Form basiert. Beachten Sie, dass die Form durch das Abrunden der Ecken kleiner wird, da die abgerundeten Ecken näher am Mittelpunkt liegen als die abgerundeten Eckpunkte. Um die Größe eines Polygons zu ändern, passen Sie den Wert für radius an. Wenn Sie die Position anpassen möchten, ändern Sie centerX oder centerY des Polygons. Alternativ können Sie das Objekt mithilfe von standardmäßigen DrawScope-Transformationsfunktionen wie DrawScope#translate() transformieren, um Größe, Position und Drehung zu ändern.

Formen morphen

Ein Morph-Objekt ist eine neue Form, die eine Animation zwischen zwei polygonalen Formen darstellt. Wenn du zwischen zwei Formen verwandeln möchtest, musst du zwei RoundedPolygons und einen Morph erstellen das diese beiden Formen annimmt. Um eine Form zwischen dem Start- und Endformen verwenden, geben Sie einen progress-Wert zwischen 0 und 1 an, um dessen zwischen den Anfangsformen (0) und den Endformen (1) platzieren:

Box(
    modifier = Modifier
        .drawWithCache {
            val triangle = RoundedPolygon(
                numVertices = 3,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f,
                rounding = CornerRounding(
                    size.minDimension / 10f,
                    smoothing = 0.1f
                )
            )
            val square = RoundedPolygon(
                numVertices = 4,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f
            )

            val morph = Morph(start = triangle, end = square)
            val morphPath = morph
                .toPath(progress = 0.5f).asComposePath()

            onDrawBehind {
                drawPath(morphPath, color = Color.Black)
            }
        }
        .fillMaxSize()
)

Im obigen Beispiel ist der Fortschritt genau auf halbem Weg zwischen den beiden Formen (abgerundetes Dreieck und Quadrat), was zu folgendem Ergebnis führt:

50% der Strecke zwischen einem abgerundeten Dreieck und einem Quadrat
Abbildung 8. 50 % zwischen einem abgerundeten Dreieck und einem Quadrat.

In den meisten Fällen wird das Morphing als Teil einer Animation und nicht nur als statisches Rendering ausgeführt. Wenn Sie zwischen diesen beiden Status wechseln möchten, können Sie die standardmäßigen Animation APIs in Compose verwenden, um den Fortschrittswert im Zeitverlauf zu ändern. So können Sie beispielsweise die Morphing-Animation zwischen diesen beiden Formen unendlich wiedergeben:

val infiniteAnimation = rememberInfiniteTransition(label = "infinite animation")
val morphProgress = infiniteAnimation.animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        tween(500),
        repeatMode = RepeatMode.Reverse
    ),
    label = "morph"
)
Box(
    modifier = Modifier
        .drawWithCache {
            val triangle = RoundedPolygon(
                numVertices = 3,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f,
                rounding = CornerRounding(
                    size.minDimension / 10f,
                    smoothing = 0.1f
                )
            )
            val square = RoundedPolygon(
                numVertices = 4,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f
            )

            val morph = Morph(start = triangle, end = square)
            val morphPath = morph
                .toPath(progress = morphProgress.value)
                .asComposePath()

            onDrawBehind {
                drawPath(morphPath, color = Color.Black)
            }
        }
        .fillMaxSize()
)

Unendliche Transformation zwischen einem Quadrat und einem abgerundeten Dreieck
Abbildung 9. Unendliche Transformation zwischen einem Quadrat und einem abgerundeten Dreieck.

Polygon als Clip verwenden

In Compose wird häufig der Modifikator clip verwendet, um die Darstellung eines Composeables zu ändern und Schatten um den Zuschneidebereich zu zeichnen:

fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) }
class RoundedPolygonShape(
    private val polygon: RoundedPolygon,
    private var matrix: Matrix = Matrix()
) : Shape {
    private var path = Path()
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        path.rewind()
        path = polygon.toPath().asComposePath()
        matrix.reset()
        val bounds = polygon.getBounds()
        val maxDimension = max(bounds.width, bounds.height)
        matrix.scale(size.width / maxDimension, size.height / maxDimension)
        matrix.translate(-bounds.left, -bounds.top)

        path.transform(matrix)
        return Outline.Generic(path)
    }
}

Sie können das Polygon dann als Clip verwenden, wie im folgenden Snippet gezeigt:

val hexagon = remember {
    RoundedPolygon(
        6,
        rounding = CornerRounding(0.2f)
    )
}
val clip = remember(hexagon) {
    RoundedPolygonShape(polygon = hexagon)
}
Box(
    modifier = Modifier
        .clip(clip)
        .background(MaterialTheme.colorScheme.secondary)
        .size(200.dp)
) {
    Text(
        "Hello Compose",
        color = MaterialTheme.colorScheme.onSecondary,
        modifier = Modifier.align(Alignment.Center)
    )
}

Das führt zu folgenden Ergebnissen:

Sechseck mit dem Text „Hallo E-Mail“ in der Mitte.
Abbildung 10: Sechseck mit dem Text „Hello Compose“ in der Mitte.

Das Aussehen unterscheidet sich vielleicht nicht so sehr vom vorherigen Rendering, ermöglicht aber für die Nutzung anderer Funktionen in Compose. Mit dieser Technik können Sie beispielsweise ein Bild zuschneiden und einen Schatten um den zugeschnittenen Bereich anwenden:

val hexagon = remember {
    RoundedPolygon(
        6,
        rounding = CornerRounding(0.2f)
    )
}
val clip = remember(hexagon) {
    RoundedPolygonShape(polygon = hexagon)
}
Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center
) {
    Image(
        painter = painterResource(id = R.drawable.dog),
        contentDescription = "Dog",
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .graphicsLayer {
                this.shadowElevation = 6.dp.toPx()
                this.shape = clip
                this.clip = true
                this.ambientShadowColor = Color.Black
                this.spotShadowColor = Color.Black
            }
            .size(200.dp)

    )
}

Hund in Sechseck mit Schatten an den Kanten
Abbildung 11. Benutzerdefinierte Form als Clip angewendet.

Morph-Schaltfläche beim Klicken

Mit der graphics-shape-Bibliothek können Sie eine Schaltfläche erstellen, die beim Drücken zwischen zwei Formen wechselt. Erstellen Sie zuerst eine MorphPolygonShape, die Shape erweitert, zu skalieren und zu übersetzen. Beachten Sie die Übergabe des damit die Form animiert werden kann:

class MorphPolygonShape(
    private val morph: Morph,
    private val percentage: Float
) : Shape {

    private val matrix = Matrix()
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f
        // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y.
        matrix.scale(size.width / 2f, size.height / 2f)
        matrix.translate(1f, 1f)

        val path = morph.toPath(progress = percentage).asComposePath()
        path.transform(matrix)
        return Outline.Generic(path)
    }
}

Erstellen Sie zur Verwendung dieser Morph-Form die beiden Polygone shapeA und shapeB. Erstellen Sie Morph und merken Sie sich die ID. Wenden Sie dann das Morphing als Clip-Umriss auf die Schaltfläche an und verwenden Sie die interactionSource beim Drücken als treibende Kraft hinter der Animation:

val shapeA = remember {
    RoundedPolygon(
        6,
        rounding = CornerRounding(0.2f)
    )
}
val shapeB = remember {
    RoundedPolygon.star(
        6,
        rounding = CornerRounding(0.1f)
    )
}
val morph = remember {
    Morph(shapeA, shapeB)
}
val interactionSource = remember {
    MutableInteractionSource()
}
val isPressed by interactionSource.collectIsPressedAsState()
val animatedProgress = animateFloatAsState(
    targetValue = if (isPressed) 1f else 0f,
    label = "progress",
    animationSpec = spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium)
)
Box(
    modifier = Modifier
        .size(200.dp)
        .padding(8.dp)
        .clip(MorphPolygonShape(morph, animatedProgress.value))
        .background(Color(0xFF80DEEA))
        .size(200.dp)
        .clickable(interactionSource = interactionSource, indication = null) {
        }
) {
    Text("Hello", modifier = Modifier.align(Alignment.Center))
}

Wenn auf das Feld getippt wird, wird folgende Animation angezeigt:

Morph wird als Klick zwischen zwei Formen angewendet
Abbildung 12. Morph wird als Klick zwischen zwei Formen angewendet.

Formen unendlich morphen

Wenn Sie eine Morph-Form unendlich animieren möchten, verwenden Sie rememberInfiniteTransition. Unten sehen Sie ein Beispiel für ein Profilbild, das sich im Laufe der Zeit unendlich oft in Form und Ausrichtung ändert. Bei diesem Ansatz wird eine kleine Anpassung an der oben gezeigten MorphPolygonShape vorgenommen:

class CustomRotatingMorphShape(
    private val morph: Morph,
    private val percentage: Float,
    private val rotation: Float
) : Shape {

    private val matrix = Matrix()
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f
        // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y.
        matrix.scale(size.width / 2f, size.height / 2f)
        matrix.translate(1f, 1f)
        matrix.rotateZ(rotation)

        val path = morph.toPath(progress = percentage).asComposePath()
        path.transform(matrix)

        return Outline.Generic(path)
    }
}

@Preview
@Composable
private fun RotatingScallopedProfilePic() {
    val shapeA = remember {
        RoundedPolygon(
            12,
            rounding = CornerRounding(0.2f)
        )
    }
    val shapeB = remember {
        RoundedPolygon.star(
            12,
            rounding = CornerRounding(0.2f)
        )
    }
    val morph = remember {
        Morph(shapeA, shapeB)
    }
    val infiniteTransition = rememberInfiniteTransition("infinite outline movement")
    val animatedProgress = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            tween(2000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "animatedMorphProgress"
    )
    val animatedRotation = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            tween(6000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "animatedMorphProgress"
    )
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Image(
            painter = painterResource(id = R.drawable.dog),
            contentDescription = "Dog",
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .clip(
                    CustomRotatingMorphShape(
                        morph,
                        animatedProgress.value,
                        animatedRotation.value
                    )
                )
                .size(200.dp)
        )
    }
}

Dieser Code liefert das folgende amüsante Ergebnis:

Herzform
Abbildung 13. Profilbild, das von einer rotierenden, wellenförmigen Form abgeschnitten wird.

Benutzerdefinierte Polygone

Wenn Formen, die aus regelmäßigen Polygonen erstellt wurden, Ihren Anwendungsfall nicht abdecken, können Sie um eine benutzerdefinierte Form mit einer Liste von Scheitelpunkten zu erstellen. Sie können beispielsweise eine Herzform wie diese erstellen:

Herzform
Abbildung 14. Herzform.

Sie können die einzelnen Eckpunkte dieser Form mit der Überladung von RoundedPolygon angeben, die ein Float-Array mit X‑ und Y‑Koordinaten annimmt.

Um das Herzpolygon aufzuschlüsseln, sehen Sie, dass das Polarkoordinatensystem für die Angabe von Punkten macht dies einfacher als die Verwendung der kartesischen Koordinate (x,y). Dabei beginnt auf der rechten Seite und fährt im Uhrzeigersinn fort, wobei 270° auf 12 Uhr:

Herzform
Abbildung 15. Herzform mit Koordinaten.

Die Form kann jetzt einfacher definiert werden, indem der Winkel (Θ) und Radius vom Mittelpunkt an jedem Punkt:

Herzform
Abbildung 16. Herzform mit Koordinaten, ohne Rundung.

Die Eckpunkte können jetzt erstellt und an die RoundedPolygon-Funktion übergeben werden:

val vertices = remember {
    val radius = 1f
    val radiusSides = 0.8f
    val innerRadius = .1f
    floatArrayOf(
        radialToCartesian(radiusSides, 0f.toRadians()).x,
        radialToCartesian(radiusSides, 0f.toRadians()).y,
        radialToCartesian(radius, 90f.toRadians()).x,
        radialToCartesian(radius, 90f.toRadians()).y,
        radialToCartesian(radiusSides, 180f.toRadians()).x,
        radialToCartesian(radiusSides, 180f.toRadians()).y,
        radialToCartesian(radius, 250f.toRadians()).x,
        radialToCartesian(radius, 250f.toRadians()).y,
        radialToCartesian(innerRadius, 270f.toRadians()).x,
        radialToCartesian(innerRadius, 270f.toRadians()).y,
        radialToCartesian(radius, 290f.toRadians()).x,
        radialToCartesian(radius, 290f.toRadians()).y,
    )
}

Die Eckpunkte müssen mithilfe dieser Methode in kartesische Koordinaten umgewandelt werden. radialToCartesian-Funktion:

internal fun Float.toRadians() = this * PI.toFloat() / 180f

internal val PointZero = PointF(0f, 0f)
internal fun radialToCartesian(
    radius: Float,
    angleRadians: Float,
    center: PointF = PointZero
) = directionVectorPointF(angleRadians) * radius + center

internal fun directionVectorPointF(angleRadians: Float) =
    PointF(cos(angleRadians), sin(angleRadians))

Der vorherige Code enthält die Rohvektoren für das Herz, aber Sie müssen bestimmte Ecken abrunden, um die gewünschte Herzform zu erhalten. Die Ecken bei 90° und 270° sind nicht abgerundet, die anderen Ecken aber schon. Verwenden Sie den Parameter perVertexRounding, um benutzerdefinierte Rundungen für einzelne Ecken zu erzielen:

val rounding = remember {
    val roundingNormal = 0.6f
    val roundingNone = 0f
    listOf(
        CornerRounding(roundingNormal),
        CornerRounding(roundingNone),
        CornerRounding(roundingNormal),
        CornerRounding(roundingNormal),
        CornerRounding(roundingNone),
        CornerRounding(roundingNormal),
    )
}

val polygon = remember(vertices, rounding) {
    RoundedPolygon(
        vertices = vertices,
        perVertexRounding = rounding
    )
}
Box(
    modifier = Modifier
        .drawWithCache {
            val roundedPolygonPath = polygon.toPath().asComposePath()
            onDrawBehind {
                scale(size.width * 0.5f, size.width * 0.5f) {
                    translate(size.width * 0.5f, size.height * 0.5f) {
                        drawPath(roundedPolygonPath, color = Color(0xFFF15087))
                    }
                }
            }
        }
        .size(400.dp)
)

Daraus ergibt sich ein rosa Herz:

Herzform
Abbildung 17. Herzform als Ergebnis.

Wenn die vorherigen Formen nicht für Ihren Anwendungsfall geeignet sind, können Sie die Klasse Path verwenden, um eine benutzerdefinierte Form zu zeichnen, oder eine ImageVector-Datei von der Festplatte laden. Die graphics-shapes-Bibliothek ist nicht für beliebige Zwecke vorgesehen um die Erstellung von abgerundeten Polygonen zu vereinfachen. Morphen von Animationen zwischen ihnen.

Weitere Informationen

Weitere Informationen und Beispiele finden Sie in den folgenden Ressourcen: