Za pomocą Compose możesz tworzyć kształty utworzone z poligonów. Możesz na przykład tworzyć takie kształty:

Aby utworzyć niestandardowy zaokrąglony wielokąt w Compose, dodaj do app/build.gradle
zależność graphics-shapes
:
implementation "androidx.graphics:graphics-shapes:1.0.1"
Ta biblioteka umożliwia tworzenie kształtów złożonych z poligonów. Chociaż kształty wielokątne mają tylko proste krawędzie i ostre rogi, umożliwiają opcjonalne zaokrąglenie rogów. Dzięki temu możesz łatwo przechodzić między dwoma różnymi kształtami. Przekształcanie dowolnych kształtów jest trudne i zwykle stanowi problem na etapie projektowania. Ta biblioteka ułatwia to zadanie, ponieważ umożliwia przekształcanie kształtów o podobnych strukturach wielokątnych.
Tworzenie wielokątów
Ten fragment kodu tworzy podstawowy wielokąt z 6 punktami w środku obszaru rysunku:
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() )

W tym przykładzie biblioteka tworzy RoundedPolygon
, która zawiera geometrię reprezentującą wymagany kształt. Aby narysować ten kształt w aplikacji Compose, musisz uzyskać z niego obiekt Path
, aby móc narysować go w formie, którą Compose potrafi narysować.
Zaokrąglenie narożników wielokąta
Aby zaokrąglić rogi wielokąta, użyj parametru CornerRounding
. Ta funkcja przyjmuje 2 parametry: radius
i smoothing
. Każdy zaokrąglony róg składa się z 1–3 krzywych sześciennych, których środek ma kształt łuku kolistego, a dwie krzywe boczne („flankujące”) przechodzą od krawędzi kształtu do krzywej środkowej.
Promień
radius
to promień koła użytego do zaokrąglenia wierzchołka.
Na przykład trójkąt z zaokrąglonymi rogami można utworzyć w ten sposób:


r
określa wielkość zaokrąglonych rogów.Złagodzenie
Wygładzanie to czynnik, który określa, ile czasu zajmuje przejście od zaokrąglonego narożnika do krawędzi. Wartość 0 (niewygładzona, domyślna wartość dla CornerRounding
) powoduje zaokrąglenie narożników w postaci koła. Niezerowy współczynnik wygładzania (maksymalnie 1,0) powoduje zaokrąglenie narożnika za pomocą 3 osobnych krzywych.


Na przykład poniższy fragment kodu pokazuje subtelną różnicę między ustawieniem wygładzania na 0 a 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) )

Rozmiar i położenie
Domyślnie kształt jest tworzony z promieniem 1
wokół środka (0, 0
).
Ten promień reprezentuje odległość między środkiem a zewnętrzną krawędzią wielokąta, na którym opiera się kształt. Pamiętaj, że zaokrąglenie narożników powoduje powstanie mniejszego kształtu, ponieważ zaokrąglone narożniki będą bliżej środka niż zaokrąglone wierzchołki. Aby dostosować rozmiar wielokąta, zmień wartość radius
. Aby dostosować pozycję, zmień centerX
lub centerY
wielokąta.
Możesz też zmienić rozmiar, położenie i obrót obiektu, korzystając ze standardowych funkcji przekształcenia DrawScope
, takich jak DrawScope#translate()
.
Kształty Morph
Obiekt Morph
to nowy kształt reprezentujący animację między dwoma wielokątami. Aby przekształcić jeden kształt w drugi, utwórz 2 obiekty RoundedPolygons
i Morph
, które przyjmują te 2 ksztalty. Aby obliczyć kształt między początkowym a końcowym kształtem, podaj wartość progress
z zakresu od 0 do 1, aby określić jego formę między początkowym (0) a końcowym (1) kształtem:
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() )
W tym przykładzie postęp jest dokładnie w połowie między dwoma kształtami (zaokrąglony trójkąt i kwadrat), co daje następujący wynik:

W większości przypadków przekształcanie jest wykonywane w ramach animacji, a nie tylko statycznych renderów. Aby animować przejście między tymi stanami, możesz użyć standardowych interfejsów API animacji w komponencie Compose, aby zmieniać wartość postępu w czasie. Możesz na przykład bez końca animować przejście między tymi dwoma kształtami:
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() )

Używanie wielokąta jako klipu
W komponowaniu często używa się modyfikatora clip
, aby zmienić sposób renderowania kompozytowalności i korzystać z cieni rysowanych wokół obszaru przycinania:
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) } }
Następnie możesz użyć tego wielokąta jako klipu, jak pokazano w tym fragmencie kodu:
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) ) }
Powoduje to:

Może to nie wyglądać na dużą zmianę w stosunku do poprzedniego renderowania, ale pozwala korzystać z innych funkcji w Compose. Możesz na przykład użyć tej techniki, aby wyciąć obraz i zastosować cień wokół wyciętego obszaru:
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) ) }

Przycisk zmiany kształtu
Korzystając z biblioteki graphics-shape
, możesz utworzyć przycisk, który po naciśnięciu zmienia kształt. Najpierw utwórz obiekt MorphPolygonShape
, który rozszerza obiekt Shape
, skalując i przekształcając go w odpowiednim zakresie. Zwróć uwagę na przekazywanie postępu, aby kształt mógł być animowany:
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) } }
Aby użyć tego kształtu przejścia, utwórz 2 polygony: shapeA
i shapeB
. Utwórz i zapamiętaj Morph
. Następnie zastosuj przekształcenie do przycisku jako zarys klipu, używając naciśnięcia przycisku interactionSource
jako siły napędowej animacji:
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)) }
Gdy klikniesz pole, zobaczysz tę animację:

Animowanie nieskończonego przekształcania kształtu
Aby utworzyć nieskończoną animację przekształcania kształtu, użyj rememberInfiniteTransition
.
Poniżej przedstawiamy przykład zdjęcia profilowego, które zmienia kształt (i obraca się) w nieskończoność. W tym podejściu wprowadzamy niewielkie zmiany w układzieMorphPolygonShape
pokazanym powyżej:
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) ) } }
Ten kod daje taki zabawny wynik:

Wielokąty niestandardowe
Jeśli kształty utworzone z regularnych wielokątów nie spełniają Twoich potrzeb, możesz utworzyć bardziej niestandardowy kształt za pomocą listy wierzchołków. Możesz na przykład utworzyć kształt serca:

Poszczególne wierzchołki tego kształtu możesz określić za pomocą przeciążenia funkcji RoundedPolygon
, która przyjmuje tablicę typu float z współrzędnymi x i y.
Aby podzielić serce na wielokąt, zwróć uwagę,że użycie układu współrzędnych biegunowych do określania punktów jest łatwiejsze niż użycie układu współrzędnych kartezjańskich (x, y), w którym 0°
zaczyna się po prawej stronie i porusza zgodnie z ruchem wskazówek zegara, a 270°
znajduje się w pozycji 12 godzin:

Kształt można teraz definiować w prostszy sposób, podając kąt (𝜭) i promień od środka w każdym punkcie:

Wierzchołki można teraz utworzyć i przekazać funkcji 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, ) }
Wierzchołki należy przekształcić w współrzędne kartezjańskie za pomocą tej funkcji: 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))
Powyższy kod daje surowe wierzchołki serca, ale musisz zaokrąglić określone rogi, aby uzyskać wybrany kształt serca. Narożniki w miejscach 90°
i 270°
nie są zaokrąglone, ale inne narożniki już tak. Aby zastosować niestandardowe zaokrąglenie poszczególnych narożników, użyj parametru 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) )
W efekcie pojawia się różowe serce:

Jeśli żadne z poprzednich kształtów nie odpowiada Twoim potrzebom, użyj klasy Path
, aby narysować niestandardowy kształt, lub załaduj plik ImageVector
z dysku. Biblioteka graphics-shapes
nie jest przeznaczona do tworzenia dowolnych kształtów, ale ma na celu uproszczenie tworzenia zaokrąglonych wielokątów i animacji przejściowych między nimi.
Dodatkowe materiały
Więcej informacji i przykładów znajdziesz w tych materiałach:
- Blog: The Shape of Things to Come – Shapes
- Blog: przekształcanie kształtów na Androidzie
- Prezentacja kształtów na GitHubie