Mit „Compose“ können Sie Formen erstellen, die aus Polygonen bestehen. Sie können beispielsweise die folgenden Arten von Formen erstellen:

Wenn Sie in Compose ein benutzerdefiniertes abgerundetes Polygon erstellen möchten, fügen Sie die graphics-shapes
-Abhängigkeit zu Ihrem app/build.gradle
hinzu:
implementation "androidx.graphics:graphics-shapes:1.0.1"
Mit dieser Bibliothek können Sie Formen erstellen, die aus Polygonen bestehen. Polygone haben nur gerade Kanten und scharfe Ecken. Bei diesen Formen sind jedoch optional abgerundete Ecken möglich. So lassen sich zwei unterschiedliche Formen ganz einfach ineinander überblenden. Das Morphen zwischen beliebigen Formen ist schwierig und in der Regel ein Problem, das während der Designphase auftritt. Diese Bibliothek vereinfacht das Morphen zwischen diesen Formen mit ähnlichen polygonalen Strukturen.
Polygone erstellen
Mit dem folgenden Snippet wird eine einfache Polygonform mit sechs Punkten in der Mitte des Zeichenbereichs erstellt:
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() )

In diesem Beispiel erstellt die Bibliothek ein RoundedPolygon
, das die Geometrie der angeforderten Form enthält. Wenn Sie diese Form in einer Compose-App zeichnen möchten, müssen Sie ein Path
-Objekt daraus abrufen, damit die Form in einem Format vorliegt, das Compose zeichnen kann.
Ecken eines Polygons runden
Verwenden Sie den Parameter CornerRounding
, um die Ecken eines Polygons abzurunden. Diese Funktion verwendet zwei Parameter: radius
und smoothing
. Jede abgerundete Ecke besteht aus ein bis drei kubischen Kurven. Die Mitte hat die Form eines Kreisbogens, während die beiden seitlichen Kurven vom Rand der Form zur Mittelkurve übergehen.
Radius
radius
ist der Radius des Kreises, der zum Runden einer Ecke verwendet wird.
Das folgende Dreieck mit abgerundeten Ecken wird beispielsweise so erstellt:


r
bestimmt die Größe der kreisförmigen Rundung von abgerundeten Ecken.Glättung
Die Glättung ist ein Faktor, der bestimmt, wie lange es dauert, bis die Ecke vom kreisförmigen Rundungsabschnitt zur Kante übergeht. Ein Glättungsfaktor von 0 (nicht geglättet, der Standardwert für CornerRounding
) führt zu einer rein kreisförmigen Eckenrundung. Ein Glättungsfaktor ungleich null (bis maximal 1,0) führt dazu, dass die Ecke durch drei separate Kurven abgerundet wird.


Im folgenden Snippet wird beispielsweise der subtile Unterschied zwischen dem Festlegen von „smoothing“ auf 0 und 1 veranschaulicht:
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) )

Größe und Position
Standardmäßig wird eine Form mit einem Radius von 1
um den Mittelpunkt (0, 0
) erstellt. Dieser Radius entspricht dem Abstand zwischen dem Mittelpunkt und den äußeren Eckpunkten des Polygons, auf dem die Form basiert. Durch das Abrunden der Ecken wird die Form kleiner, da die abgerundeten Ecken näher am Mittelpunkt liegen als die abgerundeten Eckpunkte. Wenn Sie die Größe eines Polygons anpassen möchten, ändern Sie den Wert von radius
. Wenn Sie die Position anpassen möchten, ändern Sie die centerX
- oder centerY
-Werte des Polygons.
Alternativ können Sie das Objekt transformieren, um seine Größe, Position und Drehung mit Standard-DrawScope
-Transformationsfunktionen wie DrawScope#translate()
zu ändern.
Formen morphen
Ein Morph
-Objekt ist eine neue Form, die eine Animation zwischen zwei polygonalen Formen darstellt. Wenn Sie zwischen zwei Formen morphen möchten, erstellen Sie zwei RoundedPolygons
-Objekte und ein Morph
-Objekt, das diese beiden Formen verwendet. Wenn Sie eine Form zwischen der Start- und der Endform berechnen möchten, geben Sie einen progress
-Wert zwischen 0 und 1 an, um die Form zwischen der Startform (0) und der Endform (1) zu bestimmen:
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 liegt der Fortschritt genau auf halbem Weg zwischen den beiden Formen (abgerundetes Dreieck und Quadrat), was zu folgendem Ergebnis führt:

In den meisten Fällen wird das Morphen als Teil einer Animation und nicht nur als statisches Rendering durchgeführt. Um zwischen diesen beiden zu animieren, können Sie die Standard-Animations-APIs in Compose verwenden, um den Fortschrittswert im Laufe der Zeit zu ändern. Sie können beispielsweise die Morphing-Animation zwischen diesen beiden Formen unendlich oft wiederholen:
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() )

Polygon als Clip verwenden
In Compose wird häufig der Modifier clip
verwendet, um die Darstellung einer Composable-Funktion zu ändern und Schatten zu nutzen, die um den Clipping-Bereich gezeichnet werden:
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 Folgendem:

Das sieht vielleicht nicht viel anders aus als zuvor, ermöglicht aber die Nutzung anderer Funktionen in Compose. Mit dieser Technik lässt sich beispielsweise ein Bild zuschneiden und ein Schatten um den zugeschnittenen Bereich legen:
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) ) }

Schaltfläche bei Klick morphen
Mit der graphics-shape
-Bibliothek können Sie eine Schaltfläche erstellen, die sich beim Drücken zwischen zwei Formen ändert. Erstellen Sie zuerst ein MorphPolygonShape
, das Shape
erweitert und es skaliert und übersetzt, damit es richtig passt. Beachten Sie, dass der Fortschritt übergeben wird, 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) } }
Um diese Morph-Form zu verwenden, erstellen Sie zwei Polygone, shapeA
und shapeB
. Erstellen Sie die Morph
und merken Sie sie sich. Wenden Sie dann den Morph als Clipumriss auf die Schaltfläche an. Verwenden Sie dazu die interactionSource
-Schaltfläche als Grundlage für die 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 die folgende Animation angezeigt:

Formenmorphing unendlich oft animieren
Wenn Sie eine Morphing-Form endlos animieren möchten, verwenden Sie rememberInfiniteTransition
.
Unten sehen Sie ein Beispiel für ein Profilbild, dessen Form sich im Laufe der Zeit unendlich oft ändert (und das sich dreht). 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 Ergebnis:

Benutzerdefinierte Polygone
Wenn Formen, die aus regulären Polygonen erstellt wurden, nicht Ihren Anforderungen entsprechen, können Sie eine benutzerdefinierte Form mit einer Liste von Eckpunkten erstellen. Sie möchten beispielsweise eine Herzform wie diese erstellen:

Sie können die einzelnen Eckpunkte dieser Form mit der RoundedPolygon
-Überladung angeben, die ein Float-Array mit x- und y-Koordinaten akzeptiert.
Um das Herz-Polygon aufzuschlüsseln, sehen Sie sich das Polarkoordinatensystem an. Damit ist es einfacher, Punkte anzugeben, als mit dem kartesischen (x,y)-Koordinatensystem, bei dem 0°
auf der rechten Seite beginnt und im Uhrzeigersinn verläuft, mit 270°
an der 12-Uhr-Position:

Die Form kann jetzt einfacher definiert werden, indem für jeden Punkt der Winkel (𝜭) und der Radius vom Mittelpunkt angegeben werden:

Die Knoten können jetzt erstellt und an die Funktion RoundedPolygon
ü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 mit dieser radialToCartesian
-Funktion in kartesische Koordinaten übersetzt werden:
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 liefert die Roh-Vertices für das Herz. Sie müssen jedoch bestimmte Ecken abrunden, um die gewünschte Herzform zu erhalten. Die Ecken bei 90°
und 270°
sind nicht abgerundet, die anderen Ecken aber schon. Wenn Sie benutzerdefinierte Rundungen für einzelne Ecken festlegen möchten, verwenden Sie den Parameter perVertexRounding
:
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 das rosafarbene Herz:

Wenn die oben genannten Formen Ihren Anwendungsfall nicht abdecken, können Sie mit der Klasse Path
eine benutzerdefinierte Form zeichnen oder eine ImageVector
-Datei von der Festplatte laden. Die graphics-shapes
-Bibliothek ist nicht für beliebige Formen vorgesehen, sondern soll speziell die Erstellung von abgerundeten Polygonen und Morph-Animationen zwischen ihnen vereinfachen.
Zusätzliche Ressourcen
Weitere Informationen und Beispiele finden Sie in den folgenden Ressourcen:
- Blog: The Shape of Things to Come - Shapes
- Blog: Shape morphing in Android
- GitHub-Demonstration für Formen