Avec Compose, vous pouvez créer des formes à partir de polygones. Par exemple, vous pouvez créer les types de formes suivants :
Pour créer un polygone arrondi personnalisé dans Compose, ajoutez la
graphics-shapes dépendance à votre
app/build.gradle :
implementation "androidx.graphics:graphics-shapes:1.0.1"
Cette bibliothèque vous permet de créer des formes à partir de polygones. Alors que les formes polygonales n'ont que des bords droits et des angles vifs, ces formes permettent d'arrondir les angles de manière facultative. Il est ainsi facile de passer d'une forme à une autre. La transformation entre des formes arbitraires est difficile et tend à être un problème de conception. Toutefois, cette bibliothèque simplifie le processus en transformant ces formes avec des structures polygonales similaires.
Créer des polygones
L'extrait de code suivant crée une forme polygonale de base avec six points au centre de la zone de dessin :
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() )
Dans cet exemple, la bibliothèque crée un RoundedPolygon qui contient la géométrie représentant la forme demandée. Pour dessiner cette forme dans une application Compose, vous devez obtenir un objet Path afin de la mettre dans un format que Compose sait dessiner.
Arrondir les angles d'un polygone
Pour arrondir les angles d'un polygone, utilisez le paramètre CornerRounding. Il prend deux paramètres : radius et smoothing. Chaque angle arrondi est composé de 1 à 3 courbes cubiques, dont le centre a une forme d'arc circulaire, tandis que les deux courbes latérales ("flanking") passent du bord de la forme à la courbe centrale.
Radius
Le radius est le rayon du cercle utilisé pour arrondir un sommet.
Par exemple, le triangle à angles arrondis suivant est créé comme suit :
r détermine la taille de l'arrondi circulaire des angles arrondis.Smoothing
Le lissage est un facteur qui détermine le temps nécessaire pour passer de la partie arrondie circulaire de l'angle au bord. Un facteur de lissage de 0 (non lissé, valeur par défaut de CornerRounding) entraîne un arrondi d'angle purement circulaire. Un facteur de lissage non nul (jusqu'à 1,0) entraîne l'arrondi de l'angle par trois courbes distinctes.
Par exemple, l'extrait de code ci-dessous illustre la légère différence entre le réglage du lissage sur 0 et 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) )
Taille et position
Par défaut, une forme est créée avec un rayon de 1 autour du centre (0, 0). Ce rayon représente la distance entre le centre et les sommets extérieurs du polygone sur lequel la forme est basée. Notez que l'arrondi des angles entraîne une forme plus petite, car les angles arrondis seront plus proches du centre que les sommets arrondis. Pour dimensionner un polygone, ajustez la valeur radius. Pour ajuster la position, modifiez le centerX ou le centerY du polygone.
Vous pouvez également transformer l'objet pour modifier sa taille, sa position et sa rotation
à l'aide de fonctions de transformation DrawScope standards telles que
DrawScope#translate().
Transformer des formes
Un objet Morph est une nouvelle forme représentant une animation entre deux formes polygonales. Pour transformer deux formes, créez deux RoundedPolygons et un objet Morph qui prend ces deux formes. Pour calculer une forme entre les formes de début et de fin, fournissez une valeur progress comprise entre zéro et un afin de déterminer sa forme entre les formes de début (0) et de fin (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() )
Dans l'exemple ci-dessus, la progression se situe exactement à mi-chemin entre les deux formes (triangle arrondi et carré), ce qui donne le résultat suivant :
Dans la plupart des cas, la transformation est effectuée dans le cadre d'une animation, et pas seulement d'un rendu statique. Pour animer ces deux éléments, vous pouvez utiliser les API d'animation standards dans Compose afin de modifier la valeur de progression au fil du temps. Par exemple, vous pouvez animer la transformation entre ces deux formes de manière infinie comme suit :
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() )
Utiliser un polygone comme clip
Il est courant d'utiliser le
clip
modificateur dans Compose pour modifier le rendu d'un composable et profiter des
ombres qui se dessinent autour de la zone de découpe :
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) } }
Vous pouvez ensuite utiliser le polygone comme clip, comme illustré dans l'extrait de code suivant :
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) ) }
Cela donne le résultat suivant :
Cela ne semble pas très différent de ce qui était rendu auparavant, mais cela permet d'exploiter d'autres fonctionnalités de Compose. Par exemple, cette technique peut être utilisée pour découper une image et appliquer une ombre autour de la région découpée :
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) ) }
Transformer un bouton au clic
Vous pouvez utiliser la bibliothèque graphics-shape pour créer un bouton qui se transforme entre deux formes lorsque vous appuyez dessus. Commencez par créer un MorphPolygonShape qui étend Shape,
en le mettant à l'échelle et en le traduisant pour qu'il s'adapte correctement. Notez le passage de la progression afin que la forme puisse être animée :
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) } }
Pour utiliser cette forme de transformation, créez deux polygones, shapeA et shapeB. Créez et mémorisez le Morph. Ensuite, appliquez la transformation au bouton en tant que contour de clip, en utilisant le interactionSource lorsque vous appuyez dessus comme force motrice de l'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)) }
L'animation suivante s'affiche lorsque vous appuyez sur la zone :
Animer la transformation de forme à l'infini
Pour animer une forme de transformation à l'infini, utilisez
rememberInfiniteTransition.
Vous trouverez ci-dessous un exemple de photo de profil dont la forme change (et pivote) à l'infini au fil du temps. Cette approche utilise un petit ajustement du MorphPolygonShape présenté ci-dessus :
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) ) } }
Ce code donne le résultat amusant suivant :
Polygones personnalisés
Si les formes créées à partir de polygones réguliers ne couvrent pas votre cas d'utilisation, vous pouvez créer une forme plus personnalisée avec une liste de sommets. Par exemple, vous pouvez créer une forme de cœur comme celle-ci :
Vous pouvez spécifier les sommets individuels de cette forme à l'aide de la surcharge RoundedPolygon qui prend un tableau flottant de coordonnées x, y.
Pour décomposer le polygone du cœur, notez que le système de coordonnées polaires pour
spécifier des points est plus simple que le système de coordonnées cartésiennes (x, y)
où 0° commence à droite et se poursuit dans le sens des aiguilles d'une montre, avec
270° à 12 heures :
La forme peut désormais être définie plus facilement en spécifiant l'angle (𝜭) et le rayon à partir du centre à chaque point :
Les sommets peuvent désormais être créés et transmis à la fonction 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, ) }
Les sommets doivent être traduits en coordonnées cartésiennes à l'aide de cette fonction 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))
Le code précédent vous donne les sommets bruts du cœur, mais vous devez arrondir des angles spécifiques pour obtenir la forme de cœur choisie. Les angles à 90° et 270° ne sont pas arrondis, mais les autres le sont. Pour obtenir un arrondi personnalisé pour chaque angle, utilisez le paramètre 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) )
Cela donne le cœur rose :
Si les formes précédentes ne couvrent pas votre cas d'utilisation, envisagez d'utiliser la Path
classe pour dessiner une forme
personnalisée ou de charger un fichier
ImageVector à partir du
disque. La bibliothèque graphics-shapes n'est pas destinée à être utilisée pour des formes arbitraires, mais vise spécifiquement à simplifier la création de polygones arrondis et d'animations de transformation entre eux.
Ressources supplémentaires
Pour obtenir plus d'informations et des exemples, consultez les ressources suivantes :
- Blog : The Shape of Things to Come - Shapes (La forme des choses à venir – Formes)
- Blog : Shape morphing in Android (Transformation de formes dans Android)
- Démonstration de formes sur GitHub