यूज़र इंटरफ़ेस के कॉम्पोनेंट, डिवाइस के उपयोगकर्ता को सुझाव देते हैं. इसके लिए, वे उपयोगकर्ता के इंटरैक्शन के हिसाब से जवाब देते हैं. हर कॉम्पोनेंट, इंटरैक्शन का जवाब अपने तरीके से देता है. इससे उपयोगकर्ता को यह जानने में मदद मिलती है कि उसके इंटरैक्शन से क्या हो रहा है. उदाहरण के लिए, अगर कोई उपयोगकर्ता किसी डिवाइस की टचस्क्रीन पर मौजूद बटन को छूता है, तो हो सकता है कि बटन में कुछ बदलाव हो. जैसे, बटन का रंग हाइलाइट हो जाए. इस बदलाव से उपयोगकर्ता को पता चलता है कि उसने बटन को छुआ है. अगर उपयोगकर्ता को ऐसा नहीं करना है, तो वह बटन को छोड़ने से पहले अपनी उंगली को बटन से दूर खींच लेगा. ऐसा न करने पर, बटन चालू हो जाएगा.
Compose के जेस्चर दस्तावेज़ में बताया गया है कि Compose के कॉम्पोनेंट, लो-लेवल पॉइंटर इवेंट को कैसे मैनेज करते हैं. जैसे, पॉइंटर को मूव करना और क्लिक करना. Compose, लो-लेवल इवेंट को हाई-लेवल इंटरैक्शन में बदल देता है. उदाहरण के लिए, पॉइंटर इवेंट की सीरीज़ को बटन दबाने और छोड़ने के तौर पर जोड़ा जा सकता है. इन हाई-लेवल ऐब्स्ट्रैक्शन को समझने से, आपको यह तय करने में मदद मिल सकती है कि आपका यूज़र इंटरफ़ेस (यूआई), उपयोगकर्ता को कैसे जवाब दे. उदाहरण के लिए, हो सकता है कि आपको यह तय करना हो कि जब कोई उपयोगकर्ता किसी कॉम्पोनेंट के साथ इंटरैक्ट करे, तो वह कॉम्पोनेंट कैसा दिखे. इसके अलावा, हो सकता है कि आपको उपयोगकर्ता की उन कार्रवाइयों का लॉग सेव करना हो. इस दस्तावेज़ में, स्टैंडर्ड यूज़र इंटरफ़ेस (यूआई) एलिमेंट में बदलाव करने या अपना खुद का यूज़र इंटरफ़ेस (यूआई) डिज़ाइन करने के लिए ज़रूरी जानकारी दी गई है.
इंटरैक्शन
कई मामलों में, आपको यह जानने की ज़रूरत नहीं होती कि आपका Compose कॉम्पोनेंट, उपयोगकर्ता के इंटरैक्शन को कैसे समझ रहा है. उदाहरण के लिए, Button
Modifier.clickable
पर निर्भर करता है, ताकि यह पता लगाया जा सके कि उपयोगकर्ता ने बटन पर क्लिक किया है या नहीं. अगर आपको अपने ऐप्लिकेशन में कोई सामान्य बटन जोड़ना है, तो बटन का onClick कोड तय किया जा सकता है. इसके बाद, ज़रूरत पड़ने पर Modifier.clickable उस कोड को चलाता है. इसका मतलब है कि आपको यह जानने की ज़रूरत नहीं है कि उपयोगकर्ता ने स्क्रीन पर टैप किया है या कीबोर्ड से बटन चुना है. Modifier.clickable को पता चल जाता है कि उपयोगकर्ता ने क्लिक किया है और वह आपके onClick कोड को चलाकर जवाब देता है.
हालांकि, अगर आपको उपयोगकर्ता के व्यवहार के हिसाब से यूज़र इंटरफ़ेस (यूआई) कॉम्पोनेंट के रिस्पॉन्स को पसंद के मुताबिक बनाना है, तो आपको इसके बारे में ज़्यादा जानकारी होनी चाहिए. इस सेक्शन में आपको इस बारे में कुछ जानकारी मिलेगी.
जब कोई उपयोगकर्ता किसी यूज़र इंटरफ़ेस (यूआई) कॉम्पोनेंट के साथ इंटरैक्ट करता है, तो सिस्टम उसके व्यवहार को दिखाने के लिए कई Interaction इवेंट जनरेट करता है. उदाहरण के लिए, अगर कोई उपयोगकर्ता किसी बटन को छूता है, तो वह बटन
PressInteraction.Press जनरेट करता है.
अगर उपयोगकर्ता बटन के अंदर अपनी उंगली उठाता है, तो यह एक PressInteraction.Release जनरेट करता है. इससे बटन को पता चलता है कि क्लिक पूरा हो गया है. वहीं, अगर उपयोगकर्ता अपनी उंगली को बटन से बाहर खींचता है और फिर उंगली हटाता है, तो बटन PressInteraction.Cancel जनरेट करता है. इससे पता चलता है कि बटन को दबाने की कार्रवाई रद्द कर दी गई थी, पूरी नहीं हुई थी.
ये इंटरैक्शन निष्पक्ष होते हैं. इसका मतलब है कि इन लो-लेवल इंटरैक्शन इवेंट का मकसद, उपयोगकर्ता की कार्रवाइयों या उनके क्रम का मतलब निकालना नहीं है. साथ ही, वे यह भी नहीं समझ पाते कि उपयोगकर्ता की कौनसी कार्रवाइयां अन्य कार्रवाइयों से ज़्यादा अहम हो सकती हैं.
आम तौर पर, ये इंटरैक्शन जोड़े में होते हैं. इनमें एक शुरुआत और एक अंत होता है. दूसरे इंटरैक्शन में, पहले इंटरैक्शन का रेफ़रंस शामिल होता है. उदाहरण के लिए, अगर कोई उपयोगकर्ता किसी बटन को छूता है और फिर अपनी उंगली हटाता है, तो छूने से PressInteraction.Press इंटरैक्शन जनरेट होता है और उंगली हटाने से PressInteraction.Release जनरेट होता है. Release में press प्रॉपर्टी होती है, जो शुरुआती PressInteraction.Press की पहचान करती है.
किसी कॉम्पोनेंट के लिए इंटरैक्शन देखने के लिए, उसके InteractionSource को देखें. InteractionSource को Kotlin फ़्लो के ऊपर बनाया गया है. इसलिए, इससे इंटरैक्शन उसी तरह इकट्ठा किए जा सकते हैं जिस तरह किसी दूसरे फ़्लो के साथ काम किया जाता है. डिज़ाइन से जुड़े इस फ़ैसले के बारे में ज़्यादा जानने के लिए, Illuminating Interactions ब्लॉग पोस्ट देखें.
इंटरैक्शन की स्थिति
अपने कॉम्पोनेंट की पहले से मौजूद सुविधाओं को बढ़ाने के लिए, इंटरैक्शन को खुद भी ट्रैक किया जा सकता है. उदाहरण के लिए, हो सकता है कि आपको किसी बटन को दबाने पर उसका रंग बदलना हो. इंटरैक्शन को ट्रैक करने का सबसे आसान तरीका है, सही इंटरैक्शन स्टेट को देखना. InteractionSource में कई ऐसे तरीके उपलब्ध हैं जिनसे इंटरैक्शन की अलग-अलग स्थितियों को स्टेट के तौर पर दिखाया जा सकता है. उदाहरण के लिए, अगर आपको यह देखना है कि कोई बटन दबाया गया है या नहीं, तो उसके InteractionSource.collectIsPressedAsState() तरीके को कॉल किया जा सकता है:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
collectIsPressedAsState() के अलावा, Compose में collectIsFocusedAsState(), collectIsDraggedAsState(), और collectIsHoveredAsState() भी उपलब्ध हैं. ये तरीके, असल में निचले लेवल के InteractionSource एपीआई पर बनाए गए आसान तरीके हैं. कुछ मामलों में, हो सकता है कि आप उन लोवर-लेवल फ़ंक्शन को सीधे तौर पर इस्तेमाल करना चाहें.
उदाहरण के लिए, मान लें कि आपको यह जानना है कि कोई बटन दबाया जा रहा है या नहीं. साथ ही, यह भी जानना है कि उसे खींचा जा रहा है या नहीं. collectIsPressedAsState() और collectIsDraggedAsState(), दोनों का इस्तेमाल करने पर, Compose को कई डुप्लीकेट काम करने पड़ते हैं. साथ ही, इस बात की कोई गारंटी नहीं होती कि आपको सभी इंटरैक्शन सही क्रम में मिलेंगे. ऐसी स्थितियों में, हो सकता है कि आप सीधे InteractionSource के साथ काम करना चाहें. InteractionSource की मदद से इंटरैक्शन को खुद ट्रैक करने के बारे में ज़्यादा जानने के लिए, InteractionSource के साथ काम करना लेख पढ़ें.
यहां दिए गए सेक्शन में, InteractionSource और MutableInteractionSource के साथ इंटरैक्शन को इस्तेमाल करने और उन्हें एमिट करने का तरीका बताया गया है.
Interaction का इस्तेमाल करना और उसे उत्सर्जित करना
InteractionSource, Interactions की रीड-ओनली स्ट्रीम को दिखाता है — InteractionSource में Interaction को एमिट नहीं किया जा सकता. Interaction को एमिट करने के लिए, आपको MutableInteractionSource का इस्तेमाल करना होगा. यह InteractionSource से एक्सटेंड होता है.
मॉडिफ़ायर और कॉम्पोनेंट, Interactions को इस्तेमाल कर सकते हैं, एमिट कर सकते हैं या इस्तेमाल और एमिट दोनों कर सकते हैं.
यहां दिए गए सेक्शन में, मॉडिफ़ायर और कॉम्पोनेंट, दोनों से इंटरैक्शन को इस्तेमाल करने और उन्हें ट्रिगर करने का तरीका बताया गया है.
Consuming मॉडिफ़ायर का उदाहरण
फ़ोकस की स्थिति के लिए बॉर्डर बनाने वाले मॉडिफ़ायर के लिए, आपको सिर्फ़ Interactions को मॉनिटर करना होगा, ताकि आप InteractionSource को स्वीकार कर सकें:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
फ़ंक्शन सिग्नेचर से यह साफ़ तौर पर पता चलता है कि यह मॉडिफ़ायर एक कंज्यूमर है — यह Interactions का इस्तेमाल कर सकता है, लेकिन उन्हें एमिट नहीं कर सकता.
प्रॉडक्ट बनाने वाले मॉडिफ़ायर का उदाहरण
Modifier.hoverable जैसे कर्सर घुमाने से जुड़े इवेंट को मैनेज करने वाले मॉडिफ़ायर के लिए, आपको Interactions को एमिट करना होगा. साथ ही, पैरामीटर के तौर पर MutableInteractionSource को स्वीकार करना होगा:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
यह मॉडिफ़ायर एक प्रोड्यूसर है — यह दिए गए MutableInteractionSource का इस्तेमाल करके, कर्सर को होवर करने या हटाने पर HoverInteractions को एमिट कर सकता है.
ऐसे कॉम्पोनेंट बनाना जो डेटा का इस्तेमाल करते हैं और डेटा जनरेट करते हैं
मटीरियल Button जैसे हाई-लेवल कॉम्पोनेंट, प्रोड्यूसर और कंज़्यूमर, दोनों के तौर पर काम करते हैं. ये इनपुट और फ़ोकस इवेंट को हैंडल करते हैं. साथ ही, इन इवेंट के जवाब में अपना लुक बदलते हैं. जैसे, रिपल इफ़ेक्ट दिखाना या एलिवेशन को ऐनिमेट करना. इस वजह से, वे MutableInteractionSource को सीधे तौर पर पैरामीटर के तौर पर दिखाते हैं, ताकि आप याद रखे गए इंस्टेंस को उपलब्ध करा सकें:
@Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, // exposes MutableInteractionSource as a parameter interactionSource: MutableInteractionSource? = null, elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { /* content() */ }
इससे MutableInteractionSource को कॉम्पोनेंट से बाहर होस्ट किया जा सकता है. साथ ही, कॉम्पोनेंट से जनरेट किए गए सभी Interaction को देखा जा सकता है. इसका इस्तेमाल करके, उस कॉम्पोनेंट या अपने यूज़र इंटरफ़ेस (यूआई) में मौजूद किसी अन्य कॉम्पोनेंट के दिखने के तरीके को कंट्रोल किया जा सकता है.
अगर आपको खुद के इंटरैक्टिव हाई लेवल कॉम्पोनेंट बनाने हैं, तो हमारा सुझाव है कि MutableInteractionSource को इस तरह से पैरामीटर के तौर पर एक्सपोज़ करें. स्टेट होइस्टिंग के सबसे सही तरीकों को अपनाने के अलावा, इससे किसी कॉम्पोनेंट की विज़ुअल स्टेट को पढ़ना और कंट्रोल करना आसान हो जाता है. ठीक उसी तरह जैसे किसी अन्य तरह की स्टेट (जैसे, चालू की गई स्टेट) को पढ़ा और कंट्रोल किया जा सकता है.
Compose, लेयर्ड आर्किटेक्चरल अप्रोच का इस्तेमाल करता है. इसलिए, हाई-लेवल मटीरियल कॉम्पोनेंट, बुनियादी बिल्डिंग ब्लॉक के ऊपर बनाए जाते हैं. ये ब्लॉक, रिपल और अन्य विज़ुअल इफ़ेक्ट को कंट्रोल करने के लिए ज़रूरी Interaction बनाते हैं. फ़ाउंडेशन लाइब्रेरी, हाई-लेवल इंटरैक्शन मॉडिफ़ायर उपलब्ध कराती है. जैसे, Modifier.hoverable, Modifier.focusable, और Modifier.draggable.
होवर इवेंट के लिए रिस्पॉन्स देने वाला कॉम्पोनेंट बनाने के लिए, Modifier.hoverable का इस्तेमाल करें और पैरामीटर के तौर पर MutableInteractionSource को पास करें.
जब भी कॉम्पोनेंट पर कर्सर घुमाया जाता है, तो यह HoverInteractions को एमिट करता है. इसका इस्तेमाल करके, कॉम्पोनेंट के दिखने का तरीका बदला जा सकता है.
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
इस कॉम्पोनेंट को फ़ोकस करने लायक बनाने के लिए, Modifier.focusable को जोड़ा जा सकता है और पैरामीटर के तौर पर वही MutableInteractionSource पास किया जा सकता है. अब HoverInteraction.Enter/Exit और FocusInteraction.Focus/Unfocus, दोनों को एक ही MutableInteractionSource से दिखाया जाता है. साथ ही, दोनों तरह के इंटरैक्शन के दिखने के तरीके को एक ही जगह पर पसंद के मुताबिक बनाया जा सकता है:
// This InteractionSource will emit hover and focus interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource) .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Modifier.clickable, hoverable और focusable से भी ज़्यादा बेहतर लेवल का ऐब्स्ट्रैक्शन है. किसी कॉम्पोनेंट पर क्लिक किया जा सके, इसके लिए ज़रूरी है कि उस पर कर्सर घुमाया जा सके. साथ ही, जिन कॉम्पोनेंट पर क्लिक किया जा सकता है उन पर फ़ोकस भी किया जा सके. Modifier.clickable का इस्तेमाल करके, ऐसा कॉम्पोनेंट बनाया जा सकता है जो कर्सर घुमाने, फ़ोकस करने, और दबाने से जुड़े इंटरैक्शन को मैनेज करता है. इसके लिए, आपको लोअर लेवल के एपीआई को जोड़ने की ज़रूरत नहीं पड़ती. अगर आपको अपने कॉम्पोनेंट को क्लिक करने लायक भी बनाना है, तो hoverable और focusable को clickable से बदला जा सकता है:
// This InteractionSource will emit hover, focus, and press interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .clickable( onClick = {}, interactionSource = interactionSource, // Also show a ripple effect indication = ripple() ), contentAlignment = Alignment.Center ) { Text("Hello!") }
InteractionSource के साथ काम करना
अगर आपको किसी कॉम्पोनेंट के साथ होने वाले इंटरैक्शन के बारे में कम लेवल की जानकारी चाहिए, तो उस कॉम्पोनेंट के InteractionSource के लिए स्टैंडर्ड फ़्लो एपीआई का इस्तेमाल किया जा सकता है.
उदाहरण के लिए, मान लें कि आपको किसी InteractionSource के लिए, दबाने और खींचकर छोड़ने वाले इंटरैक्शन की सूची बनाए रखनी है. यह कोड आधा काम करता है,
जैसे ही नई प्रेस आती हैं, उन्हें सूची में जोड़ता है:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is DragInteraction.Start -> { interactions.add(interaction) } } } }
हालांकि, नए इंटरैक्शन जोड़ने के अलावा, आपको इंटरैक्शन खत्म होने पर उन्हें हटाना भी होगा. उदाहरण के लिए, जब उपयोगकर्ता कॉम्पोनेंट से अपनी उंगली हटाता है. ऐसा करना आसान है, क्योंकि एंड इंटरैक्शन में हमेशा उससे जुड़े स्टार्ट इंटरैक्शन का रेफ़रंस होता है. इस कोड से पता चलता है कि खत्म हो चुके इंटरैक्शन को कैसे हटाया जा सकता है:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } is DragInteraction.Start -> { interactions.add(interaction) } is DragInteraction.Stop -> { interactions.remove(interaction.start) } is DragInteraction.Cancel -> { interactions.remove(interaction.start) } } } }
अब अगर आपको यह जानना है कि कॉम्पोनेंट को फ़िलहाल दबाया जा रहा है या खींचा जा रहा है, तो आपको बस यह देखना होगा कि interactions खाली है या नहीं:
val isPressedOrDragged = interactions.isNotEmpty()
अगर आपको जानना है कि सबसे हाल ही का इंटरैक्शन कौनसा था, तो सूची में मौजूद आखिरी आइटम को देखें. उदाहरण के लिए, Compose रिपल को लागू करने का तरीका, हाल ही में की गई इंटरैक्शन के लिए इस्तेमाल किए जाने वाले सही स्टेट ओवरले का पता लगाता है:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
सभी Interaction एक ही स्ट्रक्चर का इस्तेमाल करते हैं. इसलिए, अलग-अलग तरह के उपयोगकर्ता इंटरैक्शन के साथ काम करते समय, कोड में ज़्यादा अंतर नहीं होता. कुल मिलाकर, पैटर्न एक जैसा ही होता है.
ध्यान दें कि इस सेक्शन में दिए गए पिछले उदाहरण, State का इस्तेमाल करके इंटरैक्शन के Flow को दिखाते हैं. इससे अपडेट की गई वैल्यू को देखना आसान हो जाता है, क्योंकि स्टेट वैल्यू को पढ़ने से, अपने-आप फिर से कॉम्पोज़िशन होने लगता है. हालांकि, कंपोज़िशन को हर फ़्रेम के हिसाब से बैच में प्रोसेस किया जाता है. इसका मतलब है कि अगर स्थिति बदलती है और फिर उसी फ़्रेम में वापस बदल जाती है, तो स्थिति को मॉनिटर करने वाले कॉम्पोनेंट को बदलाव नहीं दिखेगा.
यह इंटरैक्शन के लिए ज़रूरी है, क्योंकि इंटरैक्शन एक ही फ़्रेम में नियमित तौर पर शुरू और खत्म हो सकते हैं. उदाहरण के लिए, Button के साथ पिछले उदाहरण का इस्तेमाल करना:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
अगर बटन को दबाने और छोड़ने की प्रोसेस एक ही फ़्रेम में पूरी हो जाती है, तो टेक्स्ट कभी भी "Pressed!" के तौर पर नहीं दिखेगा. ज़्यादातर मामलों में, यह कोई समस्या नहीं है. इतने कम समय के लिए विज़ुअल इफ़ेक्ट दिखाने से, स्क्रीन पर फ़्लिकरिंग होगी और उपयोगकर्ता को इस पर ज़्यादा ध्यान नहीं जाएगा. कुछ मामलों में, जैसे कि रिपल इफ़ेक्ट या मिलती-जुलती ऐनिमेशन दिखाने के लिए, हो सकता है कि आप इफ़ेक्ट को कम से कम समय के लिए दिखाना चाहें. इसके बजाय, बटन को दबाने पर तुरंत बंद कर दें. ऐसा करने के लिए, स्टेट में लिखने के बजाय, सीधे कलेक्ट लैम्डा से ऐनिमेशन शुरू और बंद किए जा सकते हैं. इस पैटर्न का एक उदाहरण, ऐनिमेटेड बॉर्डर के साथ ऐडवांस Indication बनाएं सेक्शन में दिया गया है.
उदाहरण: कस्टम इंटरैक्शन हैंडलिंग की मदद से कॉम्पोनेंट बनाना
इनपुट के लिए कस्टम रिस्पॉन्स के साथ कॉम्पोनेंट बनाने का तरीका जानने के लिए, यहां बदलाव किए गए बटन का उदाहरण दिया गया है. मान लें कि आपको एक ऐसा बटन चाहिए जो दबाए जाने पर अपना रंग बदल ले:
इसके लिए, Button के आधार पर एक कस्टम कंपोज़ेबल बनाएं. साथ ही, आइकॉन (इस मामले में, शॉपिंग कार्ट) बनाने के लिए, इसमें एक और icon पैरामीटर जोड़ें. collectIsPressedAsState() को कॉल करके यह ट्रैक किया जाता है कि उपयोगकर्ता, बटन पर कर्सर घुमा रहा है या नहीं. ऐसा होने पर, आइकॉन जोड़ दिया जाता है. कोड ऐसा दिखता है:
@Composable fun PressIconButton( onClick: () -> Unit, icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource? = null ) { val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false Button( onClick = onClick, modifier = modifier, interactionSource = interactionSource ) { AnimatedVisibility(visible = isPressed) { if (isPressed) { Row { icon() Spacer(Modifier.size(ButtonDefaults.IconSpacing)) } } } text() } }
यहां बताया गया है कि उस नए कंपोज़ेबल का इस्तेमाल कैसे किया जाता है:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
यह नया PressIconButton, मौजूदा Material Button पर बनाया गया है. इसलिए, यह उपयोगकर्ता के इंटरैक्शन पर सामान्य तरीके से प्रतिक्रिया देता है. जब उपयोगकर्ता बटन दबाता है, तो उसकी ओपैसिटी थोड़ी बदल जाती है. ठीक वैसे ही जैसे सामान्य मटीरियल Button की बदलती है.
Indication की मदद से, फिर से इस्तेमाल किया जा सकने वाला कस्टम इफ़ेक्ट बनाना और उसे लागू करना
पिछले सेक्शन में, आपने अलग-अलग Interaction के जवाब में कॉम्पोनेंट के किसी हिस्से को बदलने का तरीका सीखा. जैसे, दबाए जाने पर आइकॉन दिखाना. इसी तरीके का इस्तेमाल करके, किसी कॉम्पोनेंट को दिए गए पैरामीटर की वैल्यू बदली जा सकती है या कॉम्पोनेंट में दिखने वाले कॉन्टेंट को बदला जा सकता है. हालांकि, यह सिर्फ़ हर कॉम्पोनेंट के हिसाब से लागू होता है. अक्सर, किसी ऐप्लिकेशन या डिज़ाइन सिस्टम में स्टेटफ़ुल विज़ुअल इफ़ेक्ट के लिए एक सामान्य सिस्टम होता है. यह एक ऐसा इफ़ेक्ट होता है जिसे सभी कॉम्पोनेंट पर एक जैसा लागू किया जाना चाहिए.
अगर आपको इस तरह का डिज़ाइन सिस्टम बनाना है, तो एक कॉम्पोनेंट को कस्टमाइज़ करना और इस कस्टमाइज़ेशन को अन्य कॉम्पोनेंट के लिए फिर से इस्तेमाल करना मुश्किल हो सकता है. इसकी वजहें यहां दी गई हैं:
- डिज़ाइन सिस्टम के हर कॉम्पोनेंट के लिए, एक ही बॉयलरप्लेट की ज़रूरत होती है
- नए कॉम्पोनेंट और क्लिक किए जा सकने वाले कस्टम कॉम्पोनेंट पर, इस इफ़ेक्ट को लागू करना भूल जाना आम बात है
- कस्टम इफ़ेक्ट को अन्य इफ़ेक्ट के साथ जोड़ना मुश्किल हो सकता है
इन समस्याओं से बचने और अपने सिस्टम में कस्टम कॉम्पोनेंट को आसानी से स्केल करने के लिए, Indication का इस्तेमाल किया जा सकता है.
Indication, फिर से इस्तेमाल किए जा सकने वाले विज़ुअल इफ़ेक्ट को दिखाता है. इसे किसी ऐप्लिकेशन या डिज़ाइन सिस्टम के कॉम्पोनेंट पर लागू किया जा सकता है. Indication को दो हिस्सों में बांटा गया है:
IndicationNodeFactory: यह एक फ़ैक्ट्री है, जोModifier.Nodeइंस्टेंस बनाती है. ये इंस्टेंस, किसी कॉम्पोनेंट के लिए विज़ुअल इफ़ेक्ट रेंडर करते हैं. ऐसे आसान इंप्लिमेंटेशन के लिए जो कॉम्पोनेंट के हिसाब से नहीं बदलते, यह एक सिंगलटन (ऑब्जेक्ट) हो सकता है. साथ ही, इसे पूरे ऐप्लिकेशन में फिर से इस्तेमाल किया जा सकता है.ये इंस्टेंस स्टेटफ़ुल या स्टेटलेस हो सकते हैं. इन्हें हर कॉम्पोनेंट के हिसाब से बनाया जाता है. इसलिए, ये
CompositionLocalसे वैल्यू हासिल कर सकते हैं, ताकि किसी खास कॉम्पोनेंट में इनके दिखने या काम करने के तरीके को बदला जा सके. ऐसा किसी भी अन्यModifier.Nodeकी तरह किया जा सकता है.Modifier.indication: एक मॉडिफ़ायर, जो किसी कॉम्पोनेंट के लिएIndicationड्रॉ करता है.Modifier.clickableऔर इंटरैक्शन के लिए इस्तेमाल होने वाले अन्य हाई लेवल मॉडिफ़ायर, सीधे तौर पर इंडिकेशन पैरामीटर को स्वीकार करते हैं. इसलिए, वे न सिर्फ़Interactionको एमिट करते हैं, बल्कि एमिट किए गएInteractionके लिए विज़ुअल इफ़ेक्ट भी ड्रॉ कर सकते हैं. इसलिए, आसान मामलों मेंModifier.indicationकी ज़रूरत के बिना, सिर्फ़Modifier.clickableका इस्तेमाल किया जा सकता है.
इफ़ेक्ट को Indication से बदलें
इस सेक्शन में बताया गया है कि किसी बटन पर मैन्युअल तरीके से लागू किए गए स्केल इफ़ेक्ट को, इंडिकेशन के बराबर की वैल्यू से कैसे बदला जा सकता है. इस वैल्यू को कई कॉम्पोनेंट में फिर से इस्तेमाल किया जा सकता है.
नीचे दिया गया कोड एक ऐसा बटन बनाता है जो दबाने पर छोटा हो जाता है:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") Button( modifier = Modifier.scale(scale), onClick = { }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
ऊपर दिए गए स्निपेट में मौजूद स्केल इफ़ेक्ट को Indication में बदलने के लिए, यह तरीका अपनाएं:
Modifier.Nodeबनाएं, जो स्केल इफ़ेक्ट लागू करने के लिए ज़िम्मेदार है. अटैच होने पर, नोड इंटरैक्शन सोर्स को मॉनिटर करता है. यह पिछले उदाहरणों की तरह ही है. यहां सिर्फ़ यह अंतर है कि यह आने वाले इंटरैक्शन को स्टेट में बदलने के बजाय, सीधे ऐनिमेशन लॉन्च करता है.नोड को
DrawModifierNodeको लागू करना होगा, ताकि वहContentDrawScope#draw()को बदल सके. साथ ही, Compose में किसी अन्य ग्राफ़िक्स एपीआई की तरह ही, ड्रॉइंग कमांड का इस्तेमाल करके स्केल इफ़ेक्ट को रेंडर कर सके.ContentDrawScopeरिसीवर से उपलब्धdrawContent()को कॉल करने पर, वह असल कॉम्पोनेंट दिखेगा जिस परIndicationको लागू किया जाना चाहिए. इसलिए, आपको इस फ़ंक्शन को सिर्फ़ स्केल ट्रांसफ़ॉर्मेशन में कॉल करना होगा. पक्का करें कि आपकेIndicationलागू करने पर, किसी समयdrawContent()को हमेशा कॉल किया जाए. ऐसा न करने पर, वह कॉम्पोनेंट नहीं दिखेगा जिस परIndicationलागू किया जा रहा है.private class ScaleNode(private val interactionSource: InteractionSource) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedScalePercent = Animatable(1f) private suspend fun animateToPressed(pressPosition: Offset) { currentPressPosition = pressPosition animatedScalePercent.animateTo(0.9f, spring()) } private suspend fun animateToResting() { animatedScalePercent.animateTo(1f, spring()) } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collectLatest { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { scale( scale = animatedScalePercent.value, pivot = currentPressPosition ) { this@draw.drawContent() } } }
IndicationNodeFactoryबनाएं. इसकी ज़िम्मेदारी सिर्फ़ यह है कि यह दिए गए इंटरैक्शन सोर्स के लिए, एक नया नोड इंस्टेंस बनाए. इंडिकेशन को कॉन्फ़िगर करने के लिए कोई पैरामीटर नहीं है. इसलिए, फ़ैक्ट्री एक ऑब्जेक्ट हो सकती है:object ScaleIndication : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ScaleNode(interactionSource) } override fun equals(other: Any?): Boolean = other === ScaleIndication override fun hashCode() = 100 }
Modifier.clickable, अंदरूनी तौर परModifier.indicationका इस्तेमाल करता है. इसलिए,ScaleIndicationकी मदद से क्लिक किया जा सकने वाला कॉम्पोनेंट बनाने के लिए, आपको बसIndicationकोclickableके पैरामीटर के तौर पर देना होगा:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
इससे कस्टम
Indicationका इस्तेमाल करके, हाई लेवल और फिर से इस्तेमाल किए जा सकने वाले कॉम्पोनेंट बनाना भी आसान हो जाता है. उदाहरण के लिए, बटन कुछ ऐसा दिख सकता है:@Composable fun ScaleButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, shape: Shape = CircleShape, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) .clickable( enabled = enabled, indication = ScaleIndication, interactionSource = interactionSource, onClick = onClick ) .border(width = 2.dp, color = Color.Blue, shape = shape) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) }
इसके बाद, बटन का इस्तेमाल इस तरह किया जा सकता है:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
Indication से बनाया गया बटन.ऐनिमेटेड बॉर्डर वाला ऐडवांस Indication बनाना
Indication का इस्तेमाल, सिर्फ़ ट्रांसफ़ॉर्मेशन इफ़ेक्ट के लिए नहीं किया जाता. जैसे, किसी कॉम्पोनेंट को स्केल करना. IndicationNodeFactory, Modifier.Node दिखाता है. इसलिए, अन्य ड्रॉइंग एपीआई की तरह ही, कॉन्टेंट के ऊपर या नीचे किसी भी तरह का इफ़ेक्ट ड्रॉ किया जा सकता है. उदाहरण के लिए, कॉम्पोनेंट के चारों ओर ऐनिमेटेड बॉर्डर बनाया जा सकता है. साथ ही, कॉम्पोनेंट को दबाने पर उसके ऊपर एक ओवरले बनाया जा सकता है:
Indication से बनाया गया ऐनिमेटेड बॉर्डर इफ़ेक्ट.यहां Indication को लागू करने का तरीका, पिछले उदाहरण से काफ़ी मिलता-जुलता है —
यह कुछ पैरामीटर के साथ एक नोड बनाता है. ऐनिमेटेड बॉर्डर, उस कॉम्पोनेंट के आकार और बॉर्डर पर निर्भर करता है जिसके लिए Indication का इस्तेमाल किया जाता है. इसलिए, Indication को लागू करने के लिए, आकार और बॉर्डर की चौड़ाई को पैरामीटर के तौर पर देना ज़रूरी है:
data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return NeonNode( shape, // Double the border size for a stronger press effect borderWidth * 2, interactionSource ) } }
Modifier.Node को लागू करने का तरीका भी सिद्धांत के तौर पर एक जैसा है. भले ही, ड्रॉइंग कोड ज़्यादा जटिल हो. पहले की तरह, यह InteractionSource को मॉनिटर करता है.
अटैच होने पर, ऐनिमेशन लॉन्च करता है और कॉन्टेंट के ऊपर इफ़ेक्ट दिखाने के लिए, DrawModifierNode को लागू करता है:
private class NeonNode( private val shape: Shape, private val borderWidth: Dp, private val interactionSource: InteractionSource ) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedProgress = Animatable(0f) val animatedPressAlpha = Animatable(1f) var pressedAnimation: Job? = null var restingAnimation: Job? = null private suspend fun animateToPressed(pressPosition: Offset) { // Finish any existing animations, in case of a new press while we are still showing // an animation for a previous one restingAnimation?.cancel() pressedAnimation?.cancel() pressedAnimation = coroutineScope.launch { currentPressPosition = pressPosition animatedPressAlpha.snapTo(1f) animatedProgress.snapTo(0f) animatedProgress.animateTo(1f, tween(450)) } } private fun animateToResting() { restingAnimation = coroutineScope.launch { // Wait for the existing press animation to finish if it is still ongoing pressedAnimation?.join() animatedPressAlpha.animateTo(0f, tween(250)) animatedProgress.snapTo(0f) } } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( currentPressPosition, size ) val brush = animateBrush( startPosition = startPosition, endPosition = endPosition, progress = animatedProgress.value ) val alpha = animatedPressAlpha.value drawContent() val outline = shape.createOutline(size, layoutDirection, this) // Draw overlay on top of content drawOutline( outline = outline, brush = brush, alpha = alpha * 0.1f ) // Draw border on top of overlay drawOutline( outline = outline, brush = brush, alpha = alpha, style = Stroke(width = borderWidth.toPx()) ) } /** * Calculates a gradient start / end where start is the point on the bounding rectangle of * size [size] that intercepts with the line drawn from the center to [pressPosition], * and end is the intercept on the opposite end of that line. */ private fun calculateGradientStartAndEndFromPressPosition( pressPosition: Offset, size: Size ): Pair<Offset, Offset> { // Convert to offset from the center val offset = pressPosition - size.center // y = mx + c, c is 0, so just test for x and y to see where the intercept is val gradient = offset.y / offset.x // We are starting from the center, so halve the width and height - convert the sign // to match the offset val width = (size.width / 2f) * sign(offset.x) val height = (size.height / 2f) * sign(offset.y) val x = height / gradient val y = gradient * width // Figure out which intercept lies within bounds val intercept = if (abs(y) <= abs(height)) { Offset(width, y) } else { Offset(x, height) } // Convert back to offsets from 0,0 val start = intercept + size.center val end = Offset(size.width - start.x, size.height - start.y) return start to end } private fun animateBrush( startPosition: Offset, endPosition: Offset, progress: Float ): Brush { if (progress == 0f) return TransparentBrush // This is *expensive* - we are doing a lot of allocations on each animation frame. To // recreate a similar effect in a performant way, it would be better to create one large // gradient and translate it on each frame, instead of creating a whole new gradient // and shader. The current approach will be janky! val colorStops = buildList { when { progress < 1 / 6f -> { val adjustedProgress = progress * 6f add(0f to Blue) add(adjustedProgress to Color.Transparent) } progress < 2 / 6f -> { val adjustedProgress = (progress - 1 / 6f) * 6f add(0f to Purple) add(adjustedProgress * MaxBlueStop to Blue) add(adjustedProgress to Blue) add(1f to Color.Transparent) } progress < 3 / 6f -> { val adjustedProgress = (progress - 2 / 6f) * 6f add(0f to Pink) add(adjustedProgress * MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 4 / 6f -> { val adjustedProgress = (progress - 3 / 6f) * 6f add(0f to Orange) add(adjustedProgress * MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 5 / 6f -> { val adjustedProgress = (progress - 4 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } else -> { val adjustedProgress = (progress - 5 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxYellowStop to Yellow) add(MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } } } return linearGradient( colorStops = colorStops.toTypedArray(), start = startPosition, end = endPosition ) } companion object { val TransparentBrush = SolidColor(Color.Transparent) val Blue = Color(0xFF30C0D8) val Purple = Color(0xFF7848A8) val Pink = Color(0xFFF03078) val Orange = Color(0xFFF07800) val Yellow = Color(0xFFF0D800) const val MaxYellowStop = 0.16f const val MaxOrangeStop = 0.33f const val MaxPinkStop = 0.5f const val MaxPurpleStop = 0.67f const val MaxBlueStop = 0.83f } }
यहां मुख्य अंतर यह है कि अब animateToResting() फ़ंक्शन के साथ ऐनिमेशन के लिए कम से कम अवधि तय की गई है. इसलिए, बटन को तुरंत रिलीज़ करने पर भी, बटन दबाने का ऐनिमेशन जारी रहेगा. animateToPressed की शुरुआत में, कई बार तेज़ी से दबाने की सुविधा भी मौजूद है. अगर किसी मौजूदा प्रेस या रेस्टिंग ऐनिमेशन के दौरान कोई प्रेस होता है, तो पिछला ऐनिमेशन रद्द हो जाता है और प्रेस ऐनिमेशन शुरू से शुरू होता है. एक साथ कई इफ़ेक्ट (जैसे, रिपल इफ़ेक्ट, जिसमें एक नया रिपल ऐनिमेशन अन्य रिपल के ऊपर दिखेगा) दिखाने के लिए, ऐनिमेशन को एक सूची में ट्रैक किया जा सकता है. इसके लिए, मौजूदा ऐनिमेशन को रद्द करने और नए ऐनिमेशन शुरू करने की ज़रूरत नहीं है.
आपके लिए सुझाव
- ध्यान दें: JavaScript बंद होने पर, लिंक टेक्स्ट दिखता है
- जेस्चर के बारे में जानकारी
- Jetpack Compose के लिए Kotlin
- मटीरियल कॉम्पोनेंट और लेआउट