Jetpack Compose में Material You डायनामिक कलर: जब वॉलपेपर जीत जाए तो अपना ब्रांड कलर बचाए रखना
dynamicColorScheme() आपके पैलेट को यूज़र के वॉलपेपर से बने पैलेट से बदल देता है। ब्रांड रंगों को खोने के बजाय उन्हें हार्मोनाइज़ करने की एक व्यावहारिक गाइड।
Compose ऐप में डायनामिक कलर ऑन करते ही सबसे पहली चीज़ जो नज़र आती है, वो है आपके ब्रांड का गायब हो जाना। Granyn का हरा रंग, Hydrame का नीला — गायब, और उनकी जगह वो रंग आ जाता है जो dynamicColorScheme() ने यूज़र के वॉलपेपर से निकाला है। एक स्टैंडर्ड नीले वॉलपेपर पर यह ठीक चलता है। लेकिन नारंगी या मैजेंटा वॉलपेपर पर, जिस ऐप की पूरी विज़ुअल पहचान ही “वो हरा वाला” या “वो नीला वाला” होना है, वह अब ऐप ड्रॉअर के बाकी हर ऐप जैसी दिखने लगती है। यह Material You अपने डिज़ाइन के मुताबिक ही काम कर रहा है, और अगर रंग ही वह तरीका है जिससे कोई यूज़र भीड़भाड़ वाले ऐप ड्रॉअर में आपके ऐप को पहचानता है, तो यह एक असली समस्या भी है।
समाधान डायनामिक कलर को बंद कर देना नहीं है — जिन यूज़र्स ने पूरे सिस्टम के लिए एक थीम चुनी है, वे तुरंत नोटिस कर लेते हैं जब कोई एक ऐप मेल खाने से इनकार कर देती है। समाधान है हार्मोनाइज़ करना: वॉलपेपर से बने न्यूट्रल्स और इंटरैक्शन रंगों को बनाए रखना, लेकिन अपने ब्रांड ह्यू को चुपचाप ओवरराइट होने देने के बजाय स्कीम में वापस धकेलना।
dynamicColorScheme असल में क्या देता है
API 31+ पर, dynamicLightColorScheme(context) और dynamicDarkColorScheme(context), android.R.color.system_accent1 से system_accent3 तक पढ़ते हैं — ये टोनल पैलेट हैं जो सिस्टम ने वॉलपेपर से निकाले हैं — और आपको एक पूरा Material 3 ColorScheme लौटाते हैं। यह सुविधाजनक है क्योंकि आपको एक ऐसी स्कीम मिलती है जो बाकी OS के साथ सुसंगत दिखने की गारंटी देती है। साथ ही यह एक ऐसी स्कीम भी है जिसे बिल्कुल भी पता नहीं होता कि आपका ऐप किस रंग का होना चाहिए।
@Composable
fun AppTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val colorScheme = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (isSystemInDarkTheme()) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
else -> if (isSystemInDarkTheme()) DarkColorScheme else LightColorScheme
}
MaterialTheme(colorScheme = colorScheme, content = content)
}
ज़्यादातर ट्यूटोरियल यहीं रुक जाते हैं, और यही वह कोड है जिसकी वजह से Hydrame का वॉटर-ड्रॉप आइकन और उसका इन-ऐप एक्सेंट कलर, आधे टेस्ट किए गए फोन पर मेल खाना बंद कर गया था।
बदलने के बजाय हार्मोनाइज़ करना
androidx.core.graphics.ColorUtils — कोई एक्स्ट्रा डिपेंडेंसी नहीं चाहिए, यह core-ktx में पहले से ही आता है — के पास एक blendHSL है जिसका इस्तेमाल आप एक फिक्स्ड ब्रांड कलर को स्कीम के डायनामिक प्राइमरी की ओर, छोटे-छोटे स्टेप्स में तब तक शिफ्ट करने के लिए कर सकते हैं जब तक वह अपनी पहचान खोए बिना पर्याप्त नेटिव न महसूस हो। Material की अपनी डिज़ाइन गाइडेंस इसे “हार्मोनाइज़ेशन” कहती है: एक ऐसा रंग लेना जिसका मालिक सिस्टम नहीं है (एक एरर रेड, एक ब्रांड ह्यू, एक डेटा-विज़ुअलाइज़ेशन सीरीज़) और उसे टकराव से बचाने के लिए नज़दीकी डायनामिक रोल की ओर आंशिक रूप से ब्लेंड करना।
fun harmonize(designColor: Color, dynamicColor: Color, fraction: Float = 0.2f): Color {
val blended = ColorUtils.blendHSL(
designColor.toArgb(),
dynamicColor.toArgb(),
fraction,
)
return Color(blended)
}
Hydrame पर लागू किया गया, जिसका ब्रांड प्राइमरी रंग #3B82F6 है:
val scheme = dynamicColorScheme(context)
val harmonizedAccent = harmonize(
designColor = Color(0xFF3B82F6),
dynamicColor = scheme.primary,
fraction = 0.15f,
)
fraction = 0.15f पर एक्सेंट अब भी बिना शक Hydrame के नीले रंग जैसा ही दिखता है, लेकिन वह वॉलपेपर ने जो भी टोनल न्यूट्रल बनाया हो उससे लड़ने के बजाय आराम से उसके बगल में बैठ जाता है। मैं हार्मोनाइज़्ड रंग का इस्तेमाल सिर्फ उन हिस्सों के लिए करता हूं जो ब्रांड पहचान ढोते हैं — ऐप आइकन की इन-ऐप गूंज, वॉटर-ड्रॉप प्रोग्रेस रिंग, ऑनबोर्डिंग इलस्ट्रेशन — और बाकी सब कुछ (सरफेस, टेक्स्ट, डिवाइडर, बटन) को सिस्टम के अपने डायनामिक रोल्स पर ही छोड़ देता हूं। स्कीम के हर रंग को हार्मोनाइज़ करना डायनामिक कलर के पूरे मकसद को ही खत्म कर देता है; लक्ष्य यह है कि बाकी सिस्टम-नेटिव पैलेट के भीतर एक पहचाने जाने योग्य एक्सेंट बचा रहे, पूरी तरह से ब्रांड का टेकओवर नहीं।
API 31 से नीचे फॉलबैक
ऊपर दिए गए फॉलबैक लॉजिक का दो-तिहाई हिस्सा एक ही when ब्रांच में आ जाता है, लेकिन pre-Android-12 स्कीम कैसी दिखती है इस बारे में जानबूझकर सोचना ज़रूरी है, क्योंकि यह कोई डिग्रेडेड डायनामिक स्कीम नहीं है — उन डिवाइसेज़ पर यही आपकी इकलौती स्कीम है, बस इतना ही। मैं हर ऐप के लिए, यह अनुमान लगाने की कोशिश करने के बजाय कि डायनामिक स्कीम क्या बनाती, lightColorScheme(primary = ..., secondary = ..., ...) से हाथ से बनाया गया एक ColorScheme रखता हूं, जो हर ऐप के डिज़ाइन टोकन्स में पहले से मौजूद सटीक ब्रांड वैल्यूज़ (Granyn का #22C55E, Mintly का #F59E0B, Subly का #6366F1) से बना होता है। इन डिवाइसेज़ पर इस पोस्ट में बताई गई ब्रांड कलर की समस्या सिरे से मौजूद ही नहीं होती — पूरे पैलेट के मालिक आप हैं — इसलिए ऐसा डायनामिक व्यवहार सिमुलेट करने में मेहनत बर्बाद न करें जो आपको वैसे भी मिल नहीं सकता।
सिर्फ लाइट और डार्क नहीं, बल्कि अलग-अलग वॉलपेपर पर टेस्ट करना
डायनामिक कलर एक ऐसा टेस्टिंग डायमेंशन जोड़ता है जिसे छोड़ना आसान है: लाइट थीम, डार्क थीम, और अब वॉलपेपर ह्यूज़ की एक रेंज। डिफ़ॉल्ट Pixel वॉलपेपर पर सही दिखने वाली स्क्रीन, एक सैचुरेटेड वॉलपेपर पर अपठनीय कंट्रास्ट वाली हो सकती है, क्योंकि system_accent1 ह्यू और क्रोमा दोनों को साथ में शिफ्ट करता है। किसी थीम्ड स्क्रीन को शिप करने से पहले मैं उसे कम से कम एक कूल वॉलपेपर, एक वॉर्म वॉलपेपर, और एक लो-सैचुरेशन ग्रेस्केल वॉलपेपर के सामने चेक करता हूं — Settings → Wallpaper & style → pick color से, जो बिना कोई मैचिंग इमेज ढूंढे एक खास ह्यू को फोर्स करने देता है। अगर आपके हार्मोनाइज़्ड एक्सेंट का scheme.surface के मुकाबले कंट्रास्ट रेशियो इनमें से किसी पर भी 4.5:1 से नीचे गिरता है, तो ऊपर वाला फिक्स्ड fraction उस ह्यू रेंज के लिए बहुत ज़्यादा आक्रामक है और उसे एक फिक्स्ड कॉन्स्टेंट की बजाय एक कंट्रास्ट चेक चाहिए:
fun harmonizeWithContrast(designColor: Color, scheme: ColorScheme, fraction: Float = 0.15f): Color {
val candidate = harmonize(designColor, scheme.primary, fraction)
return if (ColorUtils.calculateContrast(candidate.toArgb(), scheme.surface.toArgb()) >= 4.5) {
candidate
} else {
designColor // कम कंट्रास्ट शिप करने के बजाय बिना छेड़ा हुआ ब्रांड कलर वापस इस्तेमाल करें
}
}
शिप करने से पहले क्या चेक करें
- ब्रांड कलर ढोने वाली हर स्क्रीन (आइकन एको, इलस्ट्रेशन, चार्ट्स) एक हार्मोनाइज़्ड वैल्यू का इस्तेमाल करती है, न कि रॉ डायनामिक प्राइमरी या बिना छेड़ा हुआ ब्रांड हेक्स।
- pre-API-31 फॉलबैक स्कीम एक असली, हाथ से ट्यून किया गया
ColorSchemeहै, न कि डायनामिक वाले की हार्डकोडेड कॉपी। - कंट्रास्ट को सिर्फ डिफ़ॉल्ट के मुकाबले नहीं, बल्कि कम से कम एक सैचुरेटेड और एक लो-क्रोमा वॉलपेपर के मुकाबले भी चेक किया गया है।
- नॉन-ब्रांड सरफेस (बैकग्राउंड, डिवाइडर, डिफ़ॉल्ट बटन) सिस्टम के डायनामिक रोल्स पर बिना छेड़े छोड़े गए हैं — सब कुछ हार्मोनाइज़ करने की चाहत का विरोध करें।
डायनामिक कलर, Android प्लेटफॉर्म की उन चंद फीचर्स में से एक है जो किसी ऐप को एक जेनेरिक फोन के बजाय एक खास फोन का हिस्सा महसूस कराती है। इसे बनाए रखना सही फैसला है। बस इसे बनाए रखने के लिए अपना ब्रांड कलर कुर्बान करना सही नहीं है।
// संबंधित पठन
जर्नल से और भी
2026 में Android SplashScreen API: बिना सफ़ेद फ़्लैश के कोल्ड स्टार्ट
Android के SplashScreen API की एक व्यावहारिक गाइड — थीम सेटअप, एनिमेटेड आइकन की साइज़ सीमाएँ जिन्हें कोई नहीं पढ़ता, और अभी तैयार न हुए डेटा के लिए keepOnScreenCondition।
Android का In-App Review API: बिना परेशान किए रेटिंग माँगना
Google के In-App Review API की एक व्यावहारिक गाइड — यह असल में कैसे काम करता है, इसे कब ट्रिगर करना चाहिए, और सामान्य 'हमें रेट करें' पॉपअप आपकी Play Store रेटिंग को चुपचाप कैसे नुकसान पहुँचा रहा है।
2026 में Android पर एक्सेसिबिलिटी: एक TalkBack और Compose semantics चेकलिस्ट जो वाकई काम करती है
2026 के लिए एक व्यावहारिक Android एक्सेसिबिलिटी चेकलिस्ट — TalkBack, Compose semantics, टच टारगेट, और रिलीज़ से पहले हर स्क्रीन पर जो टेस्टिंग पास मैं करता हूं।