Funkcja Utwórz umożliwia tworzenie kształtów wykonanych z wielokątów. Możesz na przykład tworzyć te rodzaje kształtów:

Aby utworzyć niestandardowy okrągły wielokąt w widoku tworzenia, dodaj
zależność graphics-shapes
od
app/build.gradle
:
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, można w ich przypadku użyć opcjonalnych zaokrąglonych rogów. Dzięki temu możesz łatwo przekształcać jeden kształt w inny. Zmiana kształtów jest trudna i często na czas projektowania. Ta biblioteka upraszcza jednak ten proces, o podobnych wielokątach.
Utwórz wielokąty
Ten fragment tworzy prosty kształt wielokąta z sześcioma punktami poś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ć taki kształt w aplikacji Compose,
należy uzyskać z niego obiekt Path
, aby przekształcić kształt
który umie rysować.
Zaokrąglanie narożników wielokąta
Aby zaokrąglić rogi wielokąta, użyj parametru CornerRounding
. Ten
przyjmuje 2 parametry: radius
i smoothing
. Każdy zaokrąglony róg składa się
od 1 do 3 krzywych sześciennych,
krzywe boczne („przesuwające się”) przechodzą od krawędzi kształtu do środkowej krzywej.
Promień
radius
to promień okręgu służącego 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
narożnika zaokrąglonego przez trzy 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 pozycja
Domyślnie tworzony jest kształt z promieniem 1
wokół środka (0, 0
).
Ten promień reprezentuje odległość między środkiem a wierzchołkami zewnętrznymi
wielokąta, na którym oparty jest jego kształt. Pamiętaj, że zaokrąglanie rogów
uzyskać mniejszy kształt, ponieważ zaokrąglone rogi będą bliżej
niż zaokrąglone wierzchołki. Aby zmienić rozmiar wielokąta, dostosuj wartość radius
. Aby dostosować położenie, zmień centerX
lub centerY
wielokąta.
Możesz też zmienić rozmiar, położenie i obrót obiektu za pomocą 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
kształtów. 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 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 (trójkąt zaokrąglony i kwadrat) daje taki wynik:

W większości przypadków przekształcanie jest wykonywane w ramach animacji, a nie tylko statycznych renderów. Aby przełączać się między nimi, możesz użyć standardowego Interfejsy API do animacji w Compose, które zostaną zmienione 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
Powszechnie służy funkcja
clip
w Compose zmienił sposób renderowania elementu kompozycyjnego.
zalety cieni, które rysują się wokół obszaru przycięcia:
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ć 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) ) }
W efekcie:

Może nie wyglądać tak samo jak to, co było wyświetlane wcześniej, ale pozwala który pozwala korzystać z innych funkcji narzędzia 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 zmienia się
dwa kształty po naciśnięciu. Najpierw utwórz obiekt MorphPolygonShape
, który rozszerza obiekt Shape
, skalując i przekształcając go w odpowiednim zakresie. Zwróć uwagę na wprowadzenie
tak 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 przejścia, utwórz 2 wieloboki: shapeA
i shapeB
. Utwórz i zapamiętaj Morph
. Następnie zastosuj przekształcenie do przycisku jako obrysu 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)) }
Po kliknięciu pola pojawia się ta animacja:

Nieskończonie animuj kształt
Aby utworzyć nieskończoną animację przekształcania kształtu, użyj rememberInfiniteTransition
.
Poniżej znajduje się przykład zdjęcia profilowego, które zmienia kształt (i obraca się)
bez końca. Ta metoda polega na niewielkim dostosowaniu
Widoczna powyżej wartość MorphPolygonShape
:
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 ciekawy wynik:

Wielokąty niestandardowe
Jeśli kształty utworzone ze zwykłych wielokątów nie nadają się do zastosowania, możesz aby uzyskać niestandardowy kształt z listą wierzchołków. Możesz na przykład utworzyć kształt serca:

Pojedyncze wierzchołki tego kształtu można określić za pomocą funkcji RoundedPolygon
które wykorzystuje tablicę zmiennoprzecinkową współrzędnych x i y.
Aby rozbić wielokąt w kształcie serca, zwróć uwagę, że układ współrzędnych biegunowych dla argumentu
określenie punktów jest łatwiejsze niż używanie współrzędnych kartezjańskich (x,y).
w którym 0°
rozpoczyna się po prawej stronie i porusza się w prawo,
270°
na godzinie 12:

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

Możesz teraz utworzyć wierzchołki i przekazać je 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 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))
Poprzedni kod zapewnia nieprzetworzone wierzchołki serca, ale musisz
i zaokrąglanych rogów, aby uzyskać wybrany kształt serca. Narożniki: 90°
i
270°
nie ma zaokrąglenia, ale pozostałe rogi mają. Aby uzyskać niestandardowe zaokrąglanie
dla 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) )
Tak wygląda różowe serce:

Jeśli poprzednie kształty nie mają zastosowania, rozważ użycie Path
klasy do rysowania niestandardowej
kształtu lub wczytywanie
ImageVector
plik 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: zmiany kształtu na Androidzie
- Prezentacja kształtów na GitHubie