Mit der Funktion "Schreiben" können Sie Formen aus Polygonen erstellen. Sie können z. B. die folgenden Arten von Formen erstellen:
Wenn Sie in Compose ein benutzerdefiniertes abgerundetes Polygon erstellen möchten, fügen Sie der app/build.gradle
die Abhängigkeit graphics-shapes
hinzu:
implementation "androidx.graphics:graphics-shapes:1.0.0-alpha05"
Mit dieser Bibliothek können Sie Formen aus Polygonen erstellen. Polygone haben nur gerade Kanten und scharfe Ecken, aber es sind auch abgerundete Ecken möglich. Es vereinfacht das Morphen zwischen zwei verschiedenen Formen. Das Morphieren zwischen beliebigen Formen ist schwierig und ist in der Regel ein Problem bei der Entwicklung. Diese Bibliothek macht es jedoch einfach, indem sie zwischen diesen Formen mit ähnlichen polygonalen Strukturen wandelt.
Polygone erstellen
Mit dem folgenden Snippet wird eine grundlegende Polygonform mit 6 Punkten in der Mitte des Zeichnungsbereichs 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 einen RoundedPolygon
, der die Geometrie enthält, die die angeforderte Form darstellt. Wenn Sie diese Form in einer App zum Zeichnen verwenden möchten, müssen Sie ein Path
-Objekt daraus abrufen. So wird die Form in ein Formular umgewandelt, das in der Funktion „Compose“ gezeichnet wird.
Ecken eines Polygons abrunden
Verwenden Sie den Parameter CornerRounding
, um die Ecken eines Polygons abzurunden. Dafür sind die beiden Parameter radius
und smoothing
erforderlich. Jede abgerundete Ecke besteht aus 1–3 kubischen Kurven, wobei die Mitte eine kreisförmige Bogenform hat, während die beiden Seitenkurven („flankierende Kurven“) vom Rand der Form zur mittleren Kurve übergehen.
Radius
radius
ist der Radius des Kreises, der zum Runden eines Scheitelpunkts verwendet wird.
Das folgende abgerundete Eckdreieck wird beispielsweise so erstellt:
Glättung
Die Glättung ist ein Faktor, der festlegt, wie lange es dauert, bis der kreisförmige Rundungsabschnitt der Ecke bis zum Rand benötigt wird. Ein Glättungsfaktor von 0 (ungeglättet, der Standardwert für CornerRounding
) führt zu einer rein kreisförmigen Rundung der Ecken. Ein Glättungsfaktor ungleich null (bis zu 1,0) führt dazu, dass die Ecke durch drei separate Kurven gerundet wird.
Das folgende Snippet veranschaulicht beispielsweise den geringfügigen Unterschied bei 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 entspricht dem Abstand zwischen dem Mittelpunkt und den Außeneckpunkten des Polygons, auf dem die Form basiert. Durch das Abrunden der Ecken wird eine kleinere Form erzielt, da die abgerundeten Ecken näher an der Mitte liegen als die abgerundeten Eckpunkte. Passen Sie die Größe eines Polygons an den Wert radius
an. Um die Position anzupassen, ändern Sie centerX
oder centerY
des Polygons.
Alternativ können Sie das Objekt mit Standard-Transformationsfunktionen wie DrawScope#translate()
transformieren, um seine Größe, Position und Drehung zu ändern.DrawScope
Morphformen
Ein Morph
-Objekt ist eine neue Form, die eine Animation zwischen zwei Polygonformen darstellt. Wenn Sie zwischen zwei Formen wandeln möchten, erstellen Sie zwei RoundedPolygons
und ein Morph
-Objekt, das diese beiden Formen verwendet. Um eine Form zwischen der Start- und Endform zu berechnen, geben Sie einen progress
-Wert zwischen null und eins 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 halber Strecke zwischen den beiden Formen (abgerundetes Dreieck und ein Quadrat), was zu folgendem Ergebnis führt:
In den meisten Szenarien erfolgt das Morphen im Rahmen einer Animation und nicht nur als statisches Rendering. Zur Animation zwischen diesen beiden Funktionen können Sie die standardmäßigen Animations-APIs in Compose verwenden, um den Fortschrittswert im Laufe der Zeit zu ändern. So lässt sich beispielsweise das Morphen 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
Mit dem clip
-Modifikator in Compose ändern Sie, wie eine zusammensetzbare Funktion gerendert wird, und nutzen die Schatten, die den Zuschnittbereich umgeben:
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:
Dies unterscheidet sich möglicherweise nicht so sehr vom vorherigen Rendering, ermöglicht jedoch die Nutzung weiterer Funktionen in Compose. Diese Technik kann beispielsweise verwendet werden, um ein Bild zuzuschneiden und einen Schatten um den zugeschnittenen Bereich herum anzuwenden:
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
Sie können die graphics-shape
-Bibliothek verwenden, um eine Schaltfläche zu erstellen, die sich beim Drücken zwischen zwei Formen wechselt. Erstellen Sie zuerst einen MorphPolygonShape
, der Shape
erweitert und ihn entsprechend skaliert und übersetzt. Wie Sie sehen, wird der Fortschritt übergeben, 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 Morphform zu verwenden, erstellen Sie die beiden Polygone shapeA
und shapeB
. Erstellen Sie die Morph
und merken Sie sie sich. Wenden Sie das Morph auf die Schaltfläche als Clipumriss an. Verwenden Sie dazu das 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 Sie auf das Feld tippen, wird die folgende Animation angezeigt:
Unendliche Animierung der Formänderung
Verwenden Sie rememberInfiniteTransition
, um eine Morphform endlos zu animieren.
Unten siehst du ein Beispiel für ein Profilbild, das seine Form im Laufe der Zeit unendlich ändert (und 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 unterhaltsame Ergebnis:
Benutzerdefinierte Polygone
Wenn aus regelmäßigen Polygonen erstellte Formen Ihren Anwendungsfall nicht abdecken, können Sie eine benutzerdefinierte Form mit einer Liste von Eckpunkten erstellen. Sie können z. B. eine Herzform wie diese erstellen:
Sie können die einzelnen Eckpunkte dieser Form mithilfe der RoundedPolygon
-Überlastung angeben, für die ein Float-Array aus x- und y-Koordinaten verwendet wird.
Beachten Sie beim Aufschlüsseln des Herzpolygons, dass das polare Koordinatensystem zum Angeben von Punkten dies einfacher macht als die Verwendung des kartesischen Koordinatensystems (x, y), bei dem 0°
auf der rechten Seite beginnt und im Uhrzeigersinn weiterläuft, wobei 270°
bei 12 Uhr steht:
Die Form kann jetzt einfacher definiert werden, indem der Winkel (Θ) und der Radius vom Mittelpunkt an jedem Punkt angegeben werden:
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 der folgenden radialToCartesian
-Funktion 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))
Mit dem vorherigen Code erhalten Sie die unbearbeiteten Eckpunkte für das Herz. Sie müssen jedoch bestimmte Ecken abrunden, um die ausgewählte Herzform zu erhalten. Die Ecken bei 90°
und 270°
haben keine Rundung, aber die anderen Ecken. Um eine individuelle Rundung einzelner Ecken zu erzielen, 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) )
Dies führt zum rosa Herz:
Wenn die vorherigen Formen Ihren Anwendungsfall nicht abdecken, können Sie die Klasse Path
verwenden, um eine benutzerdefinierte Form zu zeichnen, oder eine ImageVector
-Datei vom Laufwerk laden. Die graphics-shapes
-Bibliothek ist nicht für die Verwendung für beliebige Formen vorgesehen, soll jedoch das Erstellen abgerundeter Polygone und Morph-Animationen zwischen ihnen vereinfachen.
Weitere Informationen
Weitere Informationen und Beispiele finden Sie in den folgenden Ressourcen:
- Blog: Die Form der Dinge, die kommen werden – Formen
- Blog: Shape-Morphing in Android
- Demo zu Shapes GitHub