Con Componi 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 dipendenza graphics-shapes
a app/build.gradle
:
implementation "androidx.graphics:graphics-shapes:1.0.0-rc01"
Questa libreria ti consente di creare forme costituite da poligoni. Le forme poligonali hanno solo bordi dritti e angoli taglienti, ma consentono di ottenere angoli arrotondati facoltativi. Consente di passare facilmente da una forma all'altra. Il morphing è difficile tra forme arbitrarie e tende a essere un problema di progettazione. Tuttavia, questa libreria semplifica il tutto grazie alla trasformazione tra queste forme con strutture poligonali simili.
Creare poligoni
Il seguente snippet crea una forma di poligono base con sei 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 rappresentante la forma richiesta. Per disegnare quella forma in un'app Compose,
devi recuperare un oggetto Path
dall'app per ottenere la forma in un modulo che Compose
sa come disegnare.
Arrotondare gli angoli di un poligono
Per arrotondare gli angoli di un poligono, utilizza il parametro CornerRounding
. Questa operazione
richiede due parametri, radius
e smoothing
. Ogni angolo arrotondato è costituito da 1-3 curve cubiche, il cui centro ha la forma di un arco circolare, mentre le due curve laterali ("di accompagnamento") passano dal bordo della forma alla curva centrale.
Raggio
radius
è il raggio del cerchio utilizzato per arrotondare un vertice.
Ad esempio, il seguente triangolo con angoli arrotondati è realizzato come segue:
Sfumatura
La levigatura è un fattore che determina il tempo necessario per passare dalla porzione arrotondata circolare dell'angolo al bordo. Un fattore di smussamento pari a 0
(non smussato, il valore predefinito per CornerRounding
) comporta un smusso puramente circolare
degli angoli. Se viene impostato un fattore di smussamento diverso da zero (fino a un massimo di 1,0), l'angolo viene arrotondato con tre curve separate.
Ad esempio, lo snippet riportato di seguito illustra la sottile differenza tra l'impostazione della funzionalità di smussamento su 0 e su 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, viene creata una forma con un raggio di 1
intorno al centro (0, 0
).
Questo raggio rappresenta la distanza tra il centro e i vertici esterni del poligono su cui è basata la forma. Tieni presente che l'arrotondamento dei bordi consente di ottenere una forma più piccola, poiché i bordi arrotondati saranno più vicini al centro rispetto ai vertici arrotondati. Per modificare le dimensioni di un poligono, modifica il valore radius
. Per regolare 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, come DrawScope#translate()
.
Trasformare le forme
Un oggetto Morph
è una nuova forma che rappresenta un'animazione tra due forme poligonali. Per morphing tra due forme, crea due RoundedPolygons
e un oggetto Morph
che assuma 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à tra le due forme (triangolo arrotondato e quadrato), producendo il seguente risultato:
Nella maggior parte dei casi, il morphing viene eseguito nell'ambito di un'animazione e non solo di un rendering statico. Per animare il passaggio da uno all'altro, puoi utilizzare le API di animazione standard in Compose per modificare il valore di avanzamento nel tempo. Ad esempio, puoi animare infinitamente la trasformazione tra queste due forme come segue:
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 un poligono come clip
È comune utilizzare il modificatore
clip
in Compose per modificare il modo in cui viene visualizzato un composable e per sfruttare
le ombre disegnate intorno 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 nello snippet seguente:
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) ) }
Questo comporta quanto segue:
Potrebbe non sembrare molto diverso da quanto veniva visualizzato in precedenza, ma consente di sfruttare altre funzionalità in Compose. Ad esempio, questa tecnica può essere utilizzata per ritagliare un'immagine e applicare un'ombra intorno all'area 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) ) }
Pulsante Morph 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
,
ridimensionandolo e traducendolo per adattarsi in modo appropriato. Tieni presente che il progresso viene passato 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 morph, crea due poligoni, shapeA
e shapeB
. Crea e ricordati il Morph
. Quindi, applica la trasformazione al pulsante come contorno del clip, utilizzando il pulsante interactionSource
premuto come forza trainante 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 la trasformazione di una forma all'infinito
Per animare in modo continuo una forma di morph, utilizza
rememberInfiniteTransition
.
Di seguito è riportato un esempio di un'immagine del profilo che cambia forma (e ruota)
in modo infinito nel tempo. Questo approccio utilizza un piccolo aggiustamento al valore MorphPolygonShape
mostrato 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 restituisce 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 il sovraccarico di RoundedPolygon
, che richiede un array in virgola mobile di coordinate x e y.
Per suddividere il poligono a forma di cuore, tieni presente che il sistema di coordinate polari per la specifica dei punti semplifica l'operazione rispetto all'utilizzo del sistema di coordinate cartesiane (x, y), in cui 0°
inizia sul lato destro e procede in senso orario, con 270°
in corrispondenza della posizione delle ore 12:
Ora la forma può essere definita in modo più semplice specificando l'angolo (𝜭) e il raggio dal centro in ogni punto:
Ora i vertici possono 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 non elaborati del cuore, ma devi arrotondare angoli specifici per ottenere la forma del cuore scelta. I vertici 90°
e
270°
non sono arrotondati, ma gli altri sì. Per ottenere un arrotondamento personalizzato
per i singoli vertici, 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) )
Viene visualizzato il cuore rosa:
Se le forme precedenti non coprono il tuo caso d'uso, ti consigliamo di utilizzare la classe Path
per disegnare una forma personalizzata o di caricare un file ImageVector
dal disco. La libreria graphics-shapes
non è pensata per essere utilizzata per forme arbitrarie, ma è specificamente progettata per semplificare la creazione di poligoni arrotondati e animazioni di morph tra di loro.
Risorse aggiuntive
Per ulteriori informazioni ed esempi, consulta le seguenti risorse:
- Post del blog: The Shape of Things to Come - Shapes
- Post del blog: Morphing delle forme in Android
- Dimostrazione di Shapes su GitHub