Mit „Zusammenstellen“ können Sie Formen aus Polygonen erstellen. Sie können beispielsweise die folgenden Arten von Formen erstellen:
Wenn Sie in Compose ein benutzerdefiniertes abgerundetes Polygon erstellen möchten, fügen Sie Ihrem app/build.gradle
die Abhängigkeit graphics-shapes
hinzu:
implementation "androidx.graphics:graphics-shapes:1.0.0-rc01"
Mit dieser Bibliothek können Sie Formen erstellen, die aus Polygonen bestehen. Polygonale Formen haben nur gerade Kanten und scharfe Ecken, diese Formen können aber optional abgerundete Ecken haben. So lässt sich ganz einfach zwischen zwei verschiedenen Formen wechseln. Die Veränderung zwischen beliebigen Formen ist schwierig und stellt in der Regel ein Problem in der Entwicklungszeit dar. Diese Bibliothek macht es jedoch einfach, indem zwischen diesen Formen mit ähnlichen polygonalen Strukturen abgewandelt wird.
Polygone erstellen
Im folgenden Snippet wird ein einfaches Polygon 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 enthält, die die angeforderte Form darstellt. Wenn Sie diese Form in einer Compose-App zeichnen möchten, müssen Sie ein Path
-Objekt daraus abrufen, damit Compose die Form zeichnen kann.
Ecken eines Polygons abrunden
Verwenden Sie den Parameter CornerRounding
, um die Ecken eines Polygons abzurunden. Es werden zwei Parameter benötigt: radius
und smoothing
. Jede abgerundete Ecke besteht aus 1 bis 3 kubischen Kurven, deren Mitte einen kreisförmigen Bogen hat, während die beiden Kurven ("flankierenden") Kurven vom Rand der Form zur Mittelkurve übergehen.
Radius
radius
ist der Radius des Kreises, mit dem ein Eckpunkt gerundet wird.
Das folgende Dreieck mit abgerundeten Ecken wird beispielsweise so erstellt:
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. Ein Glättungsfaktor von 0 (nicht geglättet, der Standardwert für CornerRounding
) führt zu rein kreisförmigen Ecken. Ein nicht nullwertiger Glättungsfaktor (bis zu einem Maximum von 1,0) führt dazu, dass die Ecke durch drei separate Kurven abgerundet wird.
Im folgenden Snippet wird beispielsweise der kleine Unterschied zwischen einer Einstellung von 0 und 1 für die Glättung 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
) 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. Ändern Sie centerX
oder centerY
des Polygons, um die Position anzupassen.
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 Sie zwischen zwei Formen morphen möchten, erstellen Sie zwei RoundedPolygons
- und ein Morph
-Objekt, die diese beiden Formen haben. Wenn Sie eine Form zwischen der Anfangs- und Endform berechnen möchten, geben Sie einen progress
-Wert zwischen null und eins an, um die Form zwischen der Anfangsform (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 ist 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 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. Das Morphensymbol zwischen diesen beiden Formen lässt sich z. B. unendlich so 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 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) ) }
Daraus ergibt sich Folgendes:
Das sieht vielleicht nicht viel anders aus als das, was vorher gerendert wurde, aber es ermöglicht die Nutzung anderer Funktionen in Compose. So 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) ) }
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, und skalieren und verschieben Sie sie so, dass sie richtig passt. Beachten Sie, dass der Fortschritt übergeben wird, damit das Shape 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 Sie Morph
und merken Sie sich die ID. Wenden Sie dann das Morph-Element als Clip-Umriss auf die Schaltfläche an und verwenden Sie interactionSource
beim Drücken als Antriebskraft 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 Sie auf das Feld tippen, wird die folgende Animation angezeigt:
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) ) } }
Mit diesem Code erhalten Sie das folgende lustige Ergebnis:
Benutzerdefinierte Polygone
Wenn Formen, die aus regelmäßigen Polygonen erstellt wurden, Ihren Anwendungsfall nicht abdecken, können Sie eine benutzerdefinierte Form mit einer Liste von Scheitelpunkten erstellen. Sie können beispielsweise eine Herzform erstellen:
Sie können die einzelnen Eckpunkte dieser Form mit der Überladung von RoundedPolygon
angeben, die ein Float-Array mit X‑ und Y‑Koordinaten annimmt.
Wenn Sie das Herzpolygon aufschlüsseln möchten, beachten Sie,dass dies durch das Polarkoordinatensystem zum Festlegen von Punkten einfacher ist als mit dem kartesischen Koordinatensystem (x, y), bei dem 0°
auf der rechten Seite beginnt und im Uhrzeigersinn weiterläuft, wobei 270°
auf der 12-Uhr-Position ist:
Die Form kann jetzt einfacher definiert werden, indem an jedem Punkt der Winkel (𝜭) und der Radius vom Mittelpunkt angegeben werden:
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 der 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 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) )
Das ergibt das rosa Herz:
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 Formen gedacht, sondern soll speziell das Erstellen von abgerundeten Polygonen und Morph-Animationen zwischen ihnen vereinfachen.
Weitere Informationen
Weitere Informationen und Beispiele finden Sie in den folgenden Ressourcen: