Funkcja Utwórz umożliwia tworzenie kształtów złożonych z wielokątów. Możesz na przykład tworzyć takie kształty:
Aby utworzyć niestandardowy okrągły wielokąt w sekcji Utwórz, dodaj zależność graphics-shapes
do app/build.gradle
:
implementation "androidx.graphics:graphics-shapes:1.0.0-alpha05"
Ta biblioteka umożliwia tworzenie kształtów złożonych z wielokątów. Chociaż kształty wielokątne mają tylko proste krawędzie i ostre rogi, te kształty umożliwiają opcjonalne zaokrąglone narożniki. Ułatwia przekształcanie 2 kształtów. Przekształcanie obiektów w obrębie dowolnych kształtów jest trudne i bywa problematyczne podczas projektowania. Ta biblioteka ułatwia jednak pracę, przekształcając kształty o podobnych wielokątnych strukturach.
Tworzenie wielokątów
Ten fragment kodu tworzy podstawowy kształt wielokąta z 6 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 element RoundedPolygon
, który zawiera geometrię reprezentującą żądany kształt. Aby narysować ten kształt w aplikacji Compose, musisz pobrać z niej obiekt Path
, aby uzyskać kształt w postaci, która potrafi rysować.
Zaokrągla rogi wielokąta
Aby zaokrąglić rogi wielokąta, użyj parametru CornerRounding
. Wymaga to 2 parametrów: radius
i smoothing
. Każdy zaokrąglony róg składa się z 1–3 krzywych sześciennych, których środek ma okrągły kształt, a krzywe dwustronne („koliczne”) przechodzą od krawędzi kształtu do krzywej środkowej.
Promień
radius
to promień koła używanego do zaokrąglenia wierzchołku.
Na przykład taki trójkąt z zaokrąglonymi rogami tworzy się w następujący sposób:
Złagodzenie
Wygładzanie to czynnik określający, ile czasu potrzeba na przejście od zaokrąglonego narożnika do krawędzi. Współczynnik wygładzania równy 0 (bez wygładzania, wartość domyślna CornerRounding
) powoduje całkowite zaokrąglenie rogów. Jeśli współczynnik wygładzania nie jest zerowy (do maksymalnej wartości 1,0), narożnik jest zaokrąglany przez 3 osobne krzywe.
Na przykład w tym fragmencie kodu widać subtelną różnicę w wygładzeniu wygładzania do wartości 0 i 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 o promieniu 1
dookoła środka (0, 0
).
Ten promień reprezentuje odległość między środkowymi a zewnętrznymi wierzchołkami wielokąta, na którym oparty jest kształt. Pamiętaj, że zaokrąglanie rogów powoduje zmniejszenie kształtu, ponieważ zaokrąglone narożniki są bliżej środka niż wierzchołki zaokrąglone. Aby zmienić rozmiar wielokąta, dostosuj wartość radius
. Aby dostosować położenie, zmień centerX
lub centerY
wielokąta.
Możesz też przekształcić obiekt, aby zmienić jego rozmiar, położenie i obrót, korzystając ze standardowych funkcji przekształcania 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ć 2 kształty, utwórz dwa obiekty RoundedPolygons
i Morph
, które mają te kształty. Aby obliczyć kształt między kształtami początkowymi i końcowymi, podaj wartość progress
z zakresu od 0 do 1, aby określić jej kształt między kształtami początkowymi (0) i końcowymi (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 przykładzie powyżej postęp jest dokładnie w połowie drogi między 2 kształtami (trójkątem zaokrąglonym i kwadratem), co daje taki wynik:
W większości przypadków przekształcanie odbywa się w ramach animacji, a nie tylko jako renderowania statycznego. Do animowania tych 2 elementów możesz używać standardowych interfejsów API animacji w tworzeniu, które pozwalają zmieniać wartość postępu w czasie. Możesz na przykład animować przejście między tymi dwoma kształtami w nieskończoność w następujący 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żyj wielokąta jako klipu
Do zmiany sposobu renderowania funkcji kompozycyjnej i lepszego wykorzystania cieni rysujących się wokół obszaru przycinania często używa się modyfikatora clip
w interfejsie Compose:
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, tak jak w tym przykładzie:
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) ) }
Efektem jest to, że:
Ta cecha może nie wyglądać podobnie do wcześniejszej renderowania, ale umożliwia korzystanie z innych funkcji tworzenia wiadomości. Tej techniki można np. używać do przycinania obrazu i nakładania 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 przesuwania po kliknięciu
Możesz użyć biblioteki graphics-shape
, aby utworzyć przycisk, który zmienia się po naciśnięciu. Najpierw utwórz MorphPolygonShape
, który rozszerza zakres Shape
, skalując i tłumacząc go, aby odpowiednio go dopasować. Zwróć uwagę na przebieg 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, utwórz dwa wielokąty, shapeA
i shapeB
. Utwórz i zapamiętaj Morph
. Następnie zastosuj przekształcenie przycisku jako kontur klipu. interactionSource
przy naciśnięciu jest siłą napędową 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)) }
Powoduje to wyświetlenie takiej animacji po kliknięciu pola:
Animuj zniekształcenie kształtu w nieskończoność
Aby animować kształt w nieskończoność, użyj funkcji rememberInfiniteTransition
.
Poniżej znajdziesz przykład zdjęcia profilowego, które zmienia kształt (i obraca się) w nieskończoność. Ta metoda wprowadza niewielką zmianę w MorphPolygonShape
pokazanych 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:
Niestandardowe wielokąty
Jeśli kształty utworzone z wielokątów foremnych nie pokrywają się z Twoim przypadkiem użycia, możesz utworzyć bardziej niestandardowy kształt z listą wierzchołków. Możesz np. utworzyć taki kształt serca:
Możesz określić poszczególne wierzchołki tego kształtu za pomocą przeciążenia RoundedPolygon
, które przyjmuje tablicę zmiennoprzecinkową ze współrzędnych x i y.
Aby rozbić wielokąt serca, zwróć uwagę, że zastosowanie współrzędnych biegunowych do określania punktów jest łatwiejsze niż użycie kartezjańskiego (x i y) układu współrzędnych, w którym 0°
rozpoczyna się po prawej stronie i działa w prawo, a 270°
jest w pozycji dwunastej:
Kształt można teraz łatwiej zdefiniować, określając kąt (⌘) i promień w każdym punkcie:
Teraz można tworzyć wierzchołki i przekazywać 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, ) }
Wierzchory należy przekształcić 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))
Poprzedni kod pokazuje surowe wierzchołki serca, ale musisz zaokrąglić konkretne narożniki, aby uzyskać wybrany kształt serca. Narożniki 90°
i 270°
nie mają zaokrąglonych rogów, natomiast pozostałe narożniki już tak. Aby niestandardowe zaokrąglić poszczególne narożniki, 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) )
Otrzymasz różowe serce:
Jeśli poprzednie kształty nie pokrywają się z Twoim przypadkiem użycia, rozważ użycie klasy Path
do narysowania kształtu niestandardowego lub wczytanie pliku ImageVector
z dysku. Biblioteka graphics-shapes
nie służy do obsługi dowolnych kształtów – ma na celu uproszczenie tworzenia zaokrąglonych wielokątów i przekształcanie między nimi animacji.
Dodatkowe materiały
Więcej informacji i przykładów znajdziesz w tych materiałach:
- Blog: kształt rzeczy, które mają pojawić się – kształty
- Blog: zmiana kształtów na Androidzie
- Prezentacja kształtów w GitHub