सॉफ़्टवेयर इंजीनियर
डेवलपर रिलेशंस इंजीनियर
क्या आपको अपने Android यूज़र इंटरफ़ेस (यूआई) को "यह काम करता है" से "यह कला का एक नमूना है" में बदलने के लिए तैयार हैं?
Jetpack Compose में ग्राफ़िक की नई सुविधाओं के बारे में जानने के लिए, हमारे साथ जुड़ें. हम आपको नए विज़ुअल इफ़ेक्ट बनाने का तरीका बताएंगे. इनमें, मेश ग्रेडिएंट से लेकर प्रोग्रेसिव ब्लर तक शामिल हैं. साथ ही, Android में मौजूद नए और बेहतरीन ग्राफ़िक्स फ़ीचर इस्तेमाल करने का तरीका भी बताएंगे. हम आपको ग्राफ़िक की सुविधाओं की परफ़ॉर्मेंस को बेहतर बनाने का तरीका भी बताएंगे. इससे बैटरी लाइफ़ या फ़्रेम रेट पर कोई असर नहीं पड़ेगा.
PolygonShape और MorphPolygonShape एपीआई उपलब्ध कराए गए हैं. MorphPolygonShape, आउटलाइन रिज़ॉल्यूशन फ़ेज़ के दौरान प्रोग्रेस को रीड करता है. साथ ही, रीकंपोज़िशन को ट्रिगर किए बिना, शेप को फ़्रेम दर फ़्रेम अपडेट करता है.RenderNode.setBackdropRenderEffect को शामिल किया गया है. डेवलपर, बैकड्रॉप इफ़ेक्ट को एक साथ जोड़कर, फ़्रॉस्टेड-ग्लास या मैग्निफ़ाइंग इफ़ेक्ट बना सकते हैं.graphicsLayer में रैप करें.ट्रांसक्रिप्ट
नमस्ते, हमारे सेशन में आपका स्वागत है. इस सेशन में, हम Compose और Android में उपलब्ध ग्राफ़िक्स की नई सुविधाओं की मदद से, आपके ऐप्लिकेशन को बेहतर बनाने के बारे में बात करेंगे.
मेरा नाम रेबेका फ़्रैंक्स है और आज मेरे साथ स्टेज पर हैं--
मैं आयुष चौधरी हूं.
नमस्ते, मेरा नाम राहुल रे है.
हम देख रहे हैं कि आप सभी Android और Compose पर उपलब्ध सुविधाओं का भरपूर इस्तेमाल कर रहे हैं. साथ ही, एआई का इस्तेमाल करके अपने शानदार आइडिया को हकीकत में बदल रहे हैं.
हमें ऐसे ऐप्लिकेशन पसंद हैं जो प्रीमियम, नेटिव, और शानदार अनुभव देते हैं.
हमें पता है कि जिन ऐप्लिकेशन को इस्तेमाल करना आसान होता है और जिन्हें उपयोगकर्ताओं से लगातार अच्छी रेटिंग मिलती है वे आम तौर पर, सिस्टम-लेवल के एपीआई में हुए नए बदलावों का फ़ायदा लेते हैं. साथ ही, वे शेयर किए गए एलिमेंट और ब्लर करने की सुविधा की मदद से, अलग-अलग प्लैटफ़ॉर्म पर एक जैसा अनुभव देते हैं. इसके अलावा, वे अलग-अलग डिवाइस के हिसाब से खुद को ढाल लेते हैं और बेहतर परफ़ॉर्म करते हैं. साथ ही, वे उपयोगकर्ताओं को भ्रम में नहीं रखते और आपके डिवाइस की सुविधाओं का ज़्यादा से ज़्यादा फ़ायदा लेते हैं.
आज की इस बातचीत में, हम आपको कुछ नई सुविधाएं दिखाएंगे. इनकी मदद से, Android का ज़्यादा से ज़्यादा फ़ायदा लिया जा सकता है. जैसे, मेश ग्रेडिएंट की मदद से उपयोगकर्ताओं को बेहतर अनुभव देना, वाइड कलर गैमट की सुविधा चालू करना, पॉलीगॉन शेप के साथ शानदार ट्रांज़िशन, ब्लर करने की सुविधा में नया क्या है. आखिर में, हम यह बताएंगे कि अपने ऐप्लिकेशन में नई ग्राफ़िक सुविधाएं जोड़ते समय, परफ़ॉर्मेंस के बारे में कैसे सोचा जा सकता है.
हालांकि, आज की बातचीत में हम स्टैक के अलग-अलग लेवल पर कुछ नए एपीआई के बारे में बात करेंगे.
लेगसी व्यू यूज़र इंटरफ़ेस (यूआई) टूलकिट अब मेंटेनेंस मोड में है. हालांकि, हम Jetpack Compose को बेहतर बनाने वाले Android ग्राफ़िक्स हार्डवेयर रेंडरिंग इंजन में लगातार निवेश कर रहे हैं.
Android में ग्राफ़िक्स को तीन अलग-अलग लेयर में समझा जा सकता है: पहली लेयर में Compose API होते हैं. इसमें सभी हाई-लेवल और नए यूज़र इंटरफ़ेस (यूआई) ग्राफ़िक्स एपीआई होते हैं. जैसे, मेश ग्रेडिएंट पेंटर, पॉलीगॉन शेप, और प्रोग्रेसिव ब्लर एपीआई.
इसके बाद, हमारे पास इंटरमीडिएट लेयर है. इसमें Compose का DrawScope, ड्रॉ मॉडिफ़ायर नोड, और एजीएसएल आपको कस्टम इफ़ेक्ट बनाने की सुविधा देते हैं.
आखिर में, हमारे पास Android प्लैटफ़ॉर्म है. Compose, नेटिव Canvas को हमेशा ऐक्सेस कर सकता है. साथ ही, आपके Compose मॉडिफ़ायर में, ओएस रेंडरिंग प्रिमिटिव का इस्तेमाल कर सकता है.
आज हम आपको इसके कुछ उदाहरण दिखाएंगे.
चलिए, शुरू करते हैं.
सबसे पहले, मेश ग्रेडिएंट के बारे में जानते हैं. यह Compose 1.12 में पेश किया गया एक नया एपीआई है.
पहले हमारे पास लीनियर ग्रेडिएंट थे. इसके बाद, हमारे पास लीनियर, स्वीप, और रेडियल ग्रेडिएंट थे. ये ग्रेडिएंट, आसान ट्रांज़िशन के लिए बेहतरीन हैं. हालांकि, अगर आपको ऐंबियंट बैकग्राउंड या फ़्लडलाइटिंग इफ़ेक्ट जैसे कई रंगों वाले ब्लेंड चाहिए, तो आपको सिस्टम के 2D ग्रिड की ज़रूरत होगी. इसमें, पोज़िशन और रंगों को स्पेस में ब्लेंड किया जा सकता है.
अब इस तरह का मेश ग्रेडिएंट बनाने के लिए, सबसे पहले यह तय करें कि आपको पैच की कितनी पंक्तियां और कॉलम चाहिए.
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}इसके बाद, ट्रेलिंग लैम्ब्डा के अंदर, setVertex को कॉल करें और 0F से 1F के बीच का नॉर्मलाइज़्ड ऑफ़सेट असाइन करें. साथ ही, हर ग्रिड इंटरसेक्शन के लिए एक रंग असाइन करें.
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)
)पेंटर तय करने के बाद, पेंट मॉडिफ़ायर का इस्तेमाल करके उसे किसी भी कंपोज़ेबल के साथ कॉल करें.
अब हमने JetChat ऐप्लिकेशन में एक सुविधा जोड़ी है. इसकी मदद से, किसी मैसेज पर दो बार टैप करके उसे पसंद किया जा सकता है.
इसे बनाने के लिए, हम 2 बाई 3 मेश सेट अप करते हैं और कॉम्पोनेंट में कलर स्टॉप डालते हैं.
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
}ध्यान दें कि हमने यहां hasBicubicColor को 'सही' पर सेट किया है.
डिफ़ॉल्ट रूप से, रंग सभी ट्रायंगल में लीनियर तरीके से इंटरपोलेट होते हैं. इससे कभी-कभी विज़िबल पैच छूट सकते हैं.
इस सुविधा को चालू करने पर, शेडर स्मूद बाइ क्यूबिक इंटरपोलेशन पर स्विच हो जाता है. इससे रंग आसानी से एक-दूसरे में मिल जाते हैं.
अब, अगर आपको इनपुट बॉक्स के पीछे इस तरह का ऐनिमेटेड ऐंबियंट ग्लो बनाना है, तो क्या करना होगा?
मेश ग्रेडिएंट की मदद से ऐसा इफ़ेक्ट बनाने के लिए, आपको दो चीज़ों की ज़रूरत होगी.
सबसे पहले, अंदरूनी वर्टिस के लिए एक स्मूद ऑर्गैनिक मोशन और बाहरी किनारों को सॉफ़्ट करने का तरीका, ताकि आपको कार्ड के आस-पास एक हार्ड रेक्टैंगल न मिले.
इसलिए, हम दो फ़ेज़ ऐंगल वाले rememberInfiniteTransition को सेट अप करके शुरुआत करते हैं. ये ऐंगल, अलग-अलग समय पर थोड़ा-थोड़ा चलते हैं, ताकि मोशन मैकेनिकल के बजाय ऑर्गैनिक लगे.
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",
)यहां, हम अंदरूनी वर्टिस को ऐनिमेट करते हैं और बाहरी रिंग को पिन करते हैं. बाहरी रिंग के वर्टिस को 0 ऐल्फ़ा वाली किनारों के साथ पिन करते हैं.
बाहरी बाउंड्री स्थिर और पारदर्शी होने की वजह से, ग्लो नैचुरल तरीके से फ़ेदर आउट होता है.
आखिर में, हम उन ऐनिमेटेड ऑफ़सेट को अपने 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])
}
}
}आपने अभी जो मेश ग्रेडिएंट देखा है उसे Android 14 और इसके बाद के वर्शन पर, drawMesh एपीआई का इस्तेमाल करके बनाया गया है.
चलिए, इस एपीआई के बारे में गहराई से जानते हैं. इसके लिए, हम शुरुआत से एक इफ़ेक्ट बनाते हैं.
JetChat की वेलकम स्क्रीन के लिए, हमें एक ऐसा पाथ बनाना है जो रिबन जैसा दिखे.
यहां दो चीज़ों पर ध्यान दें: स्ट्रोक की चौड़ाई एक जैसी नहीं है और रंग, पाथ के हर स्ट्रोक के साथ चलता है.
चलिए, इस इफ़ेक्ट को बनाने के लिए ज़रूरी चीज़ों के बारे में जानते हैं.
हर पिक्सल के लिए, हमें यह तय करना होगा कि उसे ड्रॉ करना है या नहीं और किस रंग का इस्तेमाल करना है.
यह पता लगाने के लिए, हमें दो दूरियों की ज़रूरत होती है.
पहली वैल्यू से पता चलता है कि कोई पिक्सल, स्ट्रोक के सेंटर से कितनी दूरी पर है.
दूसरी दूरी यह है कि पाथ की शुरुआत से लेकर आखिर तक, कोई पिक्सल कितनी दूर है.
इससे हमें पता चलता है कि ग्रेडिएंट से कौनसा रंग चुनना है. साथ ही, पहली वैल्यू को थोड़ा मिलाने से, चौड़ाई के हिसाब से रंग का कर्व बनता है.
क्या हम सिर्फ़ पाथ और पेंट का इस्तेमाल करके, किसी पाथ के साथ ग्रेडिएंट बना सकते हैं?
इसलिए, हां, हम ऐसा कर सकते हैं.
इसके लिए, पाथ को छोटी-छोटी सीधी लाइनों की सूची में फ़्लैट करना होगा. इससे पाथ के साथ-साथ दूरी भी पता चलती है.
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;
}क्रॉस कोऑर्डिनेट की ऐब्सलूट वैल्यू लेने से, हमें सेंटर लाइन से पिक्सल-टू-पिक्सल दूरी मिलती है.
कवरेज पाने के लिए, हम उस दूरी की तुलना रेडियस से करते हैं. साथ ही, रंग के लिए, हम लाइन के पाथ के साथ ब्लेंड करते हैं और ग्रेडिएंट को किनारे की ओर थोड़ा मोड़ते हैं.
कवरेज को कवरेज से गुणा करें. इससे हमें फ़ाइनल कलर मिलेगा.
Compose साइड पर, हम अपने शेडर और डेटा लेआउट को MeshSpecification ऑब्जेक्ट के साथ जोड़ते हैं.
val spec = remember {
MeshSpecification.make(
arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
vertexStride = 8,
arrayOf(Varying(TYPE_FLOAT3, "stroke")),
LineShaders.VERTEX, LineShaders.FRAGMENT)
}सबसे पहले, हम अपने कॉर्नर एट्रिब्यूट को बाइट 0 से शुरू होने वाले दो फ़्लोट के तौर पर तय करते हैं.
दो फ़्लोट आठ बाइट लेते हैं. इसलिए, बफ़र में हर कॉर्नर स्ट्राइड आठ है.
इसके बाद, हम अपने स्ट्रोक के वैरिएशन को तीन फ़्लोट के तौर पर तय करते हैं. साथ ही, हमने अभी जो वर्टेक्स और फ़्रैगमेंट शेडर लिखा है उसे पास करते हैं.
हम वर्टेक्स बफ़र बनाते हैं.
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
}लाइन के हर चरण पर, हम प्रोग्रेस को पूरा करते हैं. हम प्रोग्रेस को 0 से 1 तक कंप्यूट करते हैं और बफ़र में दो कोने लिखते हैं.
आखिर में, हम स्पेक और बफ़र को मेश ऑब्जेक्ट में जोड़ते हैं.
val mesh = Mesh(
spec,
mode = Mesh.TRIANGLE_STRIP,
vertexBuffer = corners,
vertexCount = (segments + 1) * 2,
bounds = RectF(0f, 0f, size.width, size.height)
)Compose कैनवस में अब तक drawMesh को एक्सपोज़ नहीं किया गया है. इसलिए, हम nativeCanvas पर ड्रॉप डाउन करते हैं और मेश ड्रॉ करते हैं.
Box(Modifier.fillMaxSize().drawWithCache {
val mesh = Mesh(...)
onDrawBehind {
mesh.setFloatUniform("uTime", time.value)
drawIntoCanvas {
it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
}
}
})ज्यामिति और पिक्सल को कंट्रोल किया जा सकता है. इसलिए, ड्रॉ मेश का इस्तेमाल करके, ज्यामिति से जुड़ा कोई भी विज़ुअल इफ़ेक्ट बनाया जा सकता है.
इस एपीआई के बारे में ज़्यादा जानने के लिए, do- दस्तावेज़ देखें.
Compose 1.12 में, हमने वाइड कलर गैमट के लिए सहायता उपलब्ध कराई है.
इसका क्या मतलब है?
क्या आपको पता है कि स्क्रीन, स्टैंडर्ड आरजीबी कलर स्पेस की तुलना में अलग-अलग रंगों को दिखा सकती हैं?
उदाहरण के लिए, इस इमेज में सफ़ेद रंग का त्रिकोण, स्टैंडर्ड sRGB रंगों की रेंज दिखाता है. ज़्यादातर स्क्रीन पर, इस रेंज के रंग दिखते हैं.
हालांकि, नई स्क्रीन पर ज़्यादा रंगों को दिखाया जा सकता है. जैसे, पीले रंग के त्रिकोण में दिखाए गए रंग. इनमें से कुछ रंग ज़्यादा गहरे और चमकदार होते हैं.
लेकिन जब हम किसी रंग को दिखाते हैं, तो हम उसे उस त्रिभुज के हर शीर्ष के ब्यौरे के साथ दिखाते हैं.
ग्रेडिएंट की इस तुलना को देखते समय, डिसप्ले गैमट और ग्रेडिएंट इंटरपोलेशन के बीच अंतर करना ज़रूरी है.
स्टैंडर्ड sRGB डिसप्ले या इस प्रोजेक्टर पर, स्क्रीन इस sRGB गैमट के बाहर के रंगों को रेंडर नहीं कर सकती.
इसलिए, फ़िलहाल स्टेज पर इसका डेमो देना काफ़ी मुश्किल है.
इसलिए, आपको मुझ पर भरोसा करना होगा कि वाइड कलर गैमट की सुविधा वाले Android डिवाइसों पर, रंग बेहतर दिखते हैं.
इस उदाहरण में, आपको जो दिख रहा है वह असल में एक बड़े कलर स्पेस में कलर स्टॉप को इंटरपोलेट करने का असर है.
वाइड या लीनियर कलर स्पेस में रंगों को ब्लेंड करने से, इस ग्रेडिएंट का असल गणितीय पाथ बदल जाता है. इससे, बीच में ग्रे रंग और गहरे रंग की बैंडिंग को रोका जा सकता है. ऐसा तब हो सकता है, जब वाइड कलर गैमट के बिना ऐसा किया जाए.
Compose 1.12 की मदद से, आपको किसी भी डिसप्ले पर स्मूद ग्रेडिएंट ब्लेंडिंग का फ़ायदा मिलता है. साथ ही, अगर आपके पास इस सुविधा के साथ काम करने वाला हार्डवेयर है, तो आपको वाइड कलर गैमट की सुविधा भी मिलती है.
डिवाइस पर सही तरीके से काम करने के लिए, पक्का करें कि आपकी गतिविधि या विंडो को वाइड कलर गैमट मोड चालू करने के लिए सेट किया गया हो.
ऐसा AndroidManifest में किया जा सकता है.
<!-- 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
}
}colorMode को सेट किए बिना, विंडो स्टैंडर्ड 8-बिट sRGB में रहती है. साथ ही, जीपीयू से रेंडर किए गए किसी भी वाइड कलर गैमट के रंग क्लिप हो जाएंगे.
इसलिए, अगर डिसप्ले पर वाइड कलर गैमट के रंग नहीं दिख सकते, तो उनका इस्तेमाल करने से बचें.
आपको सिर्फ़ उन गतिविधियों या कोड पाथ पर वाइड कलर गैमट चालू करना चाहिए जो असल में मीडिया, फ़ोटो या रिच ग्राफ़िक्स दिखाते हैं. इससे बैटरी और परफ़ॉर्मेंस को बेहतर बनाने में मदद मिलती है.
इसका मतलब है कि Compose में अलग-अलग कलर स्पेस में रंग बनाए जा सकते हैं. अब इन्हें सही तरीके से इस्तेमाल किया जाता है.
// 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
)आपके पास यह तय करने का विकल्प होता है कि आपको किस तरह के colorSpace में रंग बनाना है. अगर आपने कोई colorSpace नहीं चुना, तो स्टैंडर्ड sRGB का इस्तेमाल किया जाएगा.
खास तौर पर बिट मैप के लिए, getColorSpace का इस्तेमाल करके बिट मैप के सोर्स की जांच की जा सकती है. साथ ही, इमेज लोड करने वाली लोकप्रिय लाइब्रेरी, आपके लिए बिट मैप के सही कलर स्पेस को अपने-आप मैनेज कर लेंगी.
रंग की जानकारी को सुरक्षित रखने के लिए, आपको Bitmap.Config.HARDWARE का इस्तेमाल करना चाहिए. ऐसा इसलिए, क्योंकि 565 जैसे कॉन्फ़िगरेशन से सटीक जानकारी नहीं मिलती और उपयोगकर्ताओं को बैंडिंग दिखती है.
रंगों के बारे में ज़्यादा जानने के लिए, 2017 में वापस जाएं. इसमें रोमेन गाय ने Compose में रंगों के बारे में या Android में रंगों के बारे में बताया है.
Compose 1.13 के साथ, हम पॉलीगॉन शेप के लिए नेटिव सपोर्ट की सुविधा लॉन्च कर रहे हैं.
इससे कस्टम शेप और नैचुरल दिखने वाले यूज़र इंटरफ़ेस (यूआई) एलिमेंट बनाना काफ़ी आसान हो जाएगा.
पहले, अगर आपको इस तरह की कोई जटिल ज्यामिति बनानी होती थी, तो आपको ज्यामिति के बारे में बताने के लिए RoundedPolygon का इस्तेमाल करना पड़ता था. इससे एक पाथ आउटपुट होता था और फिर उस पाथ को Compose Shape API से मैप करना पड़ता था.
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)
)
}उदाहरण के लिए, पहले RoundedPolygon का इस्तेमाल करके अनुपात तय करें. इसके बाद, हम इस स्निपेट में, अपने अंदरूनी पॉइंट को बाहरी पॉइंट की तुलना में, केंद्र से 84% पर सेट करते हैं.
यहां कोने को गोल करने की सुविधा भी लागू की जा सकती है. हालांकि, इससे सिर्फ़ मैथमेटिकल कर्व के बारे में पता चलता है.
Compose में क्लिपिंग या बैकग्राउंड के लिए इस पॉलीगॉन का इस्तेमाल करने के लिए, आपको इस तरह का कस्टम शेप रैपर लिखना होगा.
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)
}
}इस createOutline स्निपेट में, आपको पाथ बनाना होगा. साथ ही, उन्हें मैन्युअल तरीके से स्केल और ट्रांसलेट करना होगा.
अब नए PolygonShape एपीआई की मदद से, आपको उस कस्टम Shape रैपर की ज़रूरत नहीं है.
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)यह Compose Shape बिल्डर है. इसलिए, इसे डेंसिटी और साइज़ की जानकारी सीधे तौर पर मिलती है.
इसका मतलब है कि आपके पास डाइनैमिक तौर पर वर्टिसेस का हिसाब लगाने का विकल्प होता है. साथ ही, सीधे तौर पर डीपी या प्रतिशत के तौर पर, कोने को गोल करने की वैल्यू तय की जा सकती है.
एक बार तय हो जाने के बाद, इसे किसी भी ऐसे एपीआई को पास किया जा सकता है जो शेप लेता है.
हम MorphPolygonShape एपीआई भी लॉन्च कर रहे हैं. इसकी मदद से, अलग-अलग पॉलीगॉन शेप के बीच आसानी से ट्रांज़िशन किया जा सकता है.
val recordShape = remember {
MorphPolygonShape(
start = PolygonShape.circle(),
end = PolygonShape.star(
numPoints = 9,
innerRadiusRatio = 0.8f,
outerRounding = CornerRounding(percent = 50),
),
) { shapeProgress.value }
}इस उदाहरण में, हमारे पास एक रिकॉर्ड बटन है. जब इस पर इंटरैक्ट किया जाता है, तो यह सर्कल से स्टार में बदल जाता है.
आपको बस शुरुआती शेप, आखिरी शेप, और प्रोग्रेस वैल्यू देनी होगी.
MorphPolygonShape एपीआई, आउटलाइन रिज़ॉल्यूशन फ़ेज़ के दौरान प्रोग्रेस वैल्यू को रीड करता है. इसका मतलब है कि यह कंपोज़िशन के दौरान नहीं, बल्कि रीकंपोज़िशन के दौरान रीड करता है.
इसका मतलब है कि जब आपका शेप एक फ़्रेम से दूसरे फ़्रेम में बदलता है, तो वह फिर से कंपोज़ किए बिना ही एक नई आउटलाइन में बदल जाता है.
अब बात करते हैं, धुंधला करने की सुविधा के बारे में.
पिछले कुछ रिलीज़ से, हम ब्लर करने की सुविधा को लागू करने की प्रोसेस को और आसान बनाने की कोशिश कर रहे हैं. साथ ही, इसे ज़्यादा बेहतर बनाने की कोशिश कर रहे हैं.
जब भी हम ब्लर करने की बात करते हैं, तो हमें आम तौर पर यह मॉडिफ़ायर दिखता है.
Modifier
.blur(
radiusX = 10.dp,
radiusY = 10.dp,
edgeTreatment = BlurredEdgeTreatment(
shape = RoundedCornerShape(8.dp),
),
)Compose में यह मॉडिफ़ायर, Android 12 से उपलब्ध है.
यह तब बेहतर तरीके से काम करता है, जब आपको एक जैसा ब्लर इफ़ेक्ट चाहिए. इसमें, कंपोज़ेबल में एक ही ब्लर रेडियस लागू किया जाता है.
हालांकि, मॉडर्न यूज़र इंटरफ़ेस (यूआई) डिज़ाइन में, अक्सर शार्प से ब्लर किए गए हिस्सों में ट्रांज़िशन को स्मूद बनाने की ज़रूरत होती है, न कि हार्ड कटऑफ़ की.
अब बात करते हैं प्रोग्रेसिव ब्लर की, जो Compose 1.13 से एक आधिकारिक एपीआई बन गया है.
पूरे कंपोज़ेबल में एक ही रेडियस का इस्तेमाल करने के बजाय, अब स्पेस में ब्लर की इंटेंसिटी को अलग-अलग किया जा सकता है.
यहां उपयोगकर्ता की प्रोफ़ाइल स्क्रीन से, हमारे JetChat ऐप्लिकेशन का उदाहरण दिया गया है.
नए ब्लर मॉडिफ़ायर में, ब्लर रेडियस की जानकारी दी जाती है.
Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}यहां हम दो ब्लर स्टॉप के साथ वर्टिकल ग्रेडिएंट का इस्तेमाल करते हैं. इसमें फ़ोटो के ऊपरी आधे हिस्से को 0 डीपी पर पूरी तरह से शार्प रखा जाता है और निचले हिस्से को 32 डीपी तक ब्लर किया जाता है.
ध्यान दें कि हमने edgeTreatment को अनबाउंड के तौर पर भी सेट किया है. इससे ब्लर को कंपोज़ेबल बाउंड के बाहर भी पारदर्शी पिक्सल का सैंपल लेने के लिए कहा जाता है.
वर्टिकल फ़ेड-आउट के अलावा, ब्लर रेडियस की इस खास जानकारी से आपको प्रोग्रेसिव ब्लर को कंट्रोल करने के कई तरीके मिलते हैं.
उदाहरण के लिए, बाईं ओर हॉरिज़ॉन्टल ग्रेडिएंट देखा जा सकता है. इसमें शुरुआती रेडियस 0 डीपी है और आखिरी रेडियस, स्लाइडर की वैल्यू से तय होता है.
BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)दाईं ओर, हमारे पास एक रेडियल ग्रेडिएंट है. इसके सेंटर में, ब्लर रेडियस 0 डीपी है. जैसे-जैसे आप fallOffRadius की ओर बढ़ते हैं, जो इस मामले में 300 डीपी है, ब्लर स्मूदली रैंप 24 डीपी तक बढ़ जाता है.
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
},यहां हम एक छोटा फ़्रैगमेंट शेडर लिखते हैं, जो ऐनिमेटेड डायगनल साइन वेव की गणना करता है. साथ ही, उस इंटेंसिटी को ऐल्फ़ा चैनल में आउटपुट करता है.
इसके बाद, शेडर की खास जानकारी, 0 से 1 मास्क को सीधे 24 डीपी मास्क maxRadius पर मैप करती है.
बैकग्राउंड को धुंधला करने की सुविधा के बारे में बात करने से पहले, आइए दोनों के बीच का अंतर देखते हैं.
अब बाईं ओर, हमारे पास एक टॉप ऐप्लिकेशन बार है. इसमें हमने लेयर ब्लर लागू किया है.
इसका मतलब है कि यह कंपोज़ेबल को ही धुंधला कर देता है.
जैसा कि देखा जा सकता है, टाइटल और कार्रवाई करने से जुड़े आइकॉन धुंधले हैं. हालांकि, उनके पीछे मौजूद चैट कॉम्पोनेंट बिलकुल भी धुंधला नहीं है.
दाईं ओर, बैकग्राउंड को धुंधला करने की सुविधा है.
यहां सिर्फ़ उन चैट मैसेज को सैंपल किया जाता है और धुंधला किया जाता है जो टॉप बार के पीछे स्क्रोल हो रहे हैं.
हालांकि, टेक्स्ट और आइकॉन पूरी तरह से साफ़ और पढ़ने लायक हैं.
Android 37.2 से, RenderNode.setBackdropRenderEffect नाम के नए एपीआई का इस्तेमाल करके, बैकग्राउंड को धुंधला किया जा सकता है.
हालांकि, ब्लर करने की सुविधा में हुए सुधारों के बारे में जानने से पहले, आइए यह समझें कि Android बैकग्राउंड में कैसे काम करता है, ताकि ब्लर करने की सुविधा बेहतर तरीके से काम कर सके.
Android, RenderNodes का इस्तेमाल करता है. ये हार्डवेयर-ऐक्सेलरेटेड डिसप्ले लिस्ट कंटेनर होते हैं. इसका मतलब है कि ये रिकॉर्डिंग शुरू करने और रिकॉर्डिंग खत्म करने के ज़रिए, ड्रॉइंग के अलग-अलग निर्देशों को कंपैक्ट और स्टोर कर सकते हैं.
RenderNode में आपके 3D अनुवाद, रोटेशन, ऐल्फ़ा, स्केल, और पोज़िशन बाउंड जैसी जानकारी होती है.
Compose, बैकग्राउंड में RenderNode का इस्तेमाल करता है.
इसलिए, Compose में बैकड्रॉप रेंडर इफ़ेक्ट इस्तेमाल करने के लिए, हम कस्टम Modifier.Node बना सकते हैं, ताकि इस मॉडिफ़ायर का फिर से इस्तेमाल किया जा सके.
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()
}
}हम RenderNode का रेफ़रंस सेव रखते हैं. साथ ही, RenderNode को उन इफ़ेक्ट के साथ कॉन्फ़िगर किया जाता है जिन्हें हम किसी दूसरे फ़ंक्शन में इस्तेमाल करना चाहते हैं.
उदाहरण के लिए, हम यहां ब्लर इफ़ेक्ट पास करते हैं.
इसके बाद, हम drawIntoCanvas को कॉल करते हैं. यह हमारे नेटिव कैनवस पर drawRenderNode को कॉल करता है.
इसके बाद, हम drawContent को कॉल करते हैं, ताकि धुंधला न किया गया असल कॉन्टेंट भी सबसे ऊपर दिखे.
हमने जो मॉडिफ़ायर अभी-अभी बनाया है उसका इस्तेमाल करने के लिए, हम Modifier.backdropBlur को कॉल करते हैं और ब्लर रेडियस की जानकारी पास करते हैं.
JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.uniform(5.dp)
)
)ध्यान दें कि इस तरीके से, हमें यहां अंडरलाइंग लेयर पर कुछ भी डालने की ज़रूरत नहीं है.
RenderNode.setBackdropRenderEffect, JetChat ऐप्लिकेशन बार के नीचे मौजूद कॉन्टेंट का सैंपल अपने-आप ले रहा है.
इससे बैकग्राउंड को धुंधला करने की सुविधा का शानदार नतीजा मिलता है.
हम प्रोग्रेसिव ब्लर रेडियस की वैल्यू भी सेट कर सकते हैं. इससे कॉन्टेंट पर वर्टिकल ब्लर भी लागू किया जा सकता है.
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),
),
)
)
)वर्टिकल ग्रेडिएंट की मदद से, कॉन्टेंट को धुंधला करके दिखाया जाता है. इसके लिए, कंटेनर के अलग-अलग हिस्सों में अलग-अलग रेडियस का इस्तेमाल किया जाता है.
हम Compose में, पहले से मौजूद बैकड्रॉप ब्लर मॉडिफ़ायर पर काम कर रहे हैं.
यह Compose की मदद से Android पर इस्तेमाल की जाने वाली टेक्नोलॉजी का एक उदाहरण है.
मैंने आपको जो उदाहरण दिखाया है वह सिर्फ़ एपीआई लेवल 37.2 पर काम करेगा. हालांकि, हम Compose के लिए बैकपोर्ट पर भी काम कर रहे हैं. इसलिए, हमारे साथ बने रहें.
RenderNode बैकड्रॉप इफ़ेक्ट का एक और मज़ेदार उदाहरण यह है कि हम उन्हें एक साथ जोड़कर, वाकई अच्छे दिखने वाले यूज़र इंटरफ़ेस (यूआई) बना सकते हैं. साथ ही, इसके ऊपर कई अलग-अलग इफ़ेक्ट भी लागू कर सकते हैं.
इसलिए, इफ़ेक्ट को चेन करने के लिए, हम ये दो इफ़ेक्ट बनाते हैं.
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)इस उदाहरण में, हम ब्लर इफ़ेक्ट और 30% सैचुरेशन बनाते हैं.
हम createChainEffect को कॉल करते हैं. इसमें पहले ब्लर इफ़ेक्ट और फिर सैचुरेशन इफ़ेक्ट लागू किया जाता है.
यह JetChat का एक उदाहरण है.
val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)जंप-टू बटन, सबसे नीचे जाने वाला बटन. इसे मैग्नीफ़ाइंग ग्लास जैसा दिखाने के लिए, हम कुछ अलग-अलग इफ़ेक्ट को एक साथ जोड़ सकते हैं. इससे इस कंपोज़ेबल के किनारे, मैग्नीफ़ायर की तरह गोल हो जाते हैं.
हालांकि, मैग्निफ़ाइंग इफ़ेक्ट लागू करने के अलावा, किसी भी तरह के शेडर को एक साथ जोड़कर बैकड्रॉप पर लागू किया जा सकता है.
यहां कुछ मज़ेदार उदाहरण दिए गए हैं, जिन्हें हमने लागू किया है. इनसे पता चलता है कि इन दोनों को एक साथ इस्तेमाल करने पर क्या-क्या किया जा सकता है.
अब फ़्रॉस्टेड-ग्लास-टाइप इफ़ेक्ट का इस्तेमाल किया जा सकता है.
हालांकि, हम कैलाइडोस्कोप जैसे अन्य मज़ेदार इफ़ेक्ट भी बना सकते हैं.
इसके अलावा, अगर हमें इस Android मैस्कॉट को एक ऐसे यूज़र इंटरफ़ेस (यूआई) एलिमेंट में बदलना है जो अपने नीचे मौजूद कॉन्टेंट को बदल देता है, तो हम बैकड्रॉप रेंडर इफ़ेक्ट भी लागू कर सकते हैं. इससे ऐसा लगेगा कि हम इस छोटे से ड्रॉइड के ज़रिए देख रहे हैं.
हम बैकड्रॉप रेंडर इफ़ेक्ट की मदद से भी ऐसा कर सकते हैं.
इस नए एपीआई की मदद से, बैकड्रॉप को धुंधला करने और फ़्रॉस्टेड ग्लास इफ़ेक्ट के अलावा, कई अन्य इफ़ेक्ट भी इस्तेमाल किए जा सकते हैं.
हमने प्लैटफ़ॉर्म पर एक और नया एपीआई लॉन्च किया है. इसका नाम SurfaceView.setBlurRegions है.
फ़िलहाल, Modifier.blur और RenderNode बैकड्रॉप इफ़ेक्ट का इस्तेमाल करके, टेक्सचर व्यू और किसी भी कंपोज़ेबल को धुंधला किया जा सकता है. हालांकि, SurfaceView के कुछ हिस्सों को धुंधला करना मुश्किल है. साथ ही, यह ऐप्लिकेशन के व्यू RenderNode ट्री का हिस्सा नहीं है, क्योंकि यह विंडो में एक छेद कर देता है.
इसलिए, Compose असल में SurfaceView के बैकिंग बफ़र का सैंपल नहीं ले सकता. इस वजह से, एपीआई 37.2 से पहले ऐसा करना मुमकिन नहीं था.
इसलिए, SurfaceView का setBlurRegions, ऐप्लिकेशन को सीधे तौर पर धुंधले हिस्सों के बारे में बताने की अनुमति देता है.
उदाहरण के लिए, JetChat के इस वीडियो प्लेयर में, हमने SurfaceView blur का इस्तेमाल करके, कंट्रोल दिखने और छिपने पर ब्लर इफ़ेक्ट जोड़ा है.
इस नए एपीआई का इस्तेमाल करने के लिए, SurfaceView तरीके पर setBlurRegions को ऐक्सेस किया जा सकता है.
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यह बाउंड के एरिया, ब्लर रेडियस, ऐल्फ़ा, और कॉर्नर रेडियस की सूची लेता है.
इन सीमाओं को पास करते समय, उन्हें स्क्रीन पर SurfaceView के हिसाब से होना चाहिए.
इसलिए, आपको SurfaceView के कोऑर्डिनेट और उस हिस्से को ट्रैक करना होगा जिसे आपको धुंधला करना है.
Android 16 और इसके बाद के वर्शन में, RuntimeColorFilter की सुविधा भी जोड़ी गई है.
पहले, इमेज के लिए सिर्फ़ स्टैटिक मैट्रिक्स कलर फ़िल्टर सेट किया जा सकता था. साथ ही, सैटुरेशन जैसे इफ़ेक्ट लागू किए जा सकते थे.
हालांकि, Android 16 और इसके बाद के वर्शन में, अब ऐसा शेडर पास किया जा सकता है जो किसी खास कॉन्टेंट के लिए इनपुट पिक्सल और इन कलर लेता है और नया कलर जनरेट करता है.
उदाहरण के लिए, अगर हमें किसी फ़ोटो पर साइबरपंक फ़िल्टर लागू करना है, तो हम एक छोटा शेडर तय कर सकते हैं. इसका इस्तेमाल, 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);
}
"""इस शेडर से साइबरपंक थीम बनती है. हालांकि, अपनी पसंद के मुताबिक कोई भी इफ़ेक्ट बनाया जा सकता है.
इसलिए, शेडर का इस्तेमाल करने के लिए, हम RuntimeColorFilter लागू करते हैं और इसे Compose कलर फ़िल्टर में बदलते हैं.
AsyncImage(
model = imageUri,
contentDescription = filter.displayName,
colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
.asComposeColorFilter(),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)जैसा कि इस उदाहरण में देखा जा सकता है, अब हम पहले से ज़्यादा डाइनैमिक इफ़ेक्ट बना सकते हैं. इसके लिए, हमें OpenGL, SurfaceView सेटअप या इस तरह की किसी भी चीज़ के बारे में ज़्यादा जानने की ज़रूरत नहीं है.
इसे किसी भी कंपोज़ेबल पर लागू किया जा सकता है. इसलिए, यह सिर्फ़ इमेज पर ही नहीं, बल्कि अन्य चीज़ों पर भी लागू होता है.
धन्यवाद.
हमने कुछ बेहतरीन इफ़ेक्ट बनाए हैं. हालांकि, हमें यह पक्का करना होगा कि हमारा ऐप्लिकेशन अब भी तेज़ और आसान हो.
बातचीत को स्क्रोल करने पर, हमारे सभी इफ़ेक्ट एक साथ दिखते हैं.
आइए, इस स्क्रोल को मापते हैं.
@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%'इसका मतलब है कि हमें छह लेट फ़्रेम मिले.
चलिए, देर से मिले फ़्रेम में से कोई एक खोलते हैं.
इसलिए, इस फ़्रेम से ठीक पहले, Compose अगले मैसेज को पहले से फ़ेच कर लेता है. ऐसा वह मैसेज के स्क्रोल अप होकर व्यू में आने से पहले करता है.
प्रीफ़ेच को फ़्रेम के बीच के छोटे गैप में फ़िट होना चाहिए. हालांकि, इस मैसेज को तैयार करने से इसकी इमेज भी मुख्य थ्रेड पर डिकोड हो जाती है. इससे यह बिना किसी रुकावट के लंबे समय तक चलता है.
इस इमेज को डिकोड करने में मुख्य थ्रेड के अटक जाने की वजह से, कोई नया फ़्रेम शुरू नहीं हो सकता.
इसके बाद, जब वह मैसेज असल में स्क्रोल करके स्क्रीन पर दिखता है, तो RenderThread को मेमोरी असाइन करनी पड़ती है और उन सभी पिक्सल को जीपीयू पर अपलोड करना पड़ता है. इस वजह से, वही इमेज दूसरे फ़्रेम में देरी से दिखती है.
अब हमें पता है कि हमारा समय कहां खर्च हुआ. हालांकि, क्या हमें इस फ़्रेम को ड्रॉ करने के लिए, इन सभी कामों की ज़रूरत है?
हमें कैसे पता चलेगा?
ट्रेस से हमें पता चलता है.
जब भी किसी बड़े बिटमैप को स्क्रीन पर बहुत छोटे हिस्से में ड्रॉ किया जाता है, तो Android, ट्रेस में मार्कर को लॉग करता है.
इसमें पिक्सल में दो संख्याएं दिखती हैं: बिटमैप का पूरा डाइमेंशन और स्क्रीन पर असल में कवर किया गया हिस्सा.
इसे स्क्रीन डेंसिटी से भाग दें. इससे आपको डीपी में वैल्यू मिल जाएगी.
इसके बाद, हम उस इमेज को ढूंढने के लिए, अपने कोड में डीपी वैल्यू खोजते हैं.
हमें यह जानकारी मिली है.
इमेज को painterResource के साथ लोड किया जाता है. इसे यह नहीं पता होता कि कंटेनर कितना बड़ा है. इसलिए, यह पूरी इमेज को फ़ुल रिज़ॉल्यूशन में डिकोड करता है.
Image(
painter = painterResource(it),
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)इसलिए, हम इसे कॉइल से मिलने वाले AsyncImage से बदल देते हैं.
AsyncImage(
model = it,
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)यह पहले कंटेनर को मेज़र करता है, ताकि सिर्फ़ ज़रूरी पिक्सल को डिकोड किया जा सके. साथ ही, यह काम एक अलग थ्रेड पर किया जाता है.
उसी मानदंड को फिर से चलाने पर, मुख्य थ्रेड पर इमेज डिकोड करने की संख्या घटकर 0 हो गई. साथ ही, देर से रेंडर होने वाले फ़्रेम की संख्या घटकर 2 हो गई.
हम यहां उन दो फ़्रेम को डीबग नहीं करेंगे. हालांकि, इससे यह बात साफ़ हो जाती है कि ऐसे पिक्सल को कंप्यूट करने की कोशिश न करें जो कभी फ़्रेम तक नहीं पहुंचेंगे.
इसलिए, इमेज को डिकोड करने में इतना समय लग रहा था कि फ़्रेम देर से रेंडर हो रहे थे. हालांकि, हर समस्या की वजह से फ़्रेम ड्रॉप नहीं होता. साथ ही, कुछ ऐसी छोटी-मोटी समस्याएं भी होती हैं जिनसे कोड लिखते समय बचा जा सकता है.
आइए, इनमें से दो के बारे में जानते हैं.
जब भी हम RuntimeShader का इस्तेमाल करें, तो हमें इसे एक बार बनाना चाहिए. ऐसा इसलिए, क्योंकि यह सिर्फ़ कंपाइल होता है और कैश मेमोरी में सेव होता है. हालांकि, जब भी हम इसे बनाते हैं, तब भी यह अपनी सोर्स स्ट्रिंग को पार्स करता है.
जब भी कोई इफ़ेक्ट हर फ़्रेम को ऐनिमेट करता है, तो हम उसे graphicsLayer में रैप कर सकते हैं. इससे सिर्फ़ ऐनिमेट होने वाले एलिमेंट को फिर से रिकॉर्ड किया जाता है. इसके आस-पास के यूज़र इंटरफ़ेस (यूआई) को फिर से ड्रॉ नहीं किया जाता.
'सबसे नीचे जाएं' बटन, स्क्रोल करने पर हमेशा दिखता है.
इस इफ़ेक्ट को दिखाने के लिए, दो RuntimeShader का इस्तेमाल किया जाता है. जब भी बटन दिखता है, तब हम दोनों शेडर को उनकी सोर्स स्ट्रिंग से फिर से बनाते हैं.
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) }इस बदलाव की वजह से, इफ़ेक्ट बनाने की प्रोसेस करीब 40 गुना तेज़ हो गई है. साथ ही, बटन दिखने पर होने वाला खर्च 20 गुना कम हो गया है.
मेश ग्रेडिएंट ग्लो में, हमने देखा कि 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)
}इसलिए, ड्रॉ फ़ेज़ में स्टेट को रीड करने से, कंपोज़िशन और लेआउट बना रहता है. हालांकि, डिसप्ले लिस्ट, ड्रॉ कमांड को अपडेट नहीं कर सकती.
इसलिए, Compose ट्री में ऊपर की ओर जाकर, ग्राफ़िक्स लेयर वाले सबसे नज़दीकी नोड को ढूंढता है. यहां यह लाल आउटलाइन है, जो पूरी स्क्रीन है. साथ ही, हर फ़्रेम में उस पूरी लेयर को फिर से रिकॉर्ड करता है.
बॉक्स पर सीधे graphicsLayer डालने से, ट्रैवल हरे रंग की आउटलाइन पर रुक जाता है. इसलिए, सिर्फ़ ग्लो बॉक्स को फिर से रिकॉर्ड किया जाता है और मुख्य थ्रेड ड्रॉइंग स्लाइस, मिलीसेकंड के दो-दसवें हिस्से तक कम हो जाता है.
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)
}Perfetto की मदद से, ट्रेस में क्या गड़बड़ी हुई है, इसका विश्लेषण करना मुश्किल हो सकता है. इसलिए, कुछ अहम जानकारी इकट्ठा करने के लिए, ट्रेस विश्लेषण की सुविधा का इस्तेमाल किया जा सकता है.
अगर आपको बेहतर परफ़ॉर्मेंस वाला Compose कोड लिखने में और मदद चाहिए, तो Android CLI इंस्टॉल करें और Compose की परफ़ॉर्मेंस स्किल का इस्तेमाल करें.
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