Software Engineer
Developer Relations Engineer
Siap mengubah UI Android Anda dari "berfungsi" menjadi "karya seni"?
Bergabunglah dengan kami untuk mempelajari kemampuan grafis terbaru di Jetpack Compose. Kami akan menunjukkan cara membuat efek visual baru, mulai dari gradien mesh yang menarik hingga blur progresif, dan menggunakan fitur grafis baru yang canggih di Android. Kami juga akan menunjukkan cara meningkatkan performa fitur grafis, tanpa mengorbankan masa pakai baterai atau kecepatan frame.
PolygonShape dan MorphPolygonShape native untuk geometri kustom. MorphPolygonShape membaca progres selama fase resolusi garis batas, memperbarui bentuk frame demi frame tanpa memicu rekomposisi.RenderNode.setBackdropRenderEffect untuk mengambil sampel konten latar belakang. Developer dapat menggabungkan efek latar belakang untuk membuat efek kaca buram atau pembesar.graphicsLayer.Transkrip
Halo semuanya, dan selamat datang di sesi kami, Meningkatkan Kualitas Aplikasi Anda dengan Fitur Grafis Terbaru di Compose dan Android.
Nama saya Rebecca Franks, dan yang bergabung dengan saya di panggung hari ini adalah--
Saya Aayush Chaudhary.
Halo, saya Rahul Ray.
Kami melihat tren yang muncul dalam cara Anda semua mendobrak batasan yang mungkin ada di Android dan Compose, menggunakan AI untuk benar-benar mewujudkan ide-ide luar biasa Anda.
Kami juga senang melihat aplikasi yang terasa premium, native, dan menyenangkan.
Kami tahu bahwa aplikasi yang terasa nyaman dan secara konsisten mendapatkan rating yang bagus dari pengguna adalah aplikasi yang umumnya memanfaatkan perubahan API tingkat sistem terbaru, memiliki kontinuitas antara platform dengan elemen bersama dan pemburaman, beradaptasi dengan baik di berbagai faktor bentuk, dan berperforma tinggi, serta tidak membuat pengguna menebak-nebak, dan mendorong batas-batas yang mungkin dilakukan dengan perangkat Anda.
Dalam diskusi hari ini, kami akan menunjukkan beberapa fitur terbaru yang membantu Anda memanfaatkan Android, mulai dari menyenangkan pengguna dengan gradien mesh, mengaktifkan dukungan gamut warna yang luas, transisi yang menyenangkan dengan bentuk poligon, hal baru dalam pemburaman, dan, terakhir, kami akan membahas cara Anda memikirkan performa saat menambahkan fitur grafis baru ke aplikasi Anda.
Namun, dalam diskusi hari ini, kita akan membahas beberapa API baru di berbagai level stack.
Meskipun toolkit UI tampilan lama kini dalam mode pemeliharaan, kami terus berinvestasi dalam mesin rendering hardware grafis Android yang mendasari dan mendukung Jetpack Compose.
Anda dapat menganggap grafis di Android sebagai tiga lapisan yang berbeda: Compose API di lapisan pertama, dan di sinilah semua API grafis UI baru tingkat tinggi, seperti painter gradien mesh, bentuk poligon, dan API blur progresif berada.
Kemudian, kita memiliki lapisan perantara, tempat DrawScope Compose, node pengubah gambar, dan AGSL memungkinkan Anda membuat efek kustom.
Namun, terakhir, kita memiliki platform Android, dan Compose selalu dapat mengakses Canvas native dan menggunakan primitif rendering OS baru langsung di dalam pengubah Compose Anda.
Kami juga akan menunjukkan beberapa contoh cara melakukannya saat ini.
Mari kita mulai.
Jadi, pertama-tama, mari kita lihat gradien mesh, API baru yang diperkenalkan di Compose 1.12.
Sebelumnya, kita memiliki gradien linear, gradien sapuan, dan gradien radial, yang sangat bagus untuk transisi sederhana, tetapi jika Anda menginginkan perpaduan kaya warna seperti latar belakang ambient atau efek pencahayaan, Anda memerlukan sistem petak 2D yang posisi dan warnanya dapat berpadu di seluruh ruang.
Sekarang, untuk membuat gradien mesh seperti ini, pertama-tama, tentukan jumlah baris dan kolom patch yang Anda inginkan.
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}Kemudian, di dalam lambda akhir, panggil setVertex dan tetapkan offset yang dinormalisasi mulai dari 0F hingga 1F, beserta warna untuk setiap persimpangan petak.
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
}
}Lalu, untuk menganimasikan verteks tersebut, tambahkan offset animasi untuk delapan ver-- verteks.
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
// ....
setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
// ....
setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
// ....
}
}
Box(
modifier = modifier.padding(32.dp)
.safeContentPadding()
.aspectRatio(16 / 9f)
.fillMaxWidth()
.paint(gradientPainter)
)Setelah painter ditentukan, cukup panggil dengan composable apa pun melalui pengubah paint.
Sekarang, di aplikasi JetChat, kami menambahkan fitur untuk menyukai pesan saat Anda mengetuknya dua kali.
Untuk membuatnya, kita menyiapkan mesh 2x3 dan menempatkan perhentian warna di seluruh komponen.
MeshGradientPainter(
rows = 2,
columns = 3,
hasBicubicColor = true,
) {
// Row 0 (top edge, y = 0.0f)
setVertex(0, 0, Offset(0.0000f, 0.0000f), Color(0xFFFF6B6B))
setVertex(0, 1, Offset(0.3300f, 0.0000f), Color(0xFFFFD6D6))
setVertex(0, 2, Offset(0.6700f, 0.0000f), Color(0xFFE8C3FF))
setVertex(0, 3, Offset(1.0000f, 0.0000f), Color(0xFFE396FF))
// ... remaining vertices for Rows 0, 1, and 2
}Perhatikan bahwa kita menetapkan hasBicubicColor ke true di sini.
Secara default, warna diinterpolasi secara linear di seluruh segitiga, yang terkadang dapat meninggalkan bercak yang terlihat.
Mengaktifkan opsi ini akan mengalihkan shader ke interpolasi bikubik yang halus sehingga warna dapat menyatu dengan mulus.
Sekarang, bagaimana jika Anda ingin membuat cahaya ambient animasi seperti ini di belakang kotak input?
Untuk mendapatkan efek seperti ini dengan gradien mesh, Anda memerlukan dua hal.
Pertama, gerakan organik yang halus untuk verteks interior dan cara untuk melembutkan tepi luar sehingga Anda tidak mendapatkan persegi panjang yang kaku di sekitar kartu.
Jadi, kita mulai dengan menyiapkan rememberInfiniteTransition dengan dua sudut fase yang berjalan sedikit pada durasi yang berbeda sehingga gerakannya terasa organik, bukan mekanis.
val transition = rememberInfiniteTransition(label = "userInputGlowMesh")
val phase1 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2800, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase1",
)
val phase2 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 4200, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase2",
)Di sini, kita menganimasikan verteks interior dan menyematkan cincin luar, cincin luar verteks dengan tepi dengan alfa 0.
Karena batas luar bersifat statis dan transparan, pendaran akan memudar secara alami.
Dan, terakhir, kita memasukkan offset animasi tersebut ke MeshGradientPainter.
MeshGradientPainter(
rows = 6,
columns = 5,
hasBicubicColor = true,
) {
for (r in 0..6) {
for (c in 0..5) {
val offset = if (r in 1..5 && c in 1..4) {
val ampX = when (r) {
1, 2 -> 0.048f
3 -> 0.042f
4 -> 0.028f
else -> 0.036f
}
val ampY = when (r) {
1, 2 -> 0.028f
3 -> 0.020f
4 -> 0.022f
else -> 0.016f
}
val d = orbit(c, r, ampX, ampY)
Offset(
x = (baseU + d.x).coerceIn(0.04f, 0.96f),
y = (baseV + d.y).coerceIn(0.05f, 0.95f),
)
} else {
Offset(baseU, baseV)
}
setVertex(r, c, offset, gridColors[r][c])
}
}
}Gradien mesh yang baru saja Anda lihat digambar menggunakan drawMesh API di Android 14 dan yang lebih baru.
Jadi, mari kita pelajari API ini lebih lanjut dengan membuat efek dari awal.
Untuk layar sambutan JetChat, kita ingin menggambar jalur yang terlihat seperti pita.
Perhatikan dua hal di sini: lebar goresan tidak konstan dan warna mengikuti setiap goresan jalur.
Mari kita uraikan dan pahami apa yang sebenarnya kita perlukan untuk membuat efek ini.
Untuk setiap piksel, kita perlu memutuskan apakah akan menggambarnya atau tidak dan warna apa yang akan digunakan.
Untuk mengetahuinya, kita memerlukan dua jarak.
Nilai pertama adalah seberapa jauh piksel dari pusat goresan.
Jarak kedua adalah seberapa jauh piksel berada di sepanjang jalur, dari awal hingga akhir.
Hal ini memberi tahu kita warna mana yang harus dipilih dari gradien dan mencampurkan sedikit nilai pertama akan membuat warna melengkung di sepanjang lebar.
Jadi, bisakah kita menggambar gradien di sepanjang jalur hanya menggunakan jalur dan cat?
Jadi, ya, kita bisa.
Hal ini memerlukan perataan jalur menjadi daftar garis lurus pendek, yang juga memberikan jarak di sepanjang jalur.
val lines = remember {
path.asAndroidPath().flatten(0.5f)
}
//... inside draw scope
lines.forEach { line ->
//...
drawLine(
brush = Brush.linearGradient(...),
start = Offset(line.start.x,
line.start.y),
end = Offset(line.end.x, line.end.y),
strokeWidth = 30f,
)
//...
}Kemudian, kita menggambar setiap garis dengan gradiennya sendiri.
Cara ini berhasil, tetapi setiap baris memerlukan panggilan gambar dan objek gambar terpisah, dan seluruh jalur masih memiliki satu lebar.
Opsi lainnya adalah shader runtime, tetapi hanya mendapatkan posisi piksel dalam batas penampung.
Data ini tidak memberi tahu kita apa yang diperlukan untuk menggambar pita ini.
Dan, kita juga tidak dapat menghitungnya dengan mudah dari posisi piksel.
Draw mesh memberi kita vertex shader untuk itu.
Jadi, apa itu mesh?
Mesh terdiri dari segitiga dengan data di setiap sudut dan setiap sudut disebut verteks.
Setiap vertex membawa kumpulan data kecil.
Biasanya, data tersebut memiliki posisi, tetapi tidak harus.
Shader verteks membaca data tersebut dan menghitung posisi darinya.
Berikut jalur yang diambil oleh satu segitiga: Shader verteks berjalan untuk setiap sudut, menghitung posisi sudut, dan nilai apa pun yang ingin kita teruskan untuk setiap piksel.
Kemudian, rasterizer menemukan setiap piksel yang dicakup oleh segitiga tersebut dan menginterpolasi nilai dari ketiga sudut tersebut untuk setiap piksel.
Terakhir, shader fragmen berjalan untuk piksel tersebut, menggunakan nilai yang diinterpolasi tersebut, dan menghitung warna piksel yang dihitung.
Mari kita lihat cara menulis shader verteks dan fragmen dengan contoh yang lebih sederhana, yaitu garis lurus dengan pulsa yang bergerak di sepanjang garis tersebut.
LineShaders.VERTEX
uniform float2 uStart;
uniform float2 uEnd;
uniform float uHalfWidth;
uniform float uTime;
Varyings main(const Attributes a) {
Varyings v;
float2 dir = uEnd - uStart;
float2 normal = normalize(float2(-dir.y, dir.x));
float along = a.corner.x * length(dir);
float pulse = (a.corner.x - uTime) / 0.18;
float radius = uHalfWidth * (1.0 + 0.6 * exp(-pulse * pulse));
float across = a.corner.y * (radius + 1.0);
v.position = uStart + dir * a.corner.x + normal * across;
v.stroke = float3(along, across, radius);
return v;
}Shader vertex mengambil atribut sebagai input dan menampilkan bervariasi.
Atribut adalah data untuk satu sudut.
Berikut adalah dua angka: progresnya di sepanjang garis, dan di sisi mana garis tersebut berada.
Varying kemudian diinterpolasi dan diteruskan ke shader fragmen.
Kami menghitung jarak piksel dari awal, dan untuk lebar goresan, kami menggunakan waktu untuk membuat denyut pada radius.
Kemudian, kita mengalikan radius tersebut dengan sisi sudut, yang memberi kita jarak tanda di sepanjang goresan.
Saat GPU melakukan interpolasi di antara tanda-tanda tersebut, garis tengah menjadi nol.
Terakhir, kita menghitung posisi sudut, dan menyimpan semua nilai ini ke dalam stroke varyings dan meneruskannya ke shader fragmen.
Tidak seperti shader runtime, shader fragmen mesh menghasilkan warna melalui parameter, dan menampilkan posisi untuk mengambil sampel cat jika ditetapkan.
LineShaders.FRAGMENT
uniform float uLength;
layout(color) uniform float4 uStartColor;
layout(color) uniform float4 uEndColor;
float2 main(const Varyings v, out float4 color) {
float distance = abs(v.stroke.y);
float coverage = 1.0 - smoothstep(
v.stroke.z - 0.5, v.stroke.z + 0.5, distance);
float side = distance / v.stroke.z;
float4 ink = mix(uStartColor, uEndColor,
clamp(v.stroke.x / uLength + 0.4 * side * side, 0.0, 1.0));
color = ink * coverage;
return v.position;
}Mengambil nilai absolut koordinat melintang akan memberikan jarak piksel demi piksel dari garis tengah.
Kami membandingkan jarak tersebut dengan radius untuk mendapatkan cakupan, dan untuk warnanya, kami memadukan di sepanjang jalur garis, dan sedikit membelokkan gradien ke arah tepi.
Kalikan cakupan tersebut dengan cakupan, itulah warna akhir kita.
Di sisi Compose, kita mengikat shader dan tata letak data bersama dengan objek MeshSpecification.
val spec = remember {
MeshSpecification.make(
arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
vertexStride = 8,
arrayOf(Varying(TYPE_FLOAT3, "stroke")),
LineShaders.VERTEX, LineShaders.FRAGMENT)
}Pertama, kita mendeklarasikan atribut sudut sebagai dua float yang dimulai dari byte 0.
Karena dua float menggunakan 8 byte, setiap langkah sudut dalam buffer adalah 8.
Selanjutnya, kita mendeklarasikan variasi goresan kuas sebagai tiga float, dan kita meneruskan shader vertex dan fragmen yang baru saja kita tulis.
Kita membuat buffer verteks.
fun lineCorners(segments: Int): ByteBuffer {
val buffer = ByteBuffer.allocateDirect((segments + 1) * 2 * 8)
.order(ByteOrder.nativeOrder())
for (i in 0..segments) {
val t = i / segments.toFloat()
buffer.putFloat(t).putFloat(-1f) // one side
buffer.putFloat(t).putFloat(1f) // the other side
}
return buffer.rewind() as ByteBuffer
}Pada setiap langkah di sepanjang garis, kita menyelesaikan progres, menghitung progres dari 0 hingga 1, dan menulis dua sudut ke dalam buffer.
Terakhir, kita menggabungkan spesifikasi dan buffer menjadi objek mesh.
val mesh = Mesh(
spec,
mode = Mesh.TRIANGLE_STRIP,
vertexBuffer = corners,
vertexCount = (segments + 1) * 2,
bounds = RectF(0f, 0f, size.width, size.height)
)Kanvas Compose belum mengekspos drawMesh, jadi kita turun ke nativeCanvas dan menggambar mesh.
Box(Modifier.fillMaxSize().drawWithCache {
val mesh = Mesh(...)
onDrawBehind {
mesh.setFloatUniform("uTime", time.value)
drawIntoCanvas {
it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
}
}
})Karena Anda mengontrol geometri dan piksel, Anda dapat menggambar efek visual berbasis geometri apa pun menggunakan mesh gambar.
Pelajari lebih lanjut API ini dari dokumentasi do-.
Di Compose 1.12, kami memperkenalkan dukungan untuk gamut warna yang luas.
Lalu, apa artinya?
Tahukah Anda bahwa layar dapat mereproduksi rentang warna yang berbeda dari ruang warna sRGB tradisional?
Misalnya, jika Anda melihat gambar ini, Anda akan melihat segitiga putih adalah rentang warna sRGB standar yang dapat ditampilkan oleh sebagian besar layar.
Namun, layar yang lebih baru, beberapa di antaranya mendukung beragam warna yang lebih luas, seperti yang digambarkan oleh segitiga kuning, beberapa lebih jenuh dan cerah.
Namun, saat merepresentasikan warna, kita sebenarnya merepresentasikannya dengan deskripsi setiap verteks segitiga tersebut.
Saat Anda melihat perbandingan gradien ini, penting untuk membedakan antara gamut tampilan dan interpolasi gradien.
Pada layar sRGB standar atau mungkin proyektor ini, layar tidak dapat merender warna secara fisik di luar gamut sRGB ini.
Jadi, sebenarnya sangat sulit untuk mendemonstrasikannya kepada Anda di atas panggung saat ini.
Jadi, Anda harus percaya bahwa warna terlihat lebih baik di Android dengan dukungan gamut warna yang luas.
Dalam contoh ini, yang sebenarnya Anda lihat di sini adalah efek interpolasi perhentian warna dalam ruang warna yang lebih luas.
Memadukan warna dalam ruang warna yang luas atau linear akan mengubah jalur matematika sebenarnya dari gradien ini, sehingga mencegah warna abu-abu di tengah dan banding yang kasar yang dapat terjadi jika Anda melakukannya tanpa gamut warna yang luas.
Dengan Compose 1.12, Anda mendapatkan manfaat perpaduan gradien yang lebih halus di layar mana pun, serta dukungan gamut warna yang benar-benar luas saat Anda menjalankannya di hardware yang didukung.
Untuk mengaktifkan dukungan yang benar di perangkat, Anda harus memastikan aktivitas atau jendela Anda disetel untuk mengaktifkan mode gamut warna yang luas.
Dan ini dapat dilakukan di AndroidManifest Anda.
<!-- AndroidManifest.xml -->
<activity
android:name=".NavActivity"
android:colorMode="wideColorGamut"
android:exported="true">
</activity>Atau secara terprogram dalam composable Anda.
// Dynamic toggle in code
DisposableEffect(window) {
window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
onDispose {
window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
}
}Tanpa menyetel colorMode, jendela akan tetap dalam sRGB 8-bit standar, dan warna wide color gamut yang dirender oleh GPU akan terpotong.
Jadi, penting untuk menghindari penggunaan warna gamut warna lebar jika layar sebenarnya tidak dapat mendukungnya.
Anda hanya boleh mengaktifkan gamut warna yang luas pada aktivitas atau alur kode yang benar-benar menampilkan media, foto, atau grafik yang kaya untuk menghemat baterai dan performa.
Artinya, Anda dapat membuat warna dalam ruang warna yang berbeda di Compose, dan warna ini sekarang dihormati.
// 1. Display P3 Color (Wider gamut: greens & reds are more saturated) colorMode=wideColorGamut
val VividP3Red = Color(
red = 1.0f,
green = 0.05f,
blue = 0.05f,
alpha = 1.0f,
colorSpace = ColorSpaces.DisplayP3
)
// 2. Extended sRGB (Enables HDR values > 1.0f) use colorMode=hdr
val HdrNeonCyan = Color(
red = 0.0f,
green = 1.8f, // Peak brightness > 1.0 in extended gamut
blue = 2.0f,
alpha = 1.0f,
colorSpace = ColorSpaces.ExtendedSrgb
)
// 3. BT.2020 HLG (Broadcast HDR standard, supported API 34+) - use colorMode=hdr
val HdrSunsetOrange = Color(
red = 1.0f,
green = 0.45f,
blue = 0.1f,
alpha = 1.0f,
colorSpace = ColorSpaces.Bt2020Hlg
)Anda dapat meneruskan jenis colorSpace yang Anda inginkan untuk membuat warna, dan jika Anda tidak meneruskan colorSpace, sRGB standar akan digunakan.
Khusus untuk Bitmap, Anda dapat memeriksa sumber Bitmap menggunakan getColorSpace, dan library pemuatan gambar populer akan otomatis menangani ruang warna Bitmap yang benar untuk Anda.
Anda harus menggunakan Bitmap.Config.HARDWARE untuk mempertahankan informasi warna, karena konfigurasi seperti 565 akan kehilangan presisi dan memiliki banding yang terlihat oleh pengguna.
Untuk mempelajari warna lebih lanjut, Anda dapat kembali ke tahun 2017 untuk menonton Romain Guy membahas semua hal tentang warna di Compose, atau tentang warna di Android.
Dengan Compose 1.13, kami memperkenalkan dukungan native untuk bentuk poligon.
Hal ini akan mempermudah pembuatan bentuk kustom dan elemen UI yang terlihat organik.
Sebelumnya, jika ingin menggambar geometri kompleks seperti ini, Anda harus menggunakan RoundedPolygon untuk mendeskripsikan geometri yang akan menghasilkan jalur, lalu memetakan jalur tersebut ke API bentuk Compose.
fun cookiePolygon(): RoundedPolygon {
val vertices = FloatArray(32)
for (i in 0 until 16) {
val radius = if (i % 2 == 0) 1f else 0.84f
// …
}
return RoundedPolygon(
vertices,
rounding = CornerRounding(0.12f)
)
}Misalnya, pertama, Anda menentukan proporsi menggunakan RoundedPolygon, dan kita menetapkan Dalam cuplikan ini, kita menetapkan titik dalam berada di 84% dari pusat dibandingkan dengan titik luar.
Anda juga dapat menerapkan pembulatan sudut di sini, tetapi ini hanya menjelaskan kurva matematika.
Untuk benar-benar menggunakan poligon ini untuk kliping atau latar belakang di Compose, Anda harus menulis wrapper Bentuk kustom seperti ini.
class RoundedPolygonShape(
polygon: RoundedPolygonShape
) : Shape {
// …
override fun createOutline(size: Size, layoutDirection: LayoutDirection,
density: Density,
): Outline {
path = polygon.toPath().asComposePath()
// …
matrix.scale(size.width / maxDimension, size.height / maxDimension)
matrix.translate(-bounds.left, -bounds.top)
// …
return Outline.Generic(path)
}
}Dan di dalam cuplikan createOutline ini, Anda dapat melihat bahwa Anda harus membuat jalur, lalu menskalakan dan menerjemahkannya secara manual.
Sekarang, dengan PolygonShape API baru, Anda tidak lagi memerlukan wrapper Shape kustom tersebut.
private val AvatarShape = PolygonShape {
val vertices = List(16) { i ->
val radius = size.minDimension / 2f * if (i % 2 == 0) 1f else 0.86f
val angle = i * PI.toFloat() / 8f
size.center + Offset(cos(angle), sin(angle)) * radius
}
polygon(vertices, rounding = CornerRounding(radius = 8.dp))
}
// …
Modifier.clip(AvatarShape)Karena ini adalah builder Bentuk Compose khusus, builder ini menerima kepadatan dan ukuran secara langsung.
Artinya, Anda dapat menghitung verteks secara dinamis dan menentukan pembulatan sudut secara langsung dalam dp atau bahkan sebagai persentase.
Setelah ditentukan, Anda cukup meneruskannya ke API mana pun yang menggunakan Shape.
Kami juga memperkenalkan MorphPolygonShape API, yang memungkinkan Anda bertransisi dengan lancar di antara berbagai bentuk poligon.
val recordShape = remember {
MorphPolygonShape(
start = PolygonShape.circle(),
end = PolygonShape.star(
numPoints = 9,
innerRadiusRatio = 0.8f,
outerRounding = CornerRounding(percent = 50),
),
) { shapeProgress.value }
}Dalam contoh ini, kita memiliki tombol rekam yang berubah dari lingkaran menjadi bintang saat Anda berinteraksi dengannya.
Anda hanya perlu memberikan bentuk awal, bentuk akhir, dan nilai progres.
MorphPolygonShape API membaca nilai progres selama fase resolusi garis batas, yang berarti, bukan selama rekom- bukan selama komposisi.
Artinya, saat bentuk Anda berubah dari frame ke frame, bentuk tersebut akan berubah menjadi garis batas baru tanpa memicu rekomposisi.
Selanjutnya, mari kita bahas pemburaman.
Selama beberapa rilis terakhir, kami mencoba membuat penerapan blur jauh lebih mudah dan ekspresif.
Setiap kali kita membahas pengaburan, ini adalah pengubah yang umum digunakan yang kita lihat.
Modifier
.blur(
radiusX = 10.dp,
radiusY = 10.dp,
edgeTreatment = BlurredEdgeTreatment(
shape = RoundedCornerShape(8.dp),
),
)Compose telah memiliki pengubah ini sejak Android 12.
Fungsi ini sangat cocok jika Anda menginginkan blur yang seragam, dengan radius blur yang sama persis diterapkan di seluruh composable.
Namun, desain UI modern sering kali memerlukan transisi yang mulus dari area tajam ke buram, bukan batas yang tegas.
Dan hal ini membawa kita ke Progressive Blur, yang kini menjadi API resmi mulai Compose 1.13.
Alih-alih satu radius konstan di seluruh composable, kini Anda dapat memvariasikan intensitas blur di seluruh ruang.
Berikut contoh dari aplikasi JetChat kami dari layar profil pengguna.
Pengubah blur baru menggunakan spesifikasi radius blur.
Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}Di sini, kami menggunakan gradien vertikal dengan dua penghentian blur, sehingga separuh atas foto tetap tajam sepenuhnya pada 0 dp, dan meningkat hingga blur 32 dp di tepi bawah.
Perhatikan, kita juga menyetel edgeTreatment sebagai tidak berbatas, yang pada dasarnya memberi tahu blur untuk mengambil sampel piksel transparan di luar batas composable juga.
Selain fade-out vertikal, spesifikasi radius blur ini memberi Anda beberapa cara bawaan untuk mendorong blur progresif.
Misalnya, di sebelah kiri Anda dapat melihat gradien horizontal, dengan radius awal 0 dp, dan radius akhir ditentukan oleh nilai penggeser.
BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)Di sebelah kanan, kita memiliki gradien radial, dengan radius blur 0 dp di bagian tengah, dan saat Anda bergerak ke arah fallOffRadius, yang dalam hal ini adalah 300 dp, blur akan meningkat secara bertahap hingga 24 dp.
BlurRadiusSpec.radialGradient(
center = center,
fallOffRadius = 300.dp,
startRadius = 0.dp,
endRadius = 24.dp,
)Jika semua gradien vertikal, radial, dan horizontal ini tidak cukup, Anda dapat selalu mengarahkan blur progresif menggunakan shader kustom.
val customShader = remember {
RuntimeShader(
"""
uniform float2 size;
uniform float time;
half4 main(float2 coords) {
// Diagonal wave mask
float normalizedX = coords.x / size.x;
float normalizedY = coords.y / size.y;
float wave = sin((normalizedX + normalizedY) * 6.0 + time) * 0.5 + 0.5;
return half4(0.0, 0.0, 0.0, wave);
}
""".trimIndent()
)
}
val customBlurRadius = remember(customShader) {
BlurRadiusSpec.shader(maxRadius = 24.dp) { customShader }
}
Modifier.blur {
val w = size.width.toPx()
val h = size.height.toPx()
customShader.setFloatUniform("size", w, h)
customShader.setFloatUniform("time", animatedTime)
blurRadiusSpec = customBlurRadius
},Di sini, kita menulis shader fragmen kecil yang menghitung gelombang sinus diagonal animasi, dan menghasilkan intensitas tersebut ke dalam saluran alfa.
Kemudian, spesifikasi shader memetakan mask 0 hingga 1 tersebut langsung ke mask 24 dp maxRadius.
Sebelum membahas blur latar belakang, mari kita lihat perbedaan antara keduanya.
Sekarang di sebelah kiri, kita memiliki panel aplikasi atas tempat kita menerapkan blur lapisan.
Artinya, pengubah ini memburamkan composable itu sendiri.
Seperti yang Anda lihat, ikon judul dan tindakan terlihat buram, tetapi komponen chat di belakangnya tidak buram sama sekali.
Di sebelah kanan, kita memiliki blur latar belakang.
Di sini, hanya pesan chat yang di-scroll di belakang panel atas yang dijadikan sampel dan diburamkan.
Namun, teks dan ikonnya sangat tajam dan mudah dibaca.
Mulai Android 37.2, Anda dapat menerapkan blur latar belakang menggunakan API baru yang disebut RenderNode.setBackdropRenderEffect.
Namun, sebelum membahas cara peningkatan blur, mari kita pahami cara kerja Android di balik layar untuk memastikan blur berperforma baik.
Android menggunakan RenderNode di balik layar, yang merupakan penampung daftar tampilan yang diakselerasi hardware, yang berarti dapat memadatkan dan menyimpan banyak perintah gambar yang berbeda melalui mulai merekam dan akhiri perekaman.
RenderNode menyimpan informasi seperti terjemahan 3D, rotasi, alfa, skala, dan batas posisi Anda.
Dan Compose sebenarnya menggunakan RenderNode di balik layar.
Jadi, untuk menggunakan efek render backdrop di Compose, kita dapat membuat Modifier.Node kustom agar dapat menggunakan kembali pengubah ini.
fun Modifier.backdropBlur(spec: BlurRadiusSpec, shape: Shape, tint: Color): Modifier =
this then BackdropBlurElement(spec, shape, tint)
private class BackdropBlurNode(...) : Modifier.Node(), DrawModifierNode {
private var renderNode: RenderNode? = null
override fun ContentDrawScope.draw() {
val node = getOrCreateNode()
if (isDirty || sizeChanged) {
node.setPosition(0, 0, widthPx, heightPx)
node.setBackdropRenderEffect(spec.createRenderEffect(this))
}
drawIntoCanvas { it.nativeCanvas.drawRenderNode(node) }
drawContent()
}
}Kita menyimpan referensi ke RenderNode, dan RenderNode dikonfigurasi dengan efek yang kita inginkan dalam fungsi lain.
Misalnya, kita meneruskan efek blur di sini.
Selanjutnya, kita memanggil drawIntoCanvas, yang memanggil drawRenderNode di kanvas native.
Dan terakhir, kita memanggil drawContent untuk menggambar konten sebenarnya yang tidak diburamkan di atasnya juga.
Untuk menggunakan pengubah yang baru saja kita buat ini, kita kemudian memanggil Modifier.backdropBlur, dan meneruskan spesifikasi radius blur.
JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.uniform(5.dp)
)
)Perhatikan bahwa kita tidak perlu menempatkan apa pun pada lapisan yang mendasarinya di sini dengan pendekatan ini.
RenderNode.setBackdropRenderEffect otomatis mengambil sampel konten yang berada di bawah permukaan panel aplikasi JetChat.
Hasilnya adalah pemburaman latar belakang yang indah ini.
Kita juga dapat menetapkan spesifikasi radius blur progresif, bukan hanya progresif, dan untuk mendapatkan blur vertikal pada konten juga.
JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 32.dp),
BlurStop(1f, 0.dp),
),
)
)
)Yang dengan gradien vertikal menampilkan konten di bawahnya yang diburamkan dengan radius yang berbeda pada pecahan penampung yang berbeda.
Kami sedang mengerjakan pengubah blur latar belakang bawaan di Compose.
Dan ini hanyalah contoh teknologi dasar di Android dengan Compose.
Contoh yang saya tunjukkan ini juga hanya akan berfungsi di API level 37.2, tetapi kami juga sedang mengerjakan backport untuk Compose, jadi nantikan terus.
Contoh menarik lainnya dengan efek backdrop RenderNode adalah kita juga dapat menggabungkannya untuk membentuk UI yang sangat menarik, dan yang menjalankan beberapa efek berbeda di atasnya.
Jadi, untuk merangkai efek, kita akan membuat dua efek ini.
val blur = RenderEffect.createBlurEffect(24f, 24f, Shader.TileMode.CLAMP)
// Boost saturation by 30% and apply blur together
val matrix = ColorMatrix().apply { setSaturation(1.3f) }
val vibrancy = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix))
val vibrantBlur = RenderEffect.createChainEffect(vibrancy, blur)Dalam contoh ini, kita membuat efek blur dan saturasi 30%.
Kita memanggil createChainEffect dengan menerapkan efek blur terlebih dahulu, lalu saturasi.
Berikut adalah contoh di JetChat.
val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)Tombol lompat ke bawah, yang dapat kita buat terlihat seperti kaca pembesar kecil dengan menggabungkan beberapa efek berbeda untuk membuat tepi composable ini bulat seperti kaca pembesar.
Namun, Anda tidak hanya dapat menerapkan efek pembesaran, semua jenis shader dapat diterapkan secara berantai untuk diterapkan ke latar belakang.
Berikut beberapa contoh menarik yang telah kami terapkan tentang hal yang dapat dilakukan saat menggabungkannya.
Efek jenis kaca buram tradisional kini dapat digunakan.
Namun, kita juga bisa membuat efek seru lainnya, seperti kaleidoskop.
Atau, jika kita ingin mengubah maskot Android ini menjadi semacam elemen UI yang berubah bentuk dan mengubah konten di bawahnya, kita juga dapat menerapkan efek render latar belakang ini agar terlihat seolah-olah kita sedang melihat melalui droid kecil ini.
Kita juga dapat melakukannya dengan Render Effects latar belakang.
Jadi, tidak hanya efek blur latar belakang dan kaca buram yang dapat diakses dengan API baru ini, tetapi kini batasannya hanyalah imajinasi Anda.
API baru lainnya yang telah kami perkenalkan ke platform adalah SurfaceView.setBlurRegions.
Saat ini, Anda dapat memburamkan tampilan tekstur dan dengan composable apa pun menggunakan Efek Latar Belakang Modifier.blur dan RenderNode, tetapi memburamkan bagian SurfaceView itu rumit, dan bukan bagian dari hierarki RenderNode tampilan aplikasi, karena sebenarnya membuat lubang di jendela.
Jadi, Compose tidak dapat mengambil sampel buffering pendukung SurfaceView, sehingga hal ini sebelumnya tidak mungkin dilakukan hingga API 37.2.
Jadi, setBlurRegions SurfaceView memungkinkan aplikasi mendeklarasikan region blur secara langsung.
Misalnya, pada pemutar video di JetChat ini, kami menambahkan blur SurfaceView saat kontrol ditampilkan dan disembunyikan.
Untuk menggunakan API baru ini, Anda dapat mengakses setBlurRegions di metode SurfaceView.
val region = RoundedRectBlurRegion().apply {
bounds = RectF(left, top, right, bottom)
setCornerRadii(spec.cornerRadiusPx.coerceAtLeast(0f))
alpha = spec.alpha.coerceIn(0f, 1f)
blurRadius = spec.blurRadiusPx.coerceAtLeast(0f)
}
surfaceView.setBlurRegions(listOf(region))
surfaceView.setBlurRegions(emptyList()) // ClearFungsi ini mengambil daftar area batas, radius blur, alfa, dan daftar radius sudut di dalamnya.
Saat meneruskan batas ini, batas tersebut harus relatif terhadap SurfaceView di layar.
Jadi, Anda perlu melacak koordinat SurfaceView dan area yang ingin diburamkan.
Bagian baru lainnya dari Android 16 dan yang lebih baru adalah RuntimeColorFilter.
Sebelumnya, dengan gambar, Anda hanya dapat menyetel filter warna matriks statis dan melakukan efek seperti saturasi.
Namun, dengan Android 16 dan yang lebih baru, Anda kini dapat meneruskan shader yang menggunakan warna, piksel input untuk konten tertentu, dan menghasilkan warna baru.
Misalnya, jika ingin menerapkan filter cyberpunk ke foto, kita dapat menentukan shader kecil yang digunakan sebagai input ke RuntimeColorFilter.
@Language("AGSL")
private const val CYBERPUNK_SHADER = """
vec4 main(half4 in_color) {
float lum = dot(in_color.rgb, vec3(0.299, 0.587, 0.114));
vec3 shadow = vec3(0.06, 0.04, 0.25);
vec3 mid = vec3(0.92, 0.12, 0.58);
vec3 high = vec3(0.18, 0.94, 0.98);
vec3 col = lum < 0.5 ? mix(shadow, mid, lum * 2.0) : mix(mid, high, (lum - 0.5) * 2.0);
return vec4(mix(in_color.rgb, col, 0.7), in_color.a);
}
"""Shader ini menghasilkan tema cyberpunk, tetapi Anda dapat membuat efek lain yang Anda inginkan.
Jadi, untuk menggunakan shader itu sendiri, kita menerapkan RuntimeColorFilter dan mengonversinya menjadi filter warna Compose.
AsyncImage(
model = imageUri,
contentDescription = filter.displayName,
colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
.asComposeColorFilter(),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)Seperti yang Anda lihat dalam contoh ini, kita sekarang dapat membuat efek yang lebih dinamis daripada sebelumnya tanpa perlu mempelajari lebih dalam OpenGL, penyiapan SurfaceView, atau hal semacam itu.
Dan ini juga dapat diterapkan ke composable apa pun, jadi tidak hanya gambar.
Terima kasih.
Jadi, kita telah membuat beberapa efek yang sangat bagus, tetapi mari kita pastikan aplikasi kita tetap terasa cepat dan lancar.
Saat kita men-scroll percakapan, semua efek akan berjalan bersamaan.
Mari kita mulai dengan mengukur scroll ini.
@Test
fun scrollFling() = measureScroll(swipeSteps = 5)
private fun measureScroll(swipeSteps: Int) = rule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = defaultComposeScrollingMetrics(),
compilationMode = CompilationMode.Full(),
iterations = 5,
setupBlock = {
killProcess()
}
//...Kami menggunakan benchmark makro yang meluncurkan aplikasi dalam proses baru dan menjalankan scroll di seluruh percakapan.
Kemudian, kita membuat kueri rekaman aktivitas untuk menghitung frame yang melewati batas waktu.
select count(*) as late_frames
from actual_frame_timeline_slice
where upid = (select upid from process where name = 'com.example.compose.jetchat.perf')
and jank_type like '%App Deadline Missed%'Artinya, ada enam frame terlambat.
Mari kita buka salah satu frame yang terlambat.
Jadi, tepat sebelum frame ini, Compose mengambil pesan berikutnya sebelum di-scroll ke atas ke dalam tampilan.
Pengambilan data di awal seharusnya sesuai dengan celah pendek di antara frame, tetapi menyiapkan pesan ini juga mendekode gambarnya langsung ke thread utama, yang berjalan lama tanpa menghasilkan apa pun.
Karena thread utama macet saat mendekode gambar ini, tidak ada frame baru yang dapat dimulai.
Kemudian, saat pesan tersebut benar-benar di-scroll ke tampilan, RenderThread harus mengalokasikan memori dan mengupload semua piksel tersebut ke GPU, sehingga gambar yang sama persis menunda frame kedua.
Sekarang, kita melihat ke mana waktu kita dihabiskan, tetapi apakah kita benar-benar memerlukan semua pekerjaan ini untuk menggambar frame ini?
Dan bagaimana kita bisa tahu?
Trace akan memberi tahu kita.
Setiap kali bitmap besar digambar ke area yang jauh lebih kecil di layar, Android akan mencatat penanda langsung ke dalam rekaman aktivitas.
Alat ini menampilkan dua angka dalam piksel: dimensi bitmap penuh, dan area yang sebenarnya dicakup di layar.
Bagi nilai tersebut dengan kepadatan layar dan Anda akan mendapatkan nilai tersebut dalam DP.
Kemudian, kita menelusuri kode dengan nilai DP tersebut untuk menemukan gambar tersebut.
Inilah yang kami temukan.
Gambar dimuat dengan painterResource, yang tidak mengetahui seberapa besar penampungnya, sehingga gambar didekode secara keseluruhan dalam resolusi penuh.
Image(
painter = painterResource(it),
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)Jadi, kita menggantinya dengan AsyncImage dari coil.
AsyncImage(
model = it,
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)Library ini mengukur penampung terlebih dahulu, sehingga hanya mendekode piksel yang diperlukan, dan melakukan pekerjaan tersebut di thread yang berbeda.
Menjalankan benchmark yang sama lagi, decoding gambar di thread utama turun menjadi 0, dan frame terlambat turun menjadi 2.
Kita tidak akan men-debug kedua frame tersebut di sini, tetapi kesimpulannya sederhana: cobalah untuk tidak menghitung piksel yang tidak akan pernah mencapai frame.
Jadi, decoding gambar cukup besar untuk membuat frame terlambat, tetapi tidak setiap biaya muncul sebagai frame yang dilewati, dan masih ada biaya yang lebih kecil yang dapat kita hindari saat menulis kode.
Mari kita lihat dua di antaranya.
Setiap kali menggunakan RuntimeShader, kita harus membuatnya sekali karena meskipun hanya mengompilasi dan di-cache, class ini tetap mengurai string sumbernya setiap kali kita membuatnya.
Dan setiap kali efek menganimasikan setiap frame, kita dapat menggabungkannya dalam graphicsLayer, sehingga hanya elemen animasi yang direkam ulang, dan bukan menggambar ulang UI di sekitarnya.
Untuk tombol Lompat ke bawah, tombol ini muncul setiap kali kita men-scroll.
Ini menggunakan dua RuntimeShader untuk menampilkan efek tersebut, dan setiap kali tombol muncul, kita membuat ulang kedua shader ini dari string sumbernya.
val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content")
// …
val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply {Mengurai kedua shader tersebut dan membuat efeknya memerlukan waktu ratusan mikrodetik di thread utama setiap kali tombol muncul.
Sebagai gantinya, kita membuat shader sekali untuk seluruh proses, atau kita dapat mengingatnya, lalu memperbarui uniform-nya.
val lensShader = MagnifierShaders.lens.apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
private object MagnifierShaders {
val lens: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(MAGNIFIER_LENS_SHADER) }
val glassFinish: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(GLASS_FINISH_SHADER) }Dengan perubahan tersebut, pembuatan efek tersebut hampir 40 kali lebih cepat, dan biaya saat tombol muncul turun 20 kali lipat.
Dalam cahaya gradien mesh yang kita lihat sebelumnya, cahaya menganimasikan setiap frame di dalam drawBehind.
Box(
modifier = Modifier
.matchParentSize()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}Jadi, membaca status dalam fase gambar akan mempertahankan komposisi dan tata letak, tetapi daftar tampilan tidak dapat memperbarui satu perintah gambar di tempat.
Jadi, Compose akan menelusuri hierarki untuk menemukan node terdekat dengan lapisan grafis, yang di sini, adalah garis batas merah, yang merupakan seluruh layar, dan merekam ulang seluruh lapisan tersebut di setiap frame.
Dengan menempatkan graphicsLayer tepat di kotak, perjalanan berhenti di garis batas hijau, sehingga hanya kotak glow yang direkam ulang, dan slice gambar thread utama turun dua persepuluh milidetik.
Box(
modifier = Modifier
.matchParentSize()
.graphicsLayer()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}Dengan Perfetto, menganalisis masalah dalam rekaman aktivitas bisa jadi sulit, sehingga Anda dapat menggunakan keterampilan analisis rekaman aktivitas untuk membantu Anda mengumpulkan beberapa insight.
Jika Anda memerlukan bantuan lebih lanjut dalam menulis kode Compose yang berperforma tinggi, pastikan untuk menginstal Android CLI dan menggunakan kemampuan performa Compose untuk memulai.
android skills add perfetto-trace-analysis
android skills add jetpack-compose-performanceval gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
}
}val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
// ....
setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
// ....
setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
// ....
}
}
Box(
modifier = modifier.padding(32.dp)
.safeContentPadding()
.aspectRatio(16 / 9f)
.fillMaxWidth()
.paint(gradientPainter)
)MeshGradientPainter(
rows = 2,
columns = 3,
hasBicubicColor = true,
) {
// Row 0 (top edge, y = 0.0f)
setVertex(0, 0, Offset(0.0000f, 0.0000f), Color(0xFFFF6B6B))
setVertex(0, 1, Offset(0.3300f, 0.0000f), Color(0xFFFFD6D6))
setVertex(0, 2, Offset(0.6700f, 0.0000f), Color(0xFFE8C3FF))
setVertex(0, 3, Offset(1.0000f, 0.0000f), Color(0xFFE396FF))
// ... remaining vertices for Rows 0, 1, and 2
}val transition = rememberInfiniteTransition(label = "userInputGlowMesh")
val phase1 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2800, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase1",
)
val phase2 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 4200, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase2",
)MeshGradientPainter(
rows = 6,
columns = 5,
hasBicubicColor = true,
) {
for (r in 0..6) {
for (c in 0..5) {
val offset = if (r in 1..5 && c in 1..4) {
val ampX = when (r) {
1, 2 -> 0.048f
3 -> 0.042f
4 -> 0.028f
else -> 0.036f
}
val ampY = when (r) {
1, 2 -> 0.028f
3 -> 0.020f
4 -> 0.022f
else -> 0.016f
}
val d = orbit(c, r, ampX, ampY)
Offset(
x = (baseU + d.x).coerceIn(0.04f, 0.96f),
y = (baseV + d.y).coerceIn(0.05f, 0.95f),
)
} else {
Offset(baseU, baseV)
}
setVertex(r, c, offset, gridColors[r][c])
}
}
}val lines = remember {
path.asAndroidPath().flatten(0.5f)
}
//... inside draw scope
lines.forEach { line ->
//...
drawLine(
brush = Brush.linearGradient(...),
start = Offset(line.start.x,
line.start.y),
end = Offset(line.end.x, line.end.y),
strokeWidth = 30f,
)
//...
}LineShaders.VERTEX
uniform float2 uStart;
uniform float2 uEnd;
uniform float uHalfWidth;
uniform float uTime;
Varyings main(const Attributes a) {
Varyings v;
float2 dir = uEnd - uStart;
float2 normal = normalize(float2(-dir.y, dir.x));
float along = a.corner.x * length(dir);
float pulse = (a.corner.x - uTime) / 0.18;
float radius = uHalfWidth * (1.0 + 0.6 * exp(-pulse * pulse));
float across = a.corner.y * (radius + 1.0);
v.position = uStart + dir * a.corner.x + normal * across;
v.stroke = float3(along, across, radius);
return v;
}LineShaders.FRAGMENT
uniform float uLength;
layout(color) uniform float4 uStartColor;
layout(color) uniform float4 uEndColor;
float2 main(const Varyings v, out float4 color) {
float distance = abs(v.stroke.y);
float coverage = 1.0 - smoothstep(
v.stroke.z - 0.5, v.stroke.z + 0.5, distance);
float side = distance / v.stroke.z;
float4 ink = mix(uStartColor, uEndColor,
clamp(v.stroke.x / uLength + 0.4 * side * side, 0.0, 1.0));
color = ink * coverage;
return v.position;
}val spec = remember {
MeshSpecification.make(
arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
vertexStride = 8,
arrayOf(Varying(TYPE_FLOAT3, "stroke")),
LineShaders.VERTEX, LineShaders.FRAGMENT)
}fun lineCorners(segments: Int): ByteBuffer {
val buffer = ByteBuffer.allocateDirect((segments + 1) * 2 * 8)
.order(ByteOrder.nativeOrder())
for (i in 0..segments) {
val t = i / segments.toFloat()
buffer.putFloat(t).putFloat(-1f) // one side
buffer.putFloat(t).putFloat(1f) // the other side
}
return buffer.rewind() as ByteBuffer
}val mesh = Mesh(
spec,
mode = Mesh.TRIANGLE_STRIP,
vertexBuffer = corners,
vertexCount = (segments + 1) * 2,
bounds = RectF(0f, 0f, size.width, size.height)
)Box(Modifier.fillMaxSize().drawWithCache {
val mesh = Mesh(...)
onDrawBehind {
mesh.setFloatUniform("uTime", time.value)
drawIntoCanvas {
it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
}
}
})<!-- AndroidManifest.xml -->
<activity
android:name=".NavActivity"
android:colorMode="wideColorGamut"
android:exported="true">
</activity>// Dynamic toggle in code
DisposableEffect(window) {
window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
onDispose {
window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
}
}// 1. Display P3 Color (Wider gamut: greens & reds are more saturated) colorMode=wideColorGamut
val VividP3Red = Color(
red = 1.0f,
green = 0.05f,
blue = 0.05f,
alpha = 1.0f,
colorSpace = ColorSpaces.DisplayP3
)
// 2. Extended sRGB (Enables HDR values > 1.0f) use colorMode=hdr
val HdrNeonCyan = Color(
red = 0.0f,
green = 1.8f, // Peak brightness > 1.0 in extended gamut
blue = 2.0f,
alpha = 1.0f,
colorSpace = ColorSpaces.ExtendedSrgb
)
// 3. BT.2020 HLG (Broadcast HDR standard, supported API 34+) - use colorMode=hdr
val HdrSunsetOrange = Color(
red = 1.0f,
green = 0.45f,
blue = 0.1f,
alpha = 1.0f,
colorSpace = ColorSpaces.Bt2020Hlg
)fun cookiePolygon(): RoundedPolygon {
val vertices = FloatArray(32)
for (i in 0 until 16) {
val radius = if (i % 2 == 0) 1f else 0.84f
// …
}
return RoundedPolygon(
vertices,
rounding = CornerRounding(0.12f)
)
}class RoundedPolygonShape(
polygon: RoundedPolygonShape
) : Shape {
// …
override fun createOutline(size: Size, layoutDirection: LayoutDirection,
density: Density,
): Outline {
path = polygon.toPath().asComposePath()
// …
matrix.scale(size.width / maxDimension, size.height / maxDimension)
matrix.translate(-bounds.left, -bounds.top)
// …
return Outline.Generic(path)
}
}private val AvatarShape = PolygonShape {
val vertices = List(16) { i ->
val radius = size.minDimension / 2f * if (i % 2 == 0) 1f else 0.86f
val angle = i * PI.toFloat() / 8f
size.center + Offset(cos(angle), sin(angle)) * radius
}
polygon(vertices, rounding = CornerRounding(radius = 8.dp))
}
// …
Modifier.clip(AvatarShape)val recordShape = remember {
MorphPolygonShape(
start = PolygonShape.circle(),
end = PolygonShape.star(
numPoints = 9,
innerRadiusRatio = 0.8f,
outerRounding = CornerRounding(percent = 50),
),
) { shapeProgress.value }
}Modifier
.blur(
radiusX = 10.dp,
radiusY = 10.dp,
edgeTreatment = BlurredEdgeTreatment(
shape = RoundedCornerShape(8.dp),
),
)Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)BlurRadiusSpec.radialGradient(
center = center,
fallOffRadius = 300.dp,
startRadius = 0.dp,
endRadius = 24.dp,
)val customShader = remember {
RuntimeShader(
"""
uniform float2 size;
uniform float time;
half4 main(float2 coords) {
// Diagonal wave mask
float normalizedX = coords.x / size.x;
float normalizedY = coords.y / size.y;
float wave = sin((normalizedX + normalizedY) * 6.0 + time) * 0.5 + 0.5;
return half4(0.0, 0.0, 0.0, wave);
}
""".trimIndent()
)
}
val customBlurRadius = remember(customShader) {
BlurRadiusSpec.shader(maxRadius = 24.dp) { customShader }
}
Modifier.blur {
val w = size.width.toPx()
val h = size.height.toPx()
customShader.setFloatUniform("size", w, h)
customShader.setFloatUniform("time", animatedTime)
blurRadiusSpec = customBlurRadius
},fun Modifier.backdropBlur(spec: BlurRadiusSpec, shape: Shape, tint: Color): Modifier =
this then BackdropBlurElement(spec, shape, tint)
private class BackdropBlurNode(...) : Modifier.Node(), DrawModifierNode {
private var renderNode: RenderNode? = null
override fun ContentDrawScope.draw() {
val node = getOrCreateNode()
if (isDirty || sizeChanged) {
node.setPosition(0, 0, widthPx, heightPx)
node.setBackdropRenderEffect(spec.createRenderEffect(this))
}
drawIntoCanvas { it.nativeCanvas.drawRenderNode(node) }
drawContent()
}
}JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.uniform(5.dp)
)
)JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 32.dp),
BlurStop(1f, 0.dp),
),
)
)
)val blur = RenderEffect.createBlurEffect(24f, 24f, Shader.TileMode.CLAMP)
// Boost saturation by 30% and apply blur together
val matrix = ColorMatrix().apply { setSaturation(1.3f) }
val vibrancy = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix))
val vibrantBlur = RenderEffect.createChainEffect(vibrancy, blur)val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)val region = RoundedRectBlurRegion().apply {
bounds = RectF(left, top, right, bottom)
setCornerRadii(spec.cornerRadiusPx.coerceAtLeast(0f))
alpha = spec.alpha.coerceIn(0f, 1f)
blurRadius = spec.blurRadiusPx.coerceAtLeast(0f)
}
surfaceView.setBlurRegions(listOf(region))
surfaceView.setBlurRegions(emptyList()) // Clear@Language("AGSL")
private const val CYBERPUNK_SHADER = """
vec4 main(half4 in_color) {
float lum = dot(in_color.rgb, vec3(0.299, 0.587, 0.114));
vec3 shadow = vec3(0.06, 0.04, 0.25);
vec3 mid = vec3(0.92, 0.12, 0.58);
vec3 high = vec3(0.18, 0.94, 0.98);
vec3 col = lum < 0.5 ? mix(shadow, mid, lum * 2.0) : mix(mid, high, (lum - 0.5) * 2.0);
return vec4(mix(in_color.rgb, col, 0.7), in_color.a);
}
"""AsyncImage(
model = imageUri,
contentDescription = filter.displayName,
colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
.asComposeColorFilter(),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)@Test
fun scrollFling() = measureScroll(swipeSteps = 5)
private fun measureScroll(swipeSteps: Int) = rule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = defaultComposeScrollingMetrics(),
compilationMode = CompilationMode.Full(),
iterations = 5,
setupBlock = {
killProcess()
}
//...select count(*) as late_frames
from actual_frame_timeline_slice
where upid = (select upid from process where name = 'com.example.compose.jetchat.perf')
and jank_type like '%App Deadline Missed%'Image(
painter = painterResource(it),
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)AsyncImage(
model = it,
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content")
// …
val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply {val lensShader = MagnifierShaders.lens.apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
private object MagnifierShaders {
val lens: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(MAGNIFIER_LENS_SHADER) }
val glassFinish: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(GLASS_FINISH_SHADER) }Box(
modifier = Modifier
.matchParentSize()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}Box(
modifier = Modifier
.matchParentSize()
.graphicsLayer()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}android skills add perfetto-trace-analysis
android skills add jetpack-compose-performance