W trybie Kompozycja możesz tworzyć kształty składające się z wielokątów. Możesz na przykład tworzyć te rodzaje kształtów:

Aby utworzyć w Compose niestandardowy zaokrąglony wielokąt, dodaj zależność graphics-shapes
do pliku app/build.gradle
:
implementation "androidx.graphics:graphics-shapes:1.0.1"
Ta biblioteka umożliwia tworzenie kształtów z wielokątów. Kształty wielokątne mają tylko proste krawędzie i ostre rogi, ale mogą mieć opcjonalnie zaokrąglone rogi. Ułatwia to płynne przejście między dwoma różnymi kształtami. Przekształcanie między dowolnymi kształtami jest trudne i zwykle stanowi problem na etapie projektowania. Ta biblioteka ułatwia jednak to zadanie, ponieważ umożliwia płynne przechodzenie między tymi kształtami o podobnych strukturach wielokątnych.
Tworzenie wielokątów
Ten fragment kodu tworzy podstawowy wielokąt z 6 punktami na środku obszaru rysowania:
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 obiekt RoundedPolygon
, który zawiera geometrię reprezentującą żądany kształt. Aby narysować ten kształt w aplikacji Compose, musisz uzyskać z niego obiekt Path
, aby przekształcić go w formę, którą Compose potrafi narysować.
Zaokrąglanie 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 kołowego, a dwie krzywe boczne („flankujące”) przechodzą od krawędzi kształtu do krzywej środkowej.
Promień
radius
to promień okręgu używanego do zaokrąglania wierzchołka.
Na przykład ten trójkąt z zaokrąglonymi rogami powstaje w ten sposób:


r
określa rozmiar zaokrąglenia rogów.Złagodzenie
Wygładzanie to czynnik określający, jak długo trwa przejście od zaokrąglonej części rogu do krawędzi. Współczynnik wygładzania 0 (niewygładzony, domyślna wartość CornerRounding
) powoduje zaokrąglenie narożników w postaci czystego okręgu. Niezerowy współczynnik wygładzania (maksymalnie 1,0) powoduje, że narożnik jest zaokrąglany przez 3 osobne krzywe.


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ętrznymi wierzchołkami wielokąta, na którym oparty jest kształt. Pamiętaj, że zaokrąglenie rogów powoduje zmniejszenie kształtu, ponieważ zaokrąglone rogi będą bliżej środka niż zaokrąglane wierzchołki. Aby zmienić rozmiar wielokąta, dostosuj wartość radius
. Aby dostosować pozycję, zmień centerX
lub centerY
wielokąta.
Możesz też przekształcić obiekt, aby zmienić jego rozmiar, położenie i rotację, korzystając ze standardowych funkcji przekształcania, takich jak DrawScope
DrawScope#translate()
.
Przekształcanie kształtów
Obiekt Morph
to nowy kształt reprezentujący animację między dwoma wielokątnymi kształtami. Aby przekształcić jeden kształt w drugi, utwórz 2 obiekty RoundedPolygons
i 1 obiekt Morph
, który przyjmuje te 2 kształty. Aby obliczyć kształt między kształtem początkowym a końcowym, podaj wartość progress
z zakresu od 0 do 1, aby określić jego formę między kształtem początkowym (0) a końcowym (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() )
W powyższym przykładzie postęp jest dokładnie w połowie drogi między dwoma kształtami (zaokrąglonym trójkątem i kwadratem), co daje następujący wynik:

W większości przypadków przekształcanie jest częścią animacji, a nie tylko statycznego renderowania. Aby animować przejście między tymi 2 wartościami, możesz użyć standardowych interfejsów API animacji w Compose, aby zmieniać wartość postępu w czasie. Możesz na przykład nieskończenie animować przekształcanie między tymi dwoma kształtami w ten sposób:
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 Compose często używa się modyfikatora
clip
do zmiany sposobu renderowania komponentu kompozycyjnego i korzystania 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) } }
Wielokąt możesz następnie wykorzystać jako klip, 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) ) }
Spowoduje to:

Może to nie wyglądać inaczej niż wcześniej, ale pozwala na korzystanie z innych funkcji w Compose. Na przykład tę technikę można wykorzystać do przycięcia obrazu i zastosowania cienia wokół przycię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 przekształcenia po kliknięciu
Za pomocą biblioteki graphics-shape
możesz utworzyć przycisk, który po naciśnięciu zmienia kształt. Najpierw utwórz element MorphPolygonShape
, który rozszerza element Shape
, skalując go i przesuwając, aby odpowiednio dopasować go do ekranu. Zwróć uwagę na przekazywanie wartości zmiennej progress, aby można było animować kształt:
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, utwórz 2 wielokąty: shapeA
i shapeB
. Utwórz i zapamiętaj Morph
. Następnie zastosuj przekształcenie do przycisku jako obramowania klipu, używając interactionSource
po naciśnięciu 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)) }
Po kliknięciu pola pojawi się taka animacja:

Animowanie przekształcania kształtu w nieskończoność
Aby animować kształt przekształcenia w nieskończoność, użyj rememberInfiniteTransition
.
Poniżej znajdziesz przykład zdjęcia profilowego, które zmienia kształt (i obraca się) w nieskończoność. W tym podejściu stosuje się niewielką korektę w stosunku do wartości MorphPolygonShape
podanej 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 wielokątów foremnych nie spełniają Twoich oczekiwań, możesz utworzyć bardziej niestandardowy kształt za pomocą listy wierzchołków. Możesz na przykład utworzyć kształt serca w ten sposób:

Poszczególne wierzchołki tego kształtu możesz określić za pomocą przeciążenia RoundedPolygon
, które przyjmuje tablicę liczb zmiennoprzecinkowych współrzędnych x i y.
Aby podzielić wielokąt serca, zauważ,że układ współrzędnych biegunowych do określania punktów ułatwia to zadanie w porównaniu z układem współrzędnych kartezjańskich (x, y), w którym 0°
zaczyna się po prawej stronie i przebiega zgodnie z ruchem wskazówek zegara, a 270°
znajduje się na pozycji godziny 12:

Kształt można teraz łatwiej zdefiniować, określając kąt (𝜭) i promień od środka w każdym punkcie:

Wierzchołki można teraz tworzyć i przekazywać do 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 przetłumaczyć na 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 zawiera surowe wierzchołki serca, ale aby uzyskać wybrany kształt, musisz zaokrąglić określone rogi. Rogi w punktach 90°
i 270°
nie są zaokrąglone, ale pozostałe rogi są. Aby uzyskać niestandardowe zaokrąglenie poszczególnych rogó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 otrzymasz różowe serce:

Jeśli powyższe kształty nie odpowiadają Twoim potrzebom, możesz użyć klasy Path
, aby narysować niestandardowy kształt, lub wczytać plik ImageVector
z dysku. Biblioteka graphics-shapes
nie jest przeznaczona do używania w przypadku dowolnych kształtów, ale ma na celu uproszczenie tworzenia zaokrąglonych wielokątów i animacji przekształcania 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
- Demonstracja kształtów w GitHubie