Za pomocą Compose możesz tworzyć kształty utworzone z poligonów. Możesz na przykład tworzyć te rodzaje kształtów:
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.0-rc01"
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. Przekształcanie dowolnych kształtów jest trudne i zwykle stanowi problem na etapie projektowania. Ta biblioteka upraszcza ten proces, przekształcając kształty z podobnymi strukturami wielokątnymi.
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ć narożniki 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:
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 zmienić rozmiar wielokąta, dostosuj wartość radius
. Aby dostosować pozycję, 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. 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 tym przykładzie postęp jest dokładnie w połowie drogi 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 kompozycyjnym, 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ć 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 to nie wyglądać zbyt różnie od 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 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)) }
Gdy klikniesz pole, zobaczysz tę animację:
animować nieskończone przekształcanie 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 dostosowanie do MorphPolygonShape
pokazanego 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 system współrzędnych biegunowych ułatwia określanie punktów niż system współrzędnych kartezjańskich (x, y), w którym 0°
zaczyna się po prawej stronie i przechodzi zgodnie z kierunkiem ruchu wskazówek zegara, a 270°
znajduje się w pozycji godziny 12:
Kształt można teraz definiować łatwiej, 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 tych kształtów nie odpowiada Twoim potrzebom, użyj klasy Path
, aby narysować własny 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