Con Compose, puoi creare forme costituite da poligoni. Ad esempio, puoi creare i seguenti tipi di forme:
Per creare un poligono arrotondato personalizzato in Compose, aggiungi la
graphics-shapes dipendenza a
app/build.gradle:
implementation "androidx.graphics:graphics-shapes:1.0.1"
Questa libreria ti consente di creare forme costituite da poligoni. Sebbene le forme poligonali abbiano solo bordi dritti e angoli acuti, queste forme consentono di avere angoli arrotondati facoltativi. Semplifica la trasformazione tra due forme diverse. La trasformazione tra forme arbitrarie è difficile e tende a essere un problema di progettazione. Tuttavia, questa libreria semplifica la trasformazione tra queste forme con strutture poligonali simili.
Creare poligoni
Il seguente snippet crea una forma poligonale di base con 6 punti al centro dell'area di disegno:
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 questo esempio, la libreria crea un RoundedPolygon che contiene la geometria che rappresenta la forma richiesta. Per disegnare questa forma in un'app Compose, devi ottenere un oggetto Path per trasformare la forma in un formato che Compose sa disegnare.
Arrotondare gli angoli di un poligono
Per arrotondare gli angoli di un poligono, utilizza il parametro CornerRounding. Questo accetta due parametri, radius e smoothing. Ogni angolo arrotondato è composto da 1-3 curve cubiche, il cui centro ha una forma ad arco circolare, mentre le due curve laterali ("di fianco") passano dal bordo della forma alla curva centrale.
Radius
radius è il raggio del cerchio utilizzato per arrotondare un vertice.
Ad esempio, il seguente triangolo con angoli arrotondati viene creato nel seguente modo:
r determina la dimensione dell'arrotondamento circolare degli angoli arrotondati.Smoothing
Il fattore di smoothing determina il tempo necessario per passare dalla parte di arrotondamento circolare dell'angolo al bordo. Un fattore di smoothing pari a 0 (non uniforme, il valore predefinito di CornerRounding) comporta un arrotondamento dell'angolo puramente circolare. Un fattore di smoothing diverso da zero (fino a un massimo di 1,0) comporta l'arrotondamento dell'angolo mediante tre curve separate.
Ad esempio, lo snippet riportato di seguito illustra la sottile differenza tra l'impostazione di smoothing su 0 e 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) )
Dimensioni e posizione
Per impostazione predefinita, una forma viene creata con un raggio di 1 attorno al centro (0, 0). Questo raggio rappresenta la distanza tra il centro e i vertici esterni del poligono su cui si basa la forma. Tieni presente che l'arrotondamento degli angoli comporta una forma più piccola, poiché gli angoli arrotondati saranno più vicini al centro rispetto ai vertici arrotondati. Per ridimensionare un poligono, modifica il valore radius. Per modificare la posizione, modifica centerX o centerY del poligono.
In alternativa, trasforma l'oggetto per modificarne le dimensioni, la posizione e la rotazione
utilizzando le funzioni di trasformazione DrawScope standard, ad esempio
DrawScope#translate().
Trasformare le forme
Un oggetto Morph è una nuova forma che rappresenta un'animazione tra due forme poligonali. Per trasformare due forme, crea due RoundedPolygons e un oggetto Morph che accetta queste due forme. Per calcolare una forma tra le forme iniziale e finale, fornisci un valore progress compreso tra zero e uno per determinarne la forma tra le forme iniziale (0) e finale (1):
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() )
Nell'esempio precedente, l'avanzamento è esattamente a metà strada tra le due forme (triangolo arrotondato e quadrato), producendo il seguente risultato:
Nella maggior parte degli scenari, la trasformazione viene eseguita come parte di un'animazione e non solo come rendering statico. Per animare la transizione tra queste due forme, puoi utilizzare le API di animazione standard in Compose per modificare il valore di avanzamento nel tempo. Ad esempio, puoi animare all'infinito la trasformazione tra queste due forme nel seguente modo:
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() )
Utilizzare il poligono come clip
È comune utilizzare il
clip
modificatore in Compose per modificare il rendering di un elemento componibile e sfruttare le
ombre che vengono disegnate attorno all'area di ritaglio:
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) } }
Puoi quindi utilizzare il poligono come clip, come mostrato nel seguente snippet:
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) ) }
Il risultato è il seguente:
Questo potrebbe non sembrare molto diverso dal rendering precedente, ma consente di sfruttare altre funzionalità di Compose. Ad esempio, questa tecnica può essere utilizzata per ritagliare un'immagine e applicare un'ombra attorno alla regione ritagliata:
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) ) }
Trasformare il pulsante al clic
Puoi utilizzare la libreria graphics-shape per creare un pulsante che si trasforma tra due forme alla pressione. Innanzitutto, crea un MorphPolygonShape che estende Shape,
scalando e traslandolo in modo che si adatti correttamente. Tieni presente il passaggio dell'avanzamento in modo che la forma possa essere animata:
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) } }
Per utilizzare questa forma di trasformazione, crea due poligoni, shapeA e shapeB. Crea e ricorda Morph. Quindi, applica la trasformazione al pulsante come contorno della clip, utilizzando interactionSource alla pressione come forza motrice dell'animazione:
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)) }
Quando tocchi la casella, viene visualizzata la seguente animazione:
Animare all'infinito la trasformazione della forma
Per animare all'infinito una forma di trasformazione, utilizza
rememberInfiniteTransition.
Di seguito è riportato un esempio di un'immagine del profilo che cambia forma (e ruota) all'infinito nel tempo. Questo approccio utilizza una piccola modifica di MorphPolygonShape mostrata sopra:
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) ) } }
Questo codice produce il seguente risultato divertente:
Poligoni personalizzati
Se le forme create da poligoni regolari non coprono il tuo caso d'uso, puoi creare una forma più personalizzata con un elenco di vertici. Ad esempio, potresti voler creare una forma a cuore come questa:
Puoi specificare i singoli vertici di questa forma utilizzando l'overload RoundedPolygon che accetta un array di float di coordinate x, y.
Per scomporre il poligono del cuore, tieni presente che il sistema di coordinate polari per specificare i punti è più semplice rispetto al sistema di coordinate cartesiane (x, y), in cui 0° inizia sul lato destro e procede in senso orario, con 270° nella posizione delle 12:
Ora la forma può essere definita in modo più semplice specificando l'angolo (𝜭) e il raggio dal centro in ogni punto:
I vertici possono ora essere creati e passati alla funzione RoundedPolygon:
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, ) }
I vertici devono essere convertiti in coordinate cartesiane utilizzando questa funzione radialToCartesian:
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))
Il codice precedente fornisce i vertici grezzi per il cuore, ma devi arrotondare angoli specifici per ottenere la forma a cuore scelta. Gli angoli a 90° e 270° non hanno arrotondamento, ma gli altri angoli sì. Per ottenere un arrotondamento personalizzato per i singoli angoli, utilizza il parametro 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) )
Il risultato è il cuore rosa:
Se le forme precedenti non coprono il tuo caso d'uso, valuta la possibilità di utilizzare la Path
classe per disegnare una forma
personalizzata o caricare un file
ImageVector dal
disco. La libreria graphics-shapes non è destinata all'uso per forme arbitrarie, ma è specificamente pensata per semplificare la creazione di poligoni arrotondati e animazioni di trasformazione tra di essi.
Risorse aggiuntive
Per ulteriori informazioni ed esempi, consulta le seguenti risorse:
- Blog: The Shape of Things to Come - Shapes
- Blog: Shape morphing in Android
- Shapes Github demonstration