नेस्ट की गई स्क्रोलिंग

नेस्टेड स्क्रोलिंग एक ऐसा सिस्टम है जिसमें एक-दूसरे के अंदर मौजूद कई स्क्रोलिंग कॉम्पोनेंट, एक साथ काम करते हैं. ये कॉम्पोनेंट, स्क्रोल करने के एक ही जेस्चर पर प्रतिक्रिया देते हैं और स्क्रोलिंग के डेल्टा (बदलाव) को कम्यूनिकेट करते हैं.

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

Compose, कंपोज़ेबल के बीच नेस्ट की गई स्क्रोलिंग को मैनेज करने के कई तरीके उपलब्ध कराता है. नेस्टेड स्क्रोलिंग का एक सामान्य उदाहरण, किसी सूची के अंदर दूसरी सूची है. वहीं, इसका एक ज़्यादा जटिल उदाहरण कोलैप्स होने वाला टूलबार है.

नेस्ट किए गए स्क्रोलिंग की सुविधा अपने-आप चालू होना

सिंपल नेस्टेड स्क्रोलिंग के लिए, आपको कुछ नहीं करना है. स्क्रोल करने की सुविधा शुरू करने वाले जेस्चर, बच्चों से माता-पिता को अपने-आप मिल जाते हैं. ऐसा इसलिए होता है, ताकि जब बच्चा आगे स्क्रोल न कर पाए, तो जेस्चर को उसके पैरंट एलिमेंट से मैनेज किया जा सके.

नेस्टेड स्क्रोलिंग की सुविधा, Compose के कुछ कॉम्पोनेंट और मॉडिफ़ायर में पहले से मौजूद होती है. जैसे: verticalScroll, horizontalScroll, scrollable, Lazy एपीआई और TextField. इसका मतलब है कि जब उपयोगकर्ता नेस्ट किए गए कॉम्पोनेंट के किसी इनर चाइल्ड को स्क्रोल करता है, तो पिछले मॉडिफ़ायर, स्क्रोलिंग डेल्टा को उन पैरंट कॉम्पोनेंट तक पहुंचाते हैं जिनमें नेस्ट की गई स्क्रोलिंग की सुविधा होती है.

इस उदाहरण में, ऐसे एलिमेंट दिखाए गए हैं जिन पर verticalScroll मॉडिफ़ायर लागू किया गया है. ये एलिमेंट, ऐसे कंटेनर में मौजूद हैं जिस पर verticalScroll मॉडिफ़ायर लागू किया गया है.

@Composable
private fun AutomaticNestedScroll() {
    val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
    Box(
        modifier = Modifier
            .background(Color.LightGray)
            .verticalScroll(rememberScrollState())
            .padding(32.dp)
    ) {
        Column {
            repeat(6) {
                Box(
                    modifier = Modifier
                        .height(128.dp)
                        .verticalScroll(rememberScrollState())
                ) {
                    Text(
                        "Scroll here",
                        modifier = Modifier
                            .border(12.dp, Color.DarkGray)
                            .background(brush = gradient)
                            .padding(24.dp)
                            .height(150.dp)
                    )
                }
            }
        }
    }
}

दो नेस्ट किए गए वर्टिकल स्क्रोलिंग यूज़र इंटरफ़ेस (यूआई) एलिमेंट. इनमें से एक एलिमेंट के अंदर और दूसरा एलिमेंट के बाहर जेस्चर का इस्तेमाल किया जा रहा है
पहली इमेज. दो नेस्ट किए गए वर्टिकल स्क्रोलिंग यूज़र इंटरफ़ेस (यूआई) एलिमेंट, जो इनर एलिमेंट के अंदर और बाहर के जेस्चर का जवाब देते हैं.

nestedScroll मॉडिफ़ायर का इस्तेमाल करना

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

नेस्ट की गई स्क्रोलिंग साइकल

नेस्टेड स्क्रोल साइकल, स्क्रोल डेल्टा का फ़्लो होता है. इसे नेस्टेड स्क्रोलिंग सिस्टम का हिस्सा बनने वाले सभी कॉम्पोनेंट (या नोड) के ज़रिए, हैरारकी ट्री में ऊपर और नीचे भेजा जाता है. उदाहरण के लिए, स्क्रोल किए जा सकने वाले कॉम्पोनेंट और मॉडिफ़ायर या nestedScroll का इस्तेमाल करके.

नेस्ट की गई स्क्रोलिंग के साइकल के फ़ेज़

जब स्क्रोल किए जा सकने वाले कॉम्पोनेंट को ट्रिगर इवेंट (उदाहरण के लिए, कोई जेस्चर) का पता चलता है, तो स्क्रोल करने की कार्रवाई शुरू होने से पहले ही, जनरेट किए गए डेल्टा को नेस्ट किए गए स्क्रोल सिस्टम को भेज दिया जाता है. इसके बाद, ये तीन चरणों से गुज़रते हैं: प्री-स्क्रोल, नोड कंजम्पशन, और पोस्ट-स्क्रोल.

नेस्ट की गई स्क्रोलिंग के साइकल के फ़ेज़
दूसरी इमेज. नेस्ट की गई स्क्रोलिंग के साइकल के फ़ेज़.

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

प्री-स्क्रोल फ़ेज़ - डिसपैचिंग
अप
तीसरी इमेज. प्री-स्क्रोल फ़ेज़: dispatching up.

इससे नेस्ट किए गए स्क्रोल के पैरंट (nestedScroll या स्क्रोल किए जा सकने वाले मॉडिफ़ायर का इस्तेमाल करने वाले कंपोज़ेबल) को डेल्टा के साथ कुछ करने का मौका मिलता है. ऐसा तब होता है, जब नोड खुद इसका इस्तेमाल कर सकता है.

प्री-स्क्रोल फ़ेज़ - बब्लिंग
डाउन
चौथी इमेज. प्री-स्क्रोल फ़ेज़ - बब्लिंग डाउन.

नोड के इस्तेमाल के चरण में, नोड खुद उस डेल्टा का इस्तेमाल करेगा जिसे उसके पैरंट ने इस्तेमाल नहीं किया था. इस दौरान, स्क्रोलिंग की प्रोसेस पूरी हो जाती है और वह दिखने लगती है.

नोड के इस्तेमाल की
फ़ेज़
पांचवीं इमेज. नोड कंज़म्पशन फ़ेज़.

इस दौरान, बच्चा बची हुई स्क्रोल का पूरा या कुछ हिस्सा इस्तेमाल कर सकता है. बची हुई सभी चीज़ें वापस भेज दी जाएंगी, ताकि वे स्क्रोल करने के बाद दिखने वाले चरण से गुज़र सकें.

आखिर में, स्क्रोल के बाद वाले फ़ेज़ में, नोड ने जो भी डेटा इस्तेमाल नहीं किया है उसे उसके पूर्वजों को भेज दिया जाएगा, ताकि वे उसका इस्तेमाल कर सकें.

स्क्रोल करने के बाद का फ़ेज़ - डिसपैचिंग
अप
छठी इमेज. Post-scroll phase - dispatching up.

स्क्रोल करने के बाद दिखने वाला विज्ञापन, स्क्रोल करने से पहले दिखने वाले विज्ञापन की तरह ही काम करता है. इसमें, माता-पिता में से कोई भी व्यक्ति विज्ञापन देख सकता है या नहीं भी देख सकता है.

पोस्ट-स्क्रोल फ़ेज़ - बब्लिंग
डाउन
सातवीं इमेज. पोस्ट-स्क्रोल फ़ेज़ - बब्लिंग डाउन.

स्क्रोल करने की तरह ही, खींचकर छोड़ने का जेस्चर खत्म होने पर, उपयोगकर्ता के इरादे को रफ़्तार में बदला जा सकता है. इस रफ़्तार का इस्तेमाल, स्क्रोल किए जा सकने वाले कंटेनर को फ़्लिंग (ऐनिमेशन का इस्तेमाल करके स्क्रोल करना) करने के लिए किया जाता है. फ़्लिंग, नेस्ट किए गए स्क्रोल साइकल का भी हिस्सा है. साथ ही, खींचने वाले इवेंट से जनरेट हुई वेलोसिटी, एक जैसी फ़ेज़ से गुज़रती है: प्री-फ़्लिंग, नोड कंजम्पशन, और पोस्ट-फ़्लिंग. ध्यान दें कि फ़्लिंग ऐनिमेशन सिर्फ़ टच जेस्चर से जुड़ा होता है. यह a11y या हार्डवेयर स्क्रोल जैसे अन्य इवेंट से ट्रिगर नहीं होगा.

नेस्टेड स्क्रोलिंग साइकल में हिस्सा लेना

साइकल में हिस्सा लेने का मतलब है कि हैरारकी के हिसाब से डेल्टा को इंटरसेप्ट करना, इस्तेमाल करना, और उसके इस्तेमाल की रिपोर्ट करना. Compose, नेस्ट किए गए स्क्रोलिंग सिस्टम के काम करने के तरीके और उससे सीधे तौर पर इंटरैक्ट करने के तरीके को बेहतर बनाने के लिए टूल का एक सेट उपलब्ध कराता है. उदाहरण के लिए, जब आपको स्क्रोल किए जा सकने वाले कॉम्पोनेंट के स्क्रोल करना शुरू करने से पहले, स्क्रोल डेल्टा के साथ कुछ करना हो.

अगर नेस्ट किया गया स्क्रोल साइकल, नोड की चेन पर काम करने वाला सिस्टम है, तो nestedScroll मॉडिफ़ायर, इन बदलावों को रोकने और उनमें बदलाव करने का एक तरीका है. साथ ही, यह चेन में भेजे गए डेटा (स्क्रोल डेल्टा) को भी प्रभावित करता है. इस मॉडिफ़ायर को क्रम में कहीं भी रखा जा सकता है. यह ट्री में मौजूद नेस्ट किए गए स्क्रोल मॉडिफ़ायर इंस्टेंस के साथ कम्यूनिकेट करता है, ताकि इस चैनल के ज़रिए जानकारी शेयर की जा सके. इस मॉडिफ़ायर के बिल्डिंग ब्लॉक NestedScrollConnection और NestedScrollDispatcher हैं.

NestedScrollConnection नेस्ट किए गए स्क्रोल साइकल के फ़ेज़ के हिसाब से जवाब देने का तरीका उपलब्ध कराता है. साथ ही, नेस्ट किए गए स्क्रोल सिस्टम को प्रभावित करता है. इसमें चार कॉलबैक तरीके होते हैं. हर तरीका, खपत के एक चरण को दिखाता है: स्क्रोल करने से पहले/बाद और फ़्लिंग करने से पहले/बाद:

val nestedScrollConnection = object : NestedScrollConnection {
    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
        println("Received onPreScroll callback.")
        return Offset.Zero
    }

    override fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset {
        println("Received onPostScroll callback.")
        return Offset.Zero
    }
}

हर कॉलबैक में, डेल्टा के बारे में भी जानकारी मिलती है. डेल्टा का मतलब है कि किसी खास फ़ेज़ में कितना डेटा प्रोसेस किया गया और पिछले फ़ेज़ में कितना डेटा प्रोसेस किया गया: available उस फ़ेज़ के लिए डेल्टा और consumed पिछले फ़ेज़ में इस्तेमाल किया गया डेल्टा. अगर आपको किसी भी समय, डेल्टा को हाइरार्की में ऊपर की ओर ले जाने से रोकना है, तो इसके लिए नेस्ट किए गए स्क्रोल कनेक्शन का इस्तेमाल करें:

val disabledNestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPostScroll(
            consumed: Offset,
            available: Offset,
            source: NestedScrollSource
        ): Offset {
            return if (source == NestedScrollSource.SideEffect) {
                available
            } else {
                Offset.Zero
            }
        }
    }
}

सभी कॉलबैक, NestedScrollSource टाइप के बारे में जानकारी देते हैं.

NestedScrollDispatcher नेस्ट किए गए स्क्रोल साइकल को शुरू करता है. डिस्पैचर का इस्तेमाल करने और उसके तरीकों को कॉल करने से, साइकल ट्रिगर हो जाती है. स्क्रोल किए जा सकने वाले कंटेनर में एक बिल्ट-इन डिस्पैचर होता है. यह डिस्पैचर, सिस्टम में जेस्चर के दौरान कैप्चर किए गए डेल्टा भेजता है. इस वजह से, नेस्टेड स्क्रोलिंग को पसंद के मुताबिक बनाने के ज़्यादातर इस्तेमाल के मामलों में, नए डेल्टा भेजने के बजाय, पहले से मौजूद डेल्टा पर प्रतिक्रिया देने के लिए, डिस्पैचर के बजाय NestedScrollConnection का इस्तेमाल किया जाता है. इन्हें इस्तेमाल करने के अन्य तरीकों के बारे में जानने के लिए, NestedScrollDispatcherSample पर जाएं.

स्क्रोल करने पर इमेज का साइज़ बदलना

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

स्क्रोल की पोज़िशन के आधार पर किसी इमेज का साइज़ बदलना

इस स्निपेट में, वर्टिकल स्क्रोल की पोज़िशन के आधार पर, LazyColumn में मौजूद इमेज का साइज़ बदलने का तरीका दिखाया गया है. उपयोगकर्ता के नीचे की ओर स्क्रोल करने पर इमेज छोटी हो जाती है और ऊपर की ओर स्क्रोल करने पर बड़ी हो जाती है. हालांकि, इमेज का साइज़ तय की गई कम से कम और ज़्यादा से ज़्यादा सीमा के अंदर ही रहता है:

@Composable
fun ImageResizeOnScrollExample(
    modifier: Modifier = Modifier,
    maxImageSize: Dp = 300.dp,
    minImageSize: Dp = 100.dp
) {
    var currentImageSize by remember { mutableStateOf(maxImageSize) }
    var imageScale by remember { mutableFloatStateOf(1f) }

    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Calculate the change in image size based on scroll delta
                val delta = available.y
                val newImageSize = currentImageSize + delta.dp
                val previousImageSize = currentImageSize

                // Constrain the image size within the allowed bounds
                currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize)
                val consumed = currentImageSize - previousImageSize

                // Calculate the scale for the image
                imageScale = currentImageSize / maxImageSize

                // Return the consumed scroll amount
                return Offset(0f, consumed.value)
            }
        }
    }

    Box(Modifier.nestedScroll(nestedScrollConnection)) {
        LazyColumn(
            Modifier
                .fillMaxWidth()
                .padding(15.dp)
                .offset {
                    IntOffset(0, currentImageSize.roundToPx())
                }
        ) {
            // Placeholder list items
            items(100, key = { it }) {
                Text(
                    text = "Item: $it",
                    style = MaterialTheme.typography.bodyLarge
                )
            }
        }

        Image(
            painter = ColorPainter(Color.Red),
            contentDescription = "Red color image",
            Modifier
                .size(maxImageSize)
                .align(Alignment.TopCenter)
                .graphicsLayer {
                    scaleX = imageScale
                    scaleY = imageScale
                    // Center the image vertically as it scales
                    translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f
                }
        )
    }
}

कोड के बारे में मुख्य बातें

  • यह कोड, स्क्रोल इवेंट को इंटरसेप्ट करने के लिए NestedScrollConnection का इस्तेमाल करता है.
  • onPreScroll, स्क्रोल डेल्टा के आधार पर इमेज के साइज़ में होने वाले बदलाव का हिसाब लगाता है.
  • currentImageSize स्टेट वैरिएबल, इमेज के मौजूदा साइज़ को सेव करता है. यह minImageSize और maxImageSize. imageScale के बीच सीमित होता है. यह currentImageSize से मिलता है.
  • LazyColumn, currentImageSize के आधार पर ऑफ़सेट करता है.
  • Image, कैलकुलेट किए गए स्केल को लागू करने के लिए graphicsLayer मॉडिफ़ायर का इस्तेमाल करता है.
  • graphicsLayer में मौजूद translationY यह पक्का करता है कि इमेज का साइज़ बदलते समय, वह लंबवत रूप से बीच में रहे.

नतीजा

ऊपर दिए गए स्निपेट से, स्क्रोल करने पर इमेज के साइज़ में बदलाव होता है:

आठवीं इमेज. स्क्रोल करने पर इमेज के साइज़ में बदलाव होने का इफ़ेक्ट.

नेस्टेड स्क्रोलिंग इंटरऑप

स्क्रोल किए जा सकने वाले कंपोज़ेबल में स्क्रोल किए जा सकने वाले View एलिमेंट नेस्ट करने या इसके उलट करने पर, आपको समस्याएं आ सकती हैं. सबसे ज़्यादा ध्यान देने वाली समस्याएं तब होती हैं, जब बच्चे को स्क्रोल करके उसके शुरू या खत्म होने की सीमा तक पहुंचा जाता है और माता-पिता से स्क्रोल करने की उम्मीद की जाती है. हालांकि, ऐसा हो सकता है कि यह सुविधा काम न करे या उम्मीद के मुताबिक काम न करे.

यह समस्या, स्क्रोल किए जा सकने वाले कंपोज़ेबल में तय की गई उम्मीदों की वजह से होती है. स्क्रोल किए जा सकने वाले कंपोज़ेबल में "nested-scroll-by-default" नियम होता है. इसका मतलब है कि स्क्रोल किए जा सकने वाले किसी भी कंटेनर को नेस्ट किए गए स्क्रोल चेन में शामिल होना चाहिए. ऐसा पैरंट के तौर पर NestedScrollConnection और चाइल्ड के तौर पर NestedScrollDispatcher के ज़रिए किया जा सकता है. इसके बाद, जब चाइल्ड कॉम्पोनेंट बाउंड्री पर होता है, तब वह पैरंट कॉम्पोनेंट के लिए नेस्टेड स्क्रोल को ट्रिगर करता है. उदाहरण के लिए, इस नियम से Compose Pager और Compose LazyRow को एक साथ बेहतर तरीके से काम करने में मदद मिलती है. हालांकि, जब ViewPager2 या RecyclerView के साथ इंटरऑपरेबिलिटी स्क्रोलिंग की जा रही हो, तब चाइल्ड से पैरंट तक लगातार स्क्रोलिंग नहीं की जा सकती. ऐसा इसलिए है, क्योंकि ये NestedScrollingParent3 को लागू नहीं करते हैं.

स्क्रोल किए जा सकने वाले View एलिमेंट और स्क्रोल किए जा सकने वाले कंपोज़ेबल के बीच, नेस्टेड स्क्रोलिंग इंटरऑप एपीआई को चालू किया जा सकता है. ये दोनों एलिमेंट, दोनों दिशाओं में नेस्ट किए गए होते हैं. इन समस्याओं को कम करने के लिए, नेस्टेड स्क्रोलिंग इंटरऑप एपीआई का इस्तेमाल किया जा सकता है. ऐसा इन स्थितियों में किया जा सकता है.

माता-पिता View और बच्चा ComposeView

सहयोग करने वाला पैरंट View ऐसा पैरंट होता है जो पहले से ही NestedScrollingParent3 लागू करता है. इसलिए, वह सहयोग करने वाले नेस्ट किए गए चाइल्ड कंपोज़ेबल से स्क्रोलिंग डेल्टा पा सकता है. इस मामले में, ComposeView को बच्चे के तौर पर माना जाएगा और उसे NestedScrollingChild3 को (अप्रत्यक्ष रूप से) लागू करना होगा. androidx.coordinatorlayout.widget.CoordinatorLayout, एक साथ काम करने वाले पैरंट का एक उदाहरण है.

अगर आपको स्क्रोल किए जा सकने वाले View पैरंट कंटेनर और स्क्रोल किए जा सकने वाले नेस्ट किए गए चाइल्ड कंपोज़ेबल के बीच नेस्टेड स्क्रोलिंग इंटरऑपरेबिलिटी की ज़रूरत है, तो rememberNestedScrollInteropConnection() का इस्तेमाल किया जा सकता है.

rememberNestedScrollInteropConnection() कुकी, NestedScrollConnection को अनुमति देती है और इसे सेव करती है. NestedScrollConnection, View पैरंट और Compose चाइल्ड के बीच नेस्ट किए गए स्क्रोल की इंटरऑपरेबिलिटी को चालू करता है. View पैरंट, NestedScrollingParent3 को लागू करता है. इसका इस्तेमाल nestedScroll मॉडिफ़ायर के साथ किया जाना चाहिए. Compose में नेस्टेड स्क्रोलिंग की सुविधा डिफ़ॉल्ट रूप से चालू होती है. इसलिए, इस कनेक्शन का इस्तेमाल करके View में नेस्टेड स्क्रोलिंग की सुविधा चालू की जा सकती है. साथ ही, View और कंपोज़ेबल के बीच ज़रूरी ग्लू लॉजिक जोड़ा जा सकता है.Views

CoordinatorLayout, CollapsingToolbarLayout, और चाइल्ड कंपोज़ेबल का इस्तेमाल करने का एक सामान्य उदाहरण यहां दिया गया है:

<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:fitsSystemWindows="true">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <!--...-->

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

आपको अपनी गतिविधि या फ़्रैगमेंट में, बच्चे के लिए कंपोज़ेबल और ज़रूरी NestedScrollConnection सेट अप करना होगा:

open class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                // Add the nested scroll connection to your top level @Composable element
                // using the nestedScroll modifier.
                LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) {
                    items(20) { item ->
                        Box(
                            modifier = Modifier
                                .padding(16.dp)
                                .height(56.dp)
                                .fillMaxWidth()
                                .background(Color.Gray),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(item.toString())
                        }
                    }
                }
            }
        }
    }
}

चाइल्ड AndroidView को शामिल करने वाला पैरंट कंपोज़ेबल

इस उदाहरण में, Compose साइड पर नेस्ट की गई स्क्रोलिंग इंटरऑप एपीआई को लागू करने के बारे में बताया गया है. ऐसा तब होता है, जब आपके पास पैरंट कंपोज़ेबल होता है, जिसमें चाइल्ड AndroidView होता है. AndroidView, NestedScrollDispatcher को लागू करता है, क्योंकि यह Compose के स्क्रोल करने वाले पैरंट के लिए चाइल्ड के तौर पर काम करता है. साथ ही, यह NestedScrollingParent3 को भी लागू करता है, क्योंकि यह View के स्क्रोल करने वाले चाइल्ड के लिए पैरंट के तौर पर काम करता है. इसके बाद, पैरंट कंपोज़िशन को नेस्ट किए गए स्क्रोल करने लायक चाइल्ड View से, नेस्ट किए गए स्क्रोल के डेल्टा मिल पाएंगे.

यहां दिए गए उदाहरण में, इस स्थिति में नेस्टेड स्क्रोलिंग इंटरऑप को लागू करने का तरीका बताया गया है. साथ ही, इसमें Compose के कोलैप्स होने वाले टूलबार का इस्तेमाल किया गया है:

@Composable
private fun NestedScrollInteropComposeParentWithAndroidChildExample() {
    val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() }
    val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }

    // Sets up the nested scroll connection between the Box composable parent
    // and the child AndroidView containing the RecyclerView
    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Updates the toolbar offset based on the scroll to enable
                // collapsible behaviour
                val delta = available.y
                val newOffset = toolbarOffsetHeightPx.value + delta
                toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
                return Offset.Zero
            }
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .nestedScroll(nestedScrollConnection)
    ) {
        TopAppBar(
            modifier = Modifier
                .height(ToolbarHeight)
                .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }
        )

        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply {
                        with(findViewById<RecyclerView>(R.id.main_list)) {
                            layoutManager = LinearLayoutManager(context, VERTICAL, false)
                            adapter = NestedScrollInteropAdapter()
                        }
                    }.also {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(it, true)
                    }
            },
            // ...
        )
    }
}

private class NestedScrollInteropAdapter :
    Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() {
    val items = (1..10).map { it.toString() }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): NestedScrollInteropViewHolder {
        return NestedScrollInteropViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item, parent, false)
        )
    }

    override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) {
        // ...
    }

    class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) {
        fun bind(item: String) {
            // ...
        }
    }
    // ...
}

इस उदाहरण में, scrollable मॉडिफ़ायर के साथ एपीआई का इस्तेमाल करने का तरीका दिखाया गया है:

@Composable
fun ViewInComposeNestedScrollInteropExample() {
    Box(
        Modifier
            .fillMaxSize()
            .scrollable(rememberScrollableState {
                // View component deltas should be reflected in Compose
                // components that participate in nested scrolling
                it
            }, Orientation.Vertical)
    ) {
        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(android.R.layout.list_item, null)
                    .apply {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(this, true)
                    }
            }
        )
    }
}

आखिर में, इस उदाहरण में दिखाया गया है कि नेस्टेड स्क्रोलिंग इंटरऑप एपीआई का इस्तेमाल, BottomSheetDialogFragment के साथ कैसे किया जाता है, ताकि खींचकर छोड़ने की सुविधा काम कर सके:

class BottomSheetFragment : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false)

        rootView.findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                LazyColumn(
                    Modifier
                        .nestedScroll(nestedScrollInterop)
                        .fillMaxSize()
                ) {
                    item {
                        Text(text = "Bottom sheet title")
                    }
                    items(10) {
                        Text(
                            text = "List item number $it",
                            modifier = Modifier.fillMaxWidth()
                        )
                    }
                }
            }
            return rootView
        }
    }
}

ध्यान दें कि rememberNestedScrollInteropConnection() को जिस एलिमेंट से अटैच किया जाता है उसमें NestedScrollConnection इंस्टॉल हो जाएगा. NestedScrollConnection, कंपोज़ लेवल से View लेवल तक डेल्टा ट्रांसमिट करने के लिए ज़िम्मेदार है. इससे एलिमेंट को नेस्टेड स्क्रोलिंग में हिस्सा लेने की अनुमति मिलती है. हालांकि, इससे एलिमेंट अपने-आप स्क्रोल नहीं होते हैं. अपने-आप स्क्रोल न होने वाले कंपोज़ेबल, जैसे कि Box या Column के लिए, ऐसे कॉम्पोनेंट पर स्क्रोल डेल्टा, नेस्ट किए गए स्क्रोल सिस्टम में नहीं फैलेंगे. साथ ही, डेल्टा, rememberNestedScrollInteropConnection() से मिले NestedScrollConnection तक नहीं पहुंचेंगे. इसलिए, वे डेल्टा पैरंट View कॉम्पोनेंट तक नहीं पहुंचेंगे. इस समस्या को हल करने के लिए, पक्का करें कि आपने नेस्ट किए गए इस तरह के कंपोज़ेबल के लिए, स्क्रोल किए जा सकने वाले मॉडिफ़ायर भी सेट किए हों. ज़्यादा जानकारी के लिए, नेस्टेड स्क्रोलिंग के बारे में बताया गया पिछला सेक्शन देखें.

सहयोग न करने वाले माता-पिता View के साथ बच्चा ComposeView

ऐसा व्यू जो ज़रूरी NestedScrolling इंटरफ़ेस को View साइड पर लागू नहीं करता है उसे नॉन-कोऑपरेटिंग व्यू कहा जाता है. ध्यान दें कि इसका मतलब है कि इन Views के साथ नेस्टेड स्क्रोलिंग इंटरऑपरेबिलिटी, बॉक्स से बाहर काम नहीं करती है. सहयोग न करने वाले Views, RecyclerView और ViewPager2 हैं.

अन्य संसाधन