Mit Compose können Sie Formen erstellen, die aus Polygonen bestehen. Beispiele für Formen, die Sie erstellen können:
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. Polygonale Formen haben nur gerade Kanten und scharfe Ecken, diese Formen ermöglichen jedoch optional abgerundete Ecken. So können Sie ganz einfach zwischen zwei verschiedenen Formen wechseln. Das Morphing zwischen beliebigen Formen ist schwierig und in der Regel ein Problem bei der Gestaltung. Mit dieser Bibliothek ist es jedoch ganz einfach, zwischen diesen Formen mit ähnlichen polygonalen Strukturen zu wechseln.
Polygone erstellen
Mit dem folgenden Snippet wird eine einfache Polygonform mit 6 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 ein Format umgewandelt wird, das Compose zeichnen kann.
Ecken eines Polygons abrunden
Verwenden Sie den Parameter CornerRounding, um die Ecken eines Polygons abzurunden. Dieser Parameter verwendet zwei Parameter: radius und smoothing. Jede abgerundete Ecke besteht aus 1 bis 3 kubischen Kurven. Die Mitte hat eine kreisförmige Bogenform, während die beiden seitlichen Kurven vom Rand der Form zur Mittelkurve übergehen.
Radius
Der radius ist der Radius des Kreises, der zum Abrunden eines Eckpunkts verwendet wird.
Das folgende Dreieck mit abgerundeten Ecken wird beispielsweise so erstellt:
r bestimmt die Größe der kreisförmigen Abrundung der abgerundeten Ecken.Glättung
Die Glättung ist ein Faktor, der bestimmt, wie lange es dauert, vom kreisförmigen Abrundungsbereich der Ecke zum Rand zu gelangen. Ein Glättungsfaktor von 0 (nicht geglättet, der Standardwert für CornerRounding) führt zu einer rein kreisförmigen Abrundung der Ecke. Ein Glättungsfaktor ungleich null (bis maximal 1,0) führt dazu, dass die Ecke durch drei separate Kurven abgerundet wird.
Das folgende Snippet veranschaulicht den subtilen Unterschied zwischen der Einstellung der Glättung auf 0 und 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) )
Größe und Position
Standardmäßig wird eine Form mit einem Radius von 1 um den Mittelpunkt (0, 0) erstellt. Dieser Radius stellt den Abstand zwischen dem Mittelpunkt und den äußeren Eckpunkten des Polygons dar, auf dem die Form basiert. Beachten Sie, dass das Abrunden der Ecken zu einer kleineren Form führt, da die abgerundeten Ecken näher am Mittelpunkt liegen als die abgerundeten Eckpunkte. Wenn Sie die Größe eines Polygons ändern möchten, passen Sie den Wert von radius an. Wenn Sie die Position anpassen möchten, ändern Sie centerX oder centerY des Polygons.
Alternativ können Sie das Objekt transformieren, um seine Größe, Position und Drehung
mit den Standardtransformationsfunktionen wie
DrawScope#translate() zu ändern.DrawScope
Formen wechseln
Ein Morph-Objekt ist eine neue Form, die eine Animation zwischen zwei polygonalen Formen darstellt. Wenn Sie zwischen zwei Formen wechseln möchten, erstellen Sie zwei RoundedPolygons 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 in der Mitte zwischen den beiden Formen (abgerundetes Dreieck und Quadrat), was zu folgendem Ergebnis führt:
In den meisten Fällen erfolgt das Morphing im Rahmen einer Animation und nicht nur als statische Darstellung. Wenn Sie zwischen diesen beiden Formen animieren möchten, können Sie die Standard- Animation APIs in Compose verwenden, um den Fortschrittswert im Laufe der Zeit zu ändern. Sie können beispielsweise das Morphing zwischen diesen beiden Formen unendlich animieren:
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
clip
Modifikator verwendet, um die Darstellung eines Composables 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 Ergebnis sieht so aus:
Das sieht möglicherweise nicht viel anders aus als die vorherige Darstellung, ermöglicht aber 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) ) }
Schaltfläche beim Klicken wechseln
Mit der Bibliothek graphics-shape können Sie eine Schaltfläche erstellen, die beim Drücken zwischen zwei Formen wechselt. Erstellen Sie zuerst eine MorphPolygonShape, die Shape erweitert,
und sie so skaliert und verschiebt, dass sie 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) } }
Wenn Sie diese Morph-Form verwenden möchten, erstellen Sie zwei Polygone: shapeA und shapeB. Erstellen und speichern Sie Morph. Wenden Sie dann das Morphing als Clip-Umriss auf die Schaltfläche an. Verwenden Sie dazu 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)) }
Das führt zu folgender Animation, wenn auf das Feld getippt wird:
Formenwechsel unendlich animieren
Wenn Sie eine Morph-Form unendlich animieren möchten, verwenden Sie
rememberInfiniteTransition.
Unten sehen Sie ein Beispiel für ein Profilbild, dessen Form sich im Laufe der Zeit unendlich ändert (und das sich dreht). Bei diesem Ansatz wird die oben gezeigte MorphPolygonShape nur geringfügig angepasst:
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 führt zu folgendem Ergebnis:
Benutzerdefinierte Polygone
Wenn Formen, die aus regulären Polygonen erstellt wurden, Ihren Anwendungsfall nicht abdecken, können Sie mit einer Liste von Eckpunkten eine benutzerdefinierte Form erstellen. Sie können beispielsweise eine Herzform wie diese erstellen:
Sie können die einzelnen Eckpunkte dieser Form mit der Überladung von RoundedPolygon angeben, die ein Float-Array mit X- und Y-Koordinaten verwendet.
Wenn Sie das Herzpolygon aufschlüsseln, sehen Sie,dass das polare Koordinatensystem zum
Angeben von Punkten einfacher ist als das kartesische (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 Sie für jeden Punkt den Winkel (𝜭) und den Radius vom Mittelpunkt angeben:
Die Eckpunkte 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 Funktion radialToCartesian in kartesische Koordinaten umgewandelt 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-Eckpunkte für das Herz. Sie müssen jedoch bestimmte Ecken abrunden, um die gewünschte Herzform zu erhalten. Die Ecken bei 90° und 270° haben keine Abrundung, die anderen Ecken jedoch schon. Wenn Sie eine benutzerdefinierte Abrundung für einzelne Ecken erzielen 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) )
Das Ergebnis ist das rosa Herz:
Wenn die vorherigen Formen Ihren Anwendungsfall nicht abdecken, können Sie mit der Path
Klasse eine benutzerdefinierte Form zeichnen oder eine
ImageVector-Datei von der
Festplatte laden. Die Bibliothek graphics-shapes 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: