Dengan Compose, Anda dapat membuat bentuk yang dibuat dari poligon. Misalnya, Anda dapat membuat jenis bentuk berikut:
Untuk membuat poligon bulat kustom di Compose, tambahkan
dependensi graphics-shapes
ke
app/build.gradle
:
implementation "androidx.graphics:graphics-shapes:1.0.0-alpha05"
Library ini memungkinkan Anda membuat bentuk yang dibuat dari poligon. Meskipun bentuk poligon hanya memiliki tepi lurus dan sudut tajam, bentuk ini memungkinkan sudut membulat opsional. Membuatnya mudah untuk berubah antara dua bentuk yang berbeda. Morphing sulit dilakukan di antara bentuk yang berubah-ubah, dan cenderung menjadi masalah waktu desain. Namun, library ini membuatnya sederhana dengan melakukan morphing di 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 mewakili bentuk yang diminta. Untuk menggambar bentuk tersebut di aplikasi Compose,
Anda harus mendapatkan objek Path
darinya untuk mengubah bentuk menjadi bentuk yang diketahui
oleh Compose cara menggambarnya.
Membulatkan sudut poligon
Untuk membulatkan sudut poligon, gunakan parameter CornerRounding
. Dibutuhkan dua parameter, radius
dan smoothing
. Setiap sudut membulat terdiri dari
1-3 kurva kubik, yang bagian tengahnya memiliki bentuk busur lingkaran, sedangkan dua
sisi ("mengapit") bertransisi dari tepi suatu bentuk ke kurva tengah.
Radius
radius
adalah radius lingkaran yang digunakan untuk membulatkan verteks.
Misalnya, segitiga sudut membulat berikut dibuat sebagai berikut:
Kehalusan
Penghalusan adalah faktor yang menentukan waktu yang diperlukan untuk berpindah dari
bagian sudut yang membulat pada sudut ke tepi. Faktor penghalusan 0
(tidak dihaluskan, nilai default untuk CornerRounding
) menghasilkan pembulatan sudut
yang benar-benar melingkar. Faktor smoothing bukan nol (hingga maksimum 1,0) menghasilkan
sudut yang dibulatkan oleh tiga kurva terpisah.
Misalnya, cuplikan di bawah mengilustrasikan perbedaan kecil dalam menyetel penghalusan 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 eksterior
poligon yang menjadi dasar bentuk. Perhatikan bahwa pembulatan sudut
akan menghasilkan bentuk yang lebih kecil karena sudut yang membulat akan lebih dekat ke
tengah daripada verteks yang membulat. Untuk mengukur 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 mewakili animasi antara dua bentuk poligon. Untuk beralih di antara dua bentuk, buat dua objek RoundedPolygons
dan objek Morph
yang menggunakan dua bentuk ini. Untuk menghitung bentuk 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() )
Pada contoh di atas, progresnya tepat di tengah-tengah antara dua bentuk (segitiga bulat dan persegi), sehingga memberikan hasil berikut:
Dalam sebagian besar skenario, morphing dilakukan sebagai bagian dari animasi, bukan hanya rendering statis. Untuk membuat animasi di antara keduanya, Anda dapat menggunakan Animation API di Compose standar untuk mengubah nilai progres dari waktu ke waktu. Misalnya, Anda dapat menganimasikan tanpa batas morph antara dua bentuk ini seperti 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() )
Gunakan poligon sebagai klip
Sudah umum menggunakan pengubah
clip
di Compose untuk mengubah cara composable dirender, dan untuk
memanfaatkan bayangan yang menggambar di sekitar area kliping:
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) } }
Selanjutnya, Anda dapat menggunakan poligon sebagai klip, seperti yang ditunjukkan pada 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 hal berikut:
Ini mungkin tidak terlihat berbeda dari yang dirender sebelumnya, tetapi memungkinkan untuk memanfaatkan fitur lainnya di Compose. Misalnya, teknik ini dapat digunakan untuk memangkas gambar dan menerapkan bayangan di sekitar area yang diklip:
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 akan berubah menjadi
dua bentuk saat ditekan. Pertama, buat MorphPolygonShape
yang memperluas Shape
,
menskalakan dan menerjemahkannya agar sesuai. Perhatikan penerusan
progres agar 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 ditekan sebagai kekuatan pendorong di belakang
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)) }
Hal ini menghasilkan animasi berikut saat kotak diketuk:
Menganimasikan bentuk yang berubah tanpa batas
Untuk menganimasikan bentuk morph tanpa henti, gunakan
rememberInfiniteTransition
.
Di bawah ini adalah contoh foto profil yang berubah bentuk (dan berputar)
terus-menerus dari waktu ke 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 menyenangkan berikut:
Poligon kustom
Jika bentuk yang dibuat dari poligon reguler tidak mencakup kasus penggunaan Anda, Anda dapat membuat bentuk yang lebih khusus dengan daftar verteks. Misalnya, Anda mungkin ingin membuat bentuk hati seperti ini:
Anda dapat menentukan setiap verteks dari bentuk ini menggunakan overload RoundedPolygon
yang mengambil array float dari koordinat x, y.
Untuk mengelompokkan poligon jantung, perhatikan bahwa sistem koordinat kutub untuk
menentukan titik membuatnya lebih mudah daripada menggunakan sistem koordinat
kartesius (x,y), dengan 0°
dimulai di sisi kanan, dan dilanjutkan searah jarum jam, dengan
270°
pada posisi pukul 12:
Bentuk sekarang dapat ditentukan dengan cara yang lebih mudah dengan menentukan sudut (°) dan radius dari pusat pada setiap titik:
Verteks sekarang 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, ) }
Verteks perlu diterjemahkan ke 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 harus
membulatkan sudut tertentu untuk mendapatkan bentuk hati yang dipilih. Sudut pada 90°
dan 270°
tidak memiliki pembulatan, sedangkan sudut lainnya memiliki pembulatan. Untuk mendapatkan pembulatan kustom bagi 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) )
Hasilnya adalah hati 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 dimaksudkan untuk digunakan untuk bentuk
arbitrer, tetapi secara khusus dimaksudkan untuk menyederhanakan pembuatan poligon bulat dan
mengubah animasi di antara bentuk tersebut.
Referensi tambahan
Untuk informasi selengkapnya dan contoh, lihat referensi berikut:
- Blog: Bentuk Berbagai Hal yang Akan Datang - Bentuk
- Blog: Shape morphing di Android
- Demonstrasi GitHub bentuk