Com o Compose, é possível criar formas feitas de polígonos. Por exemplo: é possível criar os seguintes tipos de formas:

Para criar um polígono arredondado personalizado no Compose, adicione o
dependência de graphics-shapes
para sua
app/build.gradle
:
implementation "androidx.graphics:graphics-shapes:1.0.1"
Essa biblioteca permite criar formas feitas de polígonos. Enquanto as formas poligonais têm apenas bordas retas e cantos afiados, elas permitem cantos arredondados opcionais. Ele facilita a transformação entre duas formas diferentes. A transformação é difícil entre formas arbitrárias e tende a ser uma problema do tempo de design. Mas essa biblioteca simplifica o processo, transformando essas formas em estruturas poligonais semelhantes.
Criar polígonos
O snippet a seguir cria uma forma poligonal básica com seis pontos no centro. da área de desenho:
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() )

Neste exemplo, a biblioteca cria um RoundedPolygon
que contém a geometria
que representam a forma solicitada. Para desenhar essa forma em um app do Compose,
você precisa extrair um objeto Path
dele para criar a forma em um formulário que o Compose
sabe como desenhar.
Arredondar os cantos de um polígono
Para arredondar os cantos de um polígono, use o parâmetro CornerRounding
. Ele
usa dois parâmetros, radius
e smoothing
. Cada canto arredondado é composto
de uma a três curvas cúbicas, e o centro tem um formato de arco circular, enquanto as duas
curvas laterais ("laterais") fazem a transição da borda da forma para a curva central.
Radius
radius
é o raio do círculo usado para arredondar um vértice.
Por exemplo, o triângulo de canto arredondado a seguir é feito da seguinte maneira:


r
determina o tamanho do arredondamento circular
dos cantos arredondados.Suavização
A suavização é um fator que determina quanto tempo leva para ir da
parte arredondada circular do canto até a borda. Um fator de suavização de 0
(não suavizado, o valor padrão de CornerRounding
) resulta em um resultado puramente circular
arredondamento de cantos. Um fator de suavização diferente de zero (até o máximo de 1,0) resulta em
o canto sendo arredondado por três curvas separadas.


Por exemplo, o snippet abaixo ilustra a diferença sutil na configuração suavização para 0 versus 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) )

Tamanho e posição
Por padrão, uma forma é criada com um raio de 1
ao redor do centro (0, 0
).
Esse raio representa a distância entre o centro e os vértices externos
do polígono em que a forma é baseada. Arredondar os cantos
resulta em uma forma menor, pois os cantos arredondados estarão mais próximos do
do que os vértices arredondados. Para dimensionar um polígono, ajuste o radius
.
. Para ajustar a posição, mude o centerX
ou centerY
do polígono.
Como alternativa, transforme o objeto para alterar o tamanho, a posição e a rotação dele
usando funções de transformação DrawScope
padrão, como
DrawScope#translate()
.
Transformar formas
Um objeto Morph
é uma nova forma que representa uma animação entre duas formas
poligonais. Para transformar duas formas, crie duas RoundedPolygons
e um objeto Morph
que receba essas duas formas. Para calcular uma forma entre as formas inicial e final, forneça um valor progress
entre zero e um para determinar a forma entre as formas inicial (0) e final (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() )
No exemplo acima, o progresso está exatamente no meio das duas formas (triângulo arredondado e um quadrado), produzindo o seguinte resultado:

Na maioria dos casos, a transformação é feita como parte de uma animação, e não apenas uma renderização estática. Para animar entre os dois, você pode usar o padrão APIs de animação no Compose para mudar o valor do progresso ao longo do tempo. Por exemplo, você pode animar infinitamente a transformação entre essas duas formas, da seguinte maneira:
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() )

Usar polígono como clipe
É comum usar o modificador
clip
no Compose para mudar a renderização de um elemento combinável e aproveitar
as sombras que aparecem ao redor da área de corte:
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) } }
Em seguida, use o polígono como um clipe, conforme mostrado no snippet a seguir:
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) ) }
Isso resulta no seguinte:

Isso pode não parecer muito diferente do que estava sendo renderizado antes, mas permite aproveitar outros recursos no Compose. Por exemplo, essa técnica pode ser usada para recortar uma imagem e aplicar uma sombra ao redor da região recortada:
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) ) }

Botão de transformação ao clicar
É possível usar a biblioteca graphics-shape
para criar um botão que se transforma entre
duas formas ao ser pressionado. Primeiro, crie um MorphPolygonShape
que estende Shape
,
redimensionando e traduzindo-o para se ajustar adequadamente. Observe a passagem do valor
progresso para que a forma possa ser animada:
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) } }
Para usar essa forma de transformação, crie dois polígonos, shapeA
e shapeB
. Criar e
lembrar da Morph
. Em seguida, aplique a transformação no botão como um contorno de clipe,
usando o interactionSource
ao pressionar como a força motriz por trás da
animação:
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)) }
Isso resulta na animação abaixo quando a caixa é tocada:

Formas animadas que se transformam infinitamente
Para animar uma forma de transformação sem fim, use
rememberInfiniteTransition
.
Confira abaixo um exemplo de foto do perfil que muda de forma (e gira)
infinitamente ao longo do tempo. Essa abordagem usa um pequeno ajuste no
MorphPolygonShape
mostrado acima:
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) ) } }
Esse código gera o seguinte resultado divertido:

Polígonos personalizados
Se as formas criadas com polígonos regulares não atenderem ao seu caso de uso, crie uma forma mais personalizada com uma lista de vértices. Por exemplo, talvez você queira criar um formato de coração como este:

É possível especificar os vértices individuais dessa forma usando a sobrecarga RoundedPolygon
que recebe uma matriz de ponto flutuante de coordenadas x e y.
Para dividir o polígono do coração, observe que o sistema de coordenadas polares
especificar pontos torna isso mais fácil do que usar a coordenada cartesiana (x,y)
em que 0°
começa no lado direito e continua no sentido horário, com
270°
na posição de 12 horas:

Agora, a forma pode ser definida de uma maneira mais fácil, especificando o ângulo (𝜭) e o raio do centro em cada ponto:

Agora, os vértices podem ser criados e transmitidos para a função 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, ) }
Os vértices precisam ser traduzidos em coordenadas cartesianas usando esta
função 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))
O código anterior fornece os vértices brutos do coração, mas você precisa
arredondar cantos específicos para chegar ao formato de coração escolhido. Os cantos em 90°
e
270°
não têm arredondamento, mas os outros cantos têm. Para conseguir um arredondamento personalizado
para cantos individuais, use o parâmetro 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) )
O resultado é o coração rosa:

Caso as formas anteriores não atendam ao seu caso, use a Path
para desenhar um objeto
forma ou carregar uma
Arquivo ImageVector
de
disco. A biblioteca graphics-shapes
não é destinada a formas
arbitrárias, mas é especificamente destinada a simplificar a criação de polígonos arredondados e
animações de transformação entre eles.
Outros recursos
Para mais informações e exemplos, consulte os seguintes recursos:
- Blog: O formato das coisas que estão por vir - Formas
- Blog: Shape morphing no Android
- Demonstração de formas no GitHub