Dengan Compose, Anda dapat membuat bentuk yang dibuat dari poligon. Misalnya, Anda dapat membuat bentuk berikut:

Untuk membuat poligon melengkung kustom di Compose, tambahkan dependensi
graphics-shapes
ke
app/build.gradle
Anda:
implementation "androidx.graphics:graphics-shapes:1.0.1"
Dengan library ini, Anda dapat membuat bentuk yang dibuat dari poligon. Meskipun bentuk poligon hanya memiliki tepi lurus dan sudut tajam, bentuk ini memungkinkan sudut bulat opsional. Hal ini menyederhanakan perubahan bentuk antara dua bentuk yang berbeda. Transformasi sulit dilakukan antara bentuk arbitrer, dan cenderung menjadi masalah waktu desain. Namun, library ini menyederhanakannya dengan melakukan transformasi antara bentuk-bentuk ini dengan struktur poligonal yang serupa.
Membuat poligon
Cuplikan berikut membuat bentuk poligon dasar dengan 6 titik di tengah area gambar:
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() )

Dalam contoh ini, library membuat RoundedPolygon
yang menyimpan geometri yang merepresentasikan bentuk yang diminta. Untuk menggambar bentuk tersebut di aplikasi Compose,
Anda harus mendapatkan objek Path
dari objek tersebut untuk mendapatkan bentuk ke dalam formulir yang diketahui cara menggambarnya oleh Compose.
Membulatkan sudut poligon
Untuk membulatkan sudut poligon, gunakan parameter CornerRounding
. Metode ini
menggunakan dua parameter, radius
dan smoothing
. Setiap sudut lengkung terdiri dari 1-3 kurva kubik, yang bagian tengahnya memiliki bentuk busur lingkaran, sedangkan dua kurva samping ("flanking") bertransisi dari tepi bentuk ke kurva tengah.
Radius
radius
adalah radius lingkaran yang digunakan untuk membulatkan verteks.
Misalnya, segitiga sudut bulat berikut dibuat sebagai berikut:


r
menentukan ukuran pembulatan melingkar dari
sudut yang dibulatkan.Kehalusan
Penghalusan adalah faktor yang menentukan durasi yang diperlukan untuk berpindah dari bagian pembulatan melingkar sudut ke tepi. Faktor penghalus 0
(tidak dihaluskan, nilai default untuk CornerRounding
) menghasilkan pembulatan sudut
yang murni melingkar. Faktor penghalus bukan nol (hingga maksimum 1,0) akan menghasilkan
sudut yang dibulatkan oleh tiga kurva terpisah.


Misalnya, cuplikan di bawah ini mengilustrasikan perbedaan halus dalam menyetel smoothing ke 0 versus 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) )

Ukuran dan posisi
Secara default, bentuk dibuat dengan radius 1
di sekitar pusat (0, 0
).
Radius ini mewakili jarak antara pusat dan verteks luar
poligon tempat bentuk didasarkan. Perhatikan bahwa membulatkan sudut akan menghasilkan bentuk yang lebih kecil karena sudut yang dibulatkan akan lebih dekat ke tengah daripada verteks yang dibulatkan. Untuk menentukan ukuran poligon, sesuaikan nilai radius
. Untuk menyesuaikan posisi, ubah centerX
atau centerY
poligon.
Atau, ubah objek untuk mengubah ukuran, posisi, dan rotasinya menggunakan fungsi transformasi DrawScope
standar seperti DrawScope#translate()
.
Bentuk morph
Objek Morph
adalah bentuk baru yang merepresentasikan animasi antara dua bentuk
poligonal. Untuk melakukan perubahan bentuk antara dua bentuk, buat dua RoundedPolygons
dan objek Morph
yang menggunakan kedua bentuk ini. Untuk menghitung bentuk di antara bentuk awal dan akhir, berikan nilai progress
antara nol dan satu untuk menentukan bentuknya di antara bentuk awal (0) dan akhir (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() )
Dalam contoh di atas, progresnya tepat di tengah antara dua bentuk (segitiga bulat dan persegi), sehingga menghasilkan hasil berikut:

Dalam sebagian besar skenario, pengubahan bentuk dilakukan sebagai bagian dari animasi, dan bukan hanya rendering statis. Untuk menganimasikan keduanya, Anda dapat menggunakan Animation API standar di Compose untuk mengubah nilai progres seiring waktu. Misalnya, Anda dapat menganimasikan morph secara tak terbatas di antara kedua bentuk ini sebagai berikut:
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() )

Menggunakan poligon sebagai klip
Penggunaan pengubah
clip
di Compose untuk mengubah cara rendering composable, dan memanfaatkan
bayangan yang digambar di sekitar area kliping adalah hal yang umum:
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) } }
Kemudian, Anda dapat menggunakan poligon sebagai klip, seperti yang ditunjukkan dalam cuplikan berikut:
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) ) }
Hal ini menghasilkan:

Mungkin tidak terlihat berbeda dengan rendering sebelumnya, tetapi memungkinkan pemanfaatan fitur lain di Compose. Misalnya, teknik ini dapat digunakan untuk memangkas gambar dan menerapkan bayangan di sekitar area yang dipangkas:
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) ) }

Tombol morph saat diklik
Anda dapat menggunakan library graphics-shape
untuk membuat tombol yang berubah bentuk antara
dua bentuk saat ditekan. Pertama, buat MorphPolygonShape
yang memperluas Shape
,
menskalakan dan menerjemahkannya agar sesuai. Perhatikan penerusan
progres sehingga bentuk dapat dianimasikan:
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) } }
Untuk menggunakan bentuk morph ini, buat dua poligon, shapeA
dan shapeB
. Buat dan
ingat Morph
. Kemudian, terapkan morph ke tombol sebagai garis batas klip,
menggunakan interactionSource
saat tombol ditekan sebagai pendorong
animasi:
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)) }
Tindakan ini akan menghasilkan animasi berikut saat kotak diketuk:

Menganimasikan perubahan bentuk tanpa batas
Untuk menganimasikan bentuk morph tanpa henti, gunakan
rememberInfiniteTransition
.
Di bawah ini adalah contoh foto profil yang berubah bentuk (dan berputar) tanpa batas seiring waktu. Pendekatan ini menggunakan penyesuaian kecil pada
MorphPolygonShape
yang ditampilkan di atas:
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) ) } }
Kode ini memberikan hasil menarik berikut:

Poligon kustom
Jika bentuk yang dibuat dari poligon reguler tidak mencakup kasus penggunaan Anda, Anda dapat membuat bentuk yang lebih kustom dengan daftar verteks. Misalnya, Anda mungkin ingin membuat bentuk hati seperti ini:

Anda dapat menentukan setiap verteks bentuk ini menggunakan penggantian RoundedPolygon
yang menggunakan array float koordinat x, y.
Untuk menguraikan poligon hati, perhatikan bahwa sistem koordinat polar untuk menentukan titik membuatnya lebih mudah daripada menggunakan sistem koordinat kartesius (x,y), dengan 0°
dimulai di sisi kanan, dan berlanjut searah jarum jam, dengan 270°
di posisi pukul 12:

Bentuk kini dapat ditentukan dengan lebih mudah dengan menentukan sudut (𝜭) dan radius dari tengah di setiap titik:

Sekarang verteks dapat dibuat dan diteruskan ke fungsi 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, ) }
Vertex perlu diterjemahkan ke dalam koordinat kartesius menggunakan fungsi
radialToCartesian
ini:
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))
Kode sebelumnya memberi Anda verteks mentah untuk hati, tetapi Anda perlu membulatkan sudut tertentu untuk mendapatkan bentuk hati yang dipilih. Sudut di 90°
dan
270°
tidak memiliki pembulatan, tetapi sudut lainnya memiliki pembulatan. Untuk mendapatkan pembulatan kustom
untuk setiap sudut, gunakan parameter 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) )
Hal ini akan menghasilkan hati berwarna merah muda:

Jika bentuk sebelumnya tidak mencakup kasus penggunaan Anda, pertimbangkan untuk menggunakan class Path
untuk menggambar bentuk
kustom, atau memuat file
ImageVector
dari
disk. Library graphics-shapes
tidak ditujukan untuk digunakan pada bentuk
arbitrary, tetapi secara khusus dimaksudkan untuk menyederhanakan pembuatan poligon bulat dan
animasi morph di antaranya.
Referensi lainnya
Untuk informasi dan contoh selengkapnya, lihat referensi berikut:
- Blog: Bentuk yang Akan Datang - Bentuk
- Blog: Transformasi bentuk di Android
- Demonstrasi GitHub Shapes