उपयोगकर्ता के इंटरैक्शन को मैनेज करना

यूज़र इंटरफ़ेस के कॉम्पोनेंट, डिवाइस को इस्तेमाल करने वाले व्यक्ति को सुझाव देते हैं कि वे किस तरह से काम करते हैं उपयोगकर्ता इंटरैक्शन पर प्रतिक्रिया दे सकते हैं. हर कॉम्पोनेंट के लिए, रिस्पॉन्स देने का अपना तरीका होता है इंटरैक्शन होते हैं, जिनसे उपयोगकर्ता को यह जानने में मदद मिलती है कि उनके इंटरैक्शन क्या हो रहे हैं. इसके लिए उदाहरण के लिए, अगर कोई उपयोगकर्ता डिवाइस की टचस्क्रीन पर किसी बटन को छूता है, तो बटन हाइलाइट करने के लिए रंग जोड़कर दिखाया जा सकता है. यह बदलाव उपयोगकर्ता को पता चलता है कि उन्होंने बटन छू लिया है. अगर उपयोगकर्ता ऐसा नहीं करना चाहता उसे यह पता चल जाएगा कि बटन से पहले अपनी उंगली छोड़ रहे हैं--अन्यथा, बटन सक्रिय हो जाएगा.

पहली इमेज. ऐसे बटन जो हमेशा चालू दिखते हैं और जिन्हें दबाने में कोई रिपल नहीं होता.
दूसरी इमेज. दबाए गए रिपल वाले बटन, जो उसी हिसाब से अपनी चालू स्थिति को दिखाते हैं.

लिखने के हाथ के जेस्चर से जुड़ा दस्तावेज़ यह बताता है कि कंपोज़र कम-लेवल वाले पॉइंटर इवेंट को हैंडल करते हैं, जैसे कि पॉइंटर मूव और क्लिक. नए तरीके से, 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 की छत पर बनाया गया है फ़्लो के लिए सेट किया गया है, ताकि आप इससे इंटरैक्शन को उसी तरह इकट्ठा कर सकें किसी दूसरे फ़्लो के साथ काम किया जा सकता है. डिज़ाइन के इस फ़ैसले के बारे में ज़्यादा जानकारी के लिए, बेहतरीन इंटरैक्शन ब्लॉग पोस्ट देखें.

इंटरैक्शन स्थिति

यह भी हो सकता है कि आप अपने कॉम्पोनेंट की पहले से मौजूद सुविधाओं को भी बढ़ाना चाहें ट्रैक करना जारी रखें. उदाहरण के लिए, हो सकता है कि आप एक बटन की सहायता से दबाकर रखने पर रंग बदलें. इंटरैक्शन ट्रैक करने का सबसे आसान तरीका है कि सही इंटरैक्शन स्थिति देखें. 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 API पर बनाया गया है. कुछ मामलों में, सीधे उन निचले-स्तरीय फ़ंक्शन का उपयोग करना चाहते हैं.

उदाहरण के लिए, मान लें कि आपको यह जानने की ज़रूरत है कि कोई बटन दबाया जा रहा है या नहीं, और भी खींचें और छोड़ें या नहीं. अगर आप collectIsPressedAsState() दोनों का इस्तेमाल करते/करती हैं और collectIsDraggedAsState() के साथ-साथ, Compose बहुत सारे डुप्लीकेट काम करता है, और इस बात की कोई गारंटी नहीं है कि आपको सभी इंटरैक्शन सही क्रम में मिलेंगे. इसके लिए ऐसी स्थितियों में, हो सकता है कि आप सीधे तौर पर InteractionSource. इंटरैक्शन ट्रैक करने के बारे में ज़्यादा जानकारी के लिए आप खुद InteractionSource के साथ काम करते हैं, InteractionSource के साथ काम करें देखें.

इस सेक्शन में बताया गया है कि InteractionSource और MutableInteractionSource.

Interaction का इस्तेमाल करें और इसे चलाएं

InteractionSource, Interactions की रीड-ओनली स्ट्रीम को दिखाता है — यह InteractionSource में Interaction उत्सर्जन हो सकता है. उत्सर्जन करने के लिए Interaction, आपको MutableInteractionSource का इस्तेमाल करना होगा, जो InteractionSource.

मॉडिफ़ायर और कॉम्पोनेंट Interactions का इस्तेमाल कर सकते हैं, उत्सर्जन कर सकते हैं या इनका इस्तेमाल और उत्सर्जन कर सकते हैं. यहां दिए गए सेक्शन में बताया गया है कि दोनों प्लैटफ़ॉर्म पर होने वाले इंटरैक्शन का इस्तेमाल कैसे करें और कार्रवाई बदलने वाली कुंजी और कॉम्पोनेंट.

कार्रवाई बदलने वाली कुंजी इस्तेमाल करने का उदाहरण

फ़ोकस की गई स्थिति के लिए बॉर्डर बनाने वाले मॉडिफ़ायर के लिए, आपको सिर्फ़ Interactions, ताकि आप InteractionSource स्वीकार कर सकें:

fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier {
    // ...
}

फ़ंक्शन हस्ताक्षर से साफ़ तौर पर पता चलता है कि यह कार्रवाई बदलने वाला एक उपभोक्ता है — यह Interactions का इस्तेमाल कर सकता है, लेकिन उत्सर्जन नहीं कर सकता.

कार्रवाई बदलने वाली कुंजी का उदाहरण तैयार करना

Modifier.hoverable जैसे होवर इवेंट को मैनेज करने वाले मॉडिफ़ायर के लिए Interactions उत्सर्जन करना होगा और MutableInteractionSource को पैरामीटर का इस्तेमाल करें:

fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier {
    // ...
}

यह मॉडिफ़ायर प्रोड्यूसर है — यह दी गई जानकारी का इस्तेमाल कर सकता है इस पर कर्सर घुमाने पर, HoverInteractions उत्सर्जन करने के लिए MutableInteractionSource या कार्रवाई नहीं की गई.

ऐसे कॉम्पोनेंट बनाएं जो एक-दूसरे के काम आएंगे और:

मटीरियल 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 को पैरामीटर के तौर पर दिखाते हैं. इसके अलावा के सबसे सही तरीकों को फ़ॉलो किया है, तो इससे पढ़ने और समझने में आसानी होती है. किसी कॉम्पोनेंट की विज़ुअल स्थिति को उसी तरह कंट्रोल करें जैसे किसी दूसरे तरह के कॉम्पोनेंट स्थिति (जैसे कि चालू होने की स्थिति) को पढ़ा और कंट्रोल किया जा सकता है.

कंपोज़ कई लेयर वाली आर्किटेक्चरल अप्रोच का इस्तेमाल करके, कंपोज़ की सुविधा इस्तेमाल की जाती है. इसलिए, अच्छी क्वालिटी के मटीरियल कॉम्पोनेंट, बुनियादी इमारत के ऊपर बनाए जाते हैं ऐसे ब्लॉक जो रिपल और अन्य कंट्रोल के लिए Interactions जनरेट करते हैं विज़ुअल इफ़ेक्ट. फ़ाउंडेशन लाइब्रेरी से, हाई-लेवल इंटरैक्शन मॉडिफ़ायर मिलते हैं जैसे कि Modifier.hoverable, Modifier.focusable, और Modifier.draggable.

होवर इवेंट का जवाब देने वाला कॉम्पोनेंट बनाने के लिए, Modifier.hoverable और MutableInteractionSource को पैरामीटर के तौर पर पास करें. कॉम्पोनेंट पर कर्सर घुमाने पर, यह HoverInteraction उत्सर्जन करता है. इसका इस्तेमाल किया जा सकता है इसे बदला जा सकता है.

// 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 के लिए, स्टैंडर्ड flow API का इस्तेमाल करें. उदाहरण के लिए, मान लीजिए कि आप प्रेस और ड्रैग की एक सूची बनाए रखना चाहते हैं 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()

अगर आप यह जानना चाहते हैं कि हाल ही में क्या बातचीत हुई थी, तो आखिरी आइटम सूची में शामिल करें. उदाहरण के लिए, लिखें कि रिपल लागू करने का तरीका क्या है सबसे हाल के इंटरैक्शन के लिए इस्तेमाल किए जाने वाले सही ओवरले का पता लगाता है:

val lastInteraction = when (interactions.lastOrNull()) {
    is DragInteraction.Start -> "Dragged"
    is PressInteraction.Press -> "Pressed"
    else -> "No state"
}

सभी Interaction एक ही स्ट्रक्चर को फ़ॉलो करते हैं. इसलिए, आपके पास अलग-अलग तरह के उपयोगकर्ता इंटरैक्शन के साथ काम करते समय कोड में अंतर — पैटर्न समान है.

ध्यान दें कि इस सेक्शन में दिए गए उदाहरण,Flow State का इस्तेमाल करने वाले इंटरैक्शन — इससे अपडेट की गई वैल्यू को आसानी से देखा जा सकता है, क्योंकि स्टेट वैल्यू को पढ़ने से अपने-आप रीकंपोज़िशन बन जाएंगी. हालांकि, कंपोज़िशन प्री-फ़्रेम बैच किया गया है. इसका मतलब है कि अगर राज्य में बदलाव होता है और फिर उसी फ़्रेम में वापस बदल जाता है, तो स्थिति को देखने वाले कॉम्पोनेंट बदलाव देखें.

यह इंटरैक्शन के लिए ज़रूरी है, क्योंकि इंटरैक्शन नियमित रूप से शुरू और खत्म हो सकते हैं एक ही फ़्रेम में आएँ. उदाहरण के लिए, Button के साथ पिछले उदाहरण का इस्तेमाल करके:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(onClick = { /* do something */ }, interactionSource = interactionSource) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

अगर कोई मीडिया एक ही फ़्रेम में शुरू और खत्म होता है, तो टेक्स्ट कभी भी इस तरह नहीं दिखेगा "दबा हुआ!". ज़्यादातर मामलों में, यह कोई समस्या नहीं है — थोड़ा समय लेने पर, झिलमिलाहट: उपयोगकर्ता को आसानी होती है. कुछ मामलों में, जैसे कि रिपल इफ़ेक्ट या समान एनिमेशन, तो हो सकता है कि आप प्रभाव को कम से कम एक सीमा तक दिखाना चाहें यह सुविधा, समय के साथ बंद हो सकती है. यह बटन अब बटन नहीं दबाया जाता है. यहां की यात्रा पर हूं ऐसा करने पर, कलेक्शन में मौजूद ऐनिमेशन को सीधे तौर पर चालू और बंद किया जा सकता है Labda पर सेट करें. यहां इस पैटर्न का एक उदाहरण दिया गया है: ऐनिमेट किए गए बॉर्डर के साथ बेहतर 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 को मौजूदा मटीरियल के आधार पर बनाया गया है Button, यह उपयोगकर्ता के इंटरैक्शन पर सामान्य तौर पर प्रतिक्रिया करता है. जब उपयोगकर्ता बटन को दबाता है, लेकिन इसकी ओपैसिटी में थोड़ा-बहुत बदलाव होता है. सामग्री Button.

Indication का इस्तेमाल करके, फिर से इस्तेमाल किया जा सकने वाला कस्टम इफ़ेक्ट बनाएं और उसे लागू करें

पिछले सेक्शन में, आपने रिस्पॉन्स के तौर पर किसी कॉम्पोनेंट के किसी हिस्से को बदलने का तरीका बताया था अलग-अलग Interaction के लिए, जैसे कि दबाए जाने पर कोई आइकॉन दिखाना. यह एक जैसा अप्रोच का इस्तेमाल करके, आपके दिए गए पैरामीटर की वैल्यू को या कॉम्पोनेंट के अंदर दिखने वाले कॉन्टेंट में बदलाव कर सकता है, लेकिन ऐसा हो सकता है यह सिर्फ़ हर कॉम्पोनेंट के हिसाब से लागू होता है. अक्सर, किसी ऐप्लिकेशन या डिज़ाइन सिस्टम को में स्टेटफ़ुल विज़ुअल इफ़ेक्ट के लिए एक सामान्य सिस्टम होगा. सभी कॉम्पोनेंट पर एक तरह से लागू किया जाना चाहिए.

अगर आपको इस तरह का डिज़ाइन सिस्टम बनाना है, तो एक कॉम्पोनेंट को पसंद के मुताबिक बनाना और अन्य कॉम्पोनेंट के लिए इस कस्टमाइज़ेशन का दोबारा इस्तेमाल करना, पब्लिशर के लिए मुश्किल हो सकता है ये वजहें हो सकती हैं:

  • डिज़ाइन सिस्टम के हर कॉम्पोनेंट में एक ही बॉयलरप्लेट की ज़रूरत होती है
  • इस इफ़ेक्ट को, नए कॉम्पोनेंट और कस्टम कॉन्फ़िगरेशन पर लागू करना बहुत आसान है क्लिक किए जा सकने वाले कॉम्पोनेंट
  • कस्टम इफ़ेक्ट को दूसरे इफ़ेक्ट के साथ इस्तेमाल करना मुश्किल हो सकता है

इन समस्याओं से बचने और अपने पूरे सिस्टम में कस्टम कॉम्पोनेंट को आसानी से स्केल करने के लिए, तो Indication का इस्तेमाल किया जा सकता है. Indication फिर से इस्तेमाल किया जा सकने वाला विज़ुअल इफ़ेक्ट दिखाता है, जिसे सभी पर लागू किया जा सकता है कॉम्पोनेंट, ऐप्लिकेशन या डिज़ाइन सिस्टम में मौजूद होते हैं. Indication को दो हिस्सों में बांटा गया है वाहन के पुर्ज़े:

  • IndicationNodeFactory: ऐसी फ़ैक्ट्री जो Modifier.Node इंस्टेंस बनाती है किसी कॉम्पोनेंट के लिए विज़ुअल इफ़ेक्ट रेंडर करने में मदद मिलती है. आसानी से लागू करने के लिए, जो अलग-अलग कॉम्पोनेंट में बदलाव करता है, तो यह सिंगलटन (ऑब्जेक्ट) हो सकता है और पूरे कॉम्पोनेंट में फिर से इस्तेमाल किया जा सकता है को ट्रैक करने की सुविधा मिलती है.

    ये इंस्टेंस, स्टेटफ़ुल या स्टेटलेस हो सकते हैं. चूंकि वे प्रति घंटे बनाए जाते हैं कॉम्पोनेंट, वे CompositionLocal से वैल्यू वापस पा सकते हैं. इससे यह तय होगा कि वे किसी खास कॉम्पोनेंट के अंदर दिखते या काम करते हैं, जैसा कि किसी दूसरे कॉम्पोनेंट में होता है Modifier.Node.

  • Modifier.indication: ऐसा मॉडिफ़ायर जोIndication कॉम्पोनेंट. Modifier.clickable और अन्य हाई लेवल इंटरैक्शन मॉडिफ़ायर संकेत पैरामीटर को सीधे स्वीकार करते हैं, ताकि वे न सिर्फ़ Interactions. हालांकि, ये Interaction के लिए विज़ुअल इफ़ेक्ट भी बना सकते हैं उत्सर्जन इसलिए, आसान मामलों में, Modifier.clickable का इस्तेमाल बिना Modifier.indication की ज़रूरत है.

इफ़ेक्ट को 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 में बदलने के लिए, फ़ॉलो करें यह तरीका अपनाएं:

  1. स्केल इफ़ेक्ट लागू करने के लिए ज़िम्मेदार 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()
            }
        }
    }

  2. 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
    }

  3. Modifier.clickable, अंदरूनी तौर पर Modifier.indication का इस्तेमाल करता है. इसलिए, इसे ScaleIndication वाला क्लिक किया जा सकने वाला कॉम्पोनेंट चुनने के लिए, आपको बस यह जानकारी देनी होगी clickable के पैरामीटर के तौर पर Indication:

    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 की शुरुआत में कई बार तेज़ी से दबाने के लिए — अगर एक बार दबाया जाए मौजूदा प्रेस या आराम के समय के ऐनिमेशन के दौरान होता है, तो पिछला ऐनिमेशन रद्द कर दिया गया है, और प्रेस ऐनिमेशन फिर से शुरू हो रहा है. एक साथ कई डिवाइसों पर काम करने के लिए समवर्ती प्रभाव (जैसे कि रिपल के साथ, जिसमें नया रिपल ऐनिमेशन के अलावा, अन्य तरंगों से ऊपर भी टाइप करते हैं, तो आप किसी भी समय एक सूची में एनिमेशन मौजूदा ऐनिमेशन को रद्द करने और नए ऐनिमेशन शुरू करने के लिए किया जा सकता है.