Kami baru saja merilis dokumentasi baru yang membahas cara menggunakan library graphics-shapes
di Jetpack Compose. Meskipun dokumentasi itu membahas dasar-dasarnya, saya pikir akan menyenangkan mencoba sesuatu yang sedikit lebih rumit dan membuat status progres yang terlihat berbeda dari versi standar yang biasa kita gunakan.
Dalam postingan blog ini, kita akan membahas cara membuat status progres yang bertransisi dari poligon bulat berbentuk "bintang" berlekuk-lekuk menjadi lingkaran sembari menampilkan animasi progres reguler.
Membuat poligon
Langkah pertama yang ingin kita lakukan adalah membuat transisi dari lingkaran ke lingkaran berlekuk-lekuk, jadi kita membuat dua bentuk yang perlu kita transformasikan.
Kita menggunakan RoundedPolygon#star()
, karena ini memungkinkan kita menetapkan radius bagian dalam untuk bentuk dengan sudut membulat, dan RoundedPolygon#circle()
untuk bentuk lingkaran.
val starPolygon = remember {
RoundedPolygon.star(
numVerticesPerRadius = 12,
innerRadius = 1f / 3f,
rounding = CornerRounding(1f / 6f))
}
val circlePolygon = remember {
RoundedPolygon.circle(
numVertices = 12
)
}
Transformasi di antara dua bentuk
Untuk melakukan transformasi di antara dua poligon, kita perlu membuat objek Morph
:
val morph = remember {
Morph(starPolygon, circlePolygon)
}
Ini akan digunakan dengan nilai progres animasi untuk menentukan progres transformasi di antara kedua bentuk ini. Untuk menggambar objek Morph
, kita perlu mendapatkan objek Path
dari geometrinya, yang kita buat dengan menggunakan metode pembantu berikut:
fun Morph.toComposePath(progress: Float, scale: Float = 1f, path: Path = Path()): Path {
var first = true
path.rewind()
forEachCubic(progress) { bezier ->
// move to the initial position if its the first cubic curve
if (first) {
path.moveTo(bezier.anchor0X * scale, bezier.anchor0Y * scale)
first = false
}
// add cubic curve to the current path for each curve in the Morph
path.cubicTo(
bezier.control0X * scale, bezier.control0Y * scale,
bezier.control1X * scale, bezier.control1Y * scale,
bezier.anchor1X * scale, bezier.anchor1Y * scale
)
}
path.close()
return path
}
Dengan Path Morph
, kita bisa memanggil DrawScope#drawPath()
untuk menggambar bentuk transformasi animasi kita:
val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val progress = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
tween(4000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "progress"
)
//.. shapes that are created .. //
var morphPath = remember {
Path()
}
Box(
modifier = Modifier
.padding(16.dp)
.drawWithCache {
morphPath = morph
.toComposePath(progress = progress.value, scale = size.minDimension / 2f, path = morphPath)
onDrawBehind {
translate(size.width / 2f, size.height / 2f) {
drawPath(morphPath, color = Color.Black, style = Stroke(16.dp.toPx()))
}
}
}
)
Sekarang kita bisa memutar bentuk seiring waktu dengan membuat variabel animasi lain untuk rotasi dan memanggil DrawScope#rotate()
.
val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val progress = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
tween(4000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "progress"
)
val rotation = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
tween(4000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "rotation"
)
Box(
modifier = Modifier
.padding(16.dp)
.drawWithCache {
morphPath = morph
.toComposePath(progress = progress.value, scale = size.minDimension / 2f, path = morphPath)
onDrawBehind {
rotate(rotation.value){
translate(size.width / 2f, size.height / 2f) {
drawPath(morphPath, color = Color.Black, style = Stroke(16.dp.toPx()))
}
}
Langkah itu akan menghasilkan animasi bentuk yang berputar dan bertransformasi seperti berikut ini:
Menggambar jalur secara progresif seiring waktu
Kita punya jalurnya, dan kita membuatnya berputar seiring waktu, tetapi hasil akhir di atas menunjukkan bahwa kita hanya menggambar segmen jalur dan bukan jalur penuh. Bagaimana kita bisa mencapainya?
Pertama, kita perlu mengetahui panjang jalur, yang dapat kita peroleh dari PathMeasure
. Kemudian kita bisa menggunakan PathMeasure.getSegment()
untuk mendapatkan sebagian dari jalur, berdasarkan progres saat ini (totalLength * progress.value
), dengan hasil yang digunakan di objek jalur baru yang disebut destinationPath
.
Segmen jalur ini kemudian dapat digambar seperti berikut ini:
val pathMeasurer = remember {
PathMeasure()
}
var morphPath = remember {
Path()
}
//..
// in drawWithCache:
morphPath = morph
.toComposePath(
progress = progress.value,
scale = size.minDimension / 2f,
path = morphPath
)
pathMeasurer.setPath(morphPath, false)
val totalLength = pathMeasurer.length
destinationPath.reset()
pathMeasurer.getSegment(0f, totalLength * progress.value, destinationPath)
onDrawBehind {
rotate(rotation.value) {
translate(size.width / 2f, size.height / 2f) {
val brush = Brush.sweepGradient(colors, center = Offset(0.5f, 0.5f))
drawPath(destinationPath, brush, style = Stroke(16.dp.toPx(), cap = StrokeCap.Round))
}
}
}
Tanpa rotasi, ini memberikan hasil seperti berikut:
Dengan penambahan animasi untuk transformasi dan rotasi, kita bisa melihatnya secara perlahan-lahan bertransformasi menjadi lingkaran saat menggambar garis:
Menggambar warna gradien seiring waktu dengan garis
Sekarang karena kita sudah memiliki gambar jalur seiring waktu, kita ingin menerapkan gradien pada jalur tersebut. Pendekatan yang paling mudah adalah dengan menetapkan Brush.linearGradient()
menggunakan warna yang kita inginkan untuk setiap operasi menggambar. Namun jika kita menjalankannya, kita bisa melihat bahwa ia tidak memberikan efek persis seperti yang diinginkan, gradien diterapkan pada seluruh jalur dalam satu arah, dan tidak mengikuti arah garis.
Dari gambar di bawah, Anda bisa melihat, bahwa garis ini mengikuti satu arah pada seluruh bentuk, padahal kita sebenarnya menghendaki agar garis ini berubah warna saat garis digambar di tempatnya.
Masih ingat dengan pena gel pelangi yang sangat menarik yang dulu pernah Anda miliki? Kita ingin menerapkan efek tersebut pada bentuk kami — berubah warna mengikuti arah garis yang digambar.
Untuk melakukannya, kita bisa menggunakan Brush.sweepGradient()
dengan warna yang disediakan, ini memberikan efek gradien yang digambar seiring waktu.
val brush = Brush.sweepGradient(colors, center = Offset(0.5f, 0.5f))
Yang memberi kita hasil berikut:
Ini terlihat bagus! Namun, jika kita ingin punya sesuatu yang lebih umum yang dapat digunakan untuk menggambar jalur yang berubah-ubah, kita harus mengubah implementasinya menjadi seperti contoh ini.
Ringkasan
Library graphics-shapes
yang baru membuka berbagai macam kemungkinan bentuk baru di Android. Contoh dalam artikel ini membuat bentuk dan menggunakannya untuk membuat status progres melingkar kustom, tetapi ada banyak kemungkinan lain yang dapat dieksplorasi dengan API baru ini untuk membuat bentuk bulat dan mentransformasikannya. Cuplikan kode selengkapnya bisa ditemukan di sini.
Berkreasi dan buatlah bentuk-bentuk yang menyenangkan! 🟩🟡💜