নেস্ট করা স্ক্রলিং মডিফায়ার

নেস্টেড স্ক্রলিং হল এমন একটি সিস্টেম যেখানে একে অপরের মধ্যে থাকা একাধিক স্ক্রলিং কম্পোনেন্ট একসাথে কাজ করে। এটি একটি স্ক্রল জেসচারের মাধ্যমে করা হয় এবং তাদের স্ক্রলিং ডেল্টা (পরিবর্তন) কমিউনিকেট করে।

নেস্টেড স্ক্রলিং সিস্টেম স্ক্রল করা যায় এমন কম্পোনেন্ট এবং হায়ারার্কিক্যালভাবে লিঙ্ক করা কম্পোনেন্টের মধ্যে সমন্বয় করতে দেয় (প্রায়শই একই পেরেন্ট শেয়ার করার মাধ্যমে)। এই সিস্টেম স্ক্রল করার কন্টেনার লিঙ্ক করে এবং স্ক্রল করার ডেল্টার সাথে ইন্টার‍্যাক্ট করার অনুমতি দেয় যা প্রসারিত এবং শেয়ার করা হচ্ছে।

কম্পোজেবলগুলির মধ্যে নেস্টেড স্ক্রোলিং ম্যানেজ করার একাধিক উপায় কম্পোজ প্রদান করে। নেস্টেড স্ক্রলিংয়ের একটি সাধারণ উদাহরণ হল একটি তালিকার মধ্যে আরেকটি তালিকা এবং আরও জটিল উদাহরণ হল আড়াল করা যায় এমন টুলবার।

অটোমেটিক নেস্টেড স্ক্রলিং

সহজ নেস্টেড স্ক্রলিংয়ের জন্য আপনাকে কোনও অ্যাকশন নিতে হবে না। স্ক্রল করার অ্যাকশন শুরু করে এমন জেসচার চাইল্ড থেকে পেরেন্ট এলিমেন্টে অটোমেটিক প্রোপাগেট করা হয়, যাতে চাইল্ড এলিমেন্ট আর স্ক্রল করতে না পারলে, জেসচারটি পেরেন্ট এলিমেন্ট দ্বারা ম্যানেজ করা হয়।

অটোমেটিক নেস্টেড স্ক্রলিং কাজ করে এবং Compose-এর কিছু কম্পোনেন্ট ও মডিফায়ার দ্বারা আউট অফ দ্য বক্স হিসেবে প্রদান করা হয়: verticalScroll, horizontalScroll, scrollable, Lazy API এবং 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)
                    )
                }
            }
        }
    }
}

দুটি নেস্টেড উল্লম্ব স্ক্রলিং UI এলিমেন্ট, যা ইনার এলিমেন্টের ভিতরে ও বাইরে জেসচারে সাড়া দেয়
ছবি ১. দুটি নেস্টেড উল্লম্ব স্ক্রলিং UI এলিমেন্ট, যা ইনার এলিমেন্টের ভিতরে ও বাইরে জেসচারে সাড়া দেয়।

nestedScroll মডিফায়ার ব্যবহার করা

একাধিক এলিমেন্টের মধ্যে আপনাকে উন্নত কোঅর্ডিনেটেড স্ক্রল তৈরি করতে হলে, nestedScroll মডিফায়ার আপনাকে নেস্টেড স্ক্রলিং হায়ারার্কি সংজ্ঞায়িত করার মাধ্যমে আরও নমনীয়তা প্রদান করে। আগের বিভাগে যেমন উল্লেখ করা হয়েছে, কিছু কম্পোনেন্টে নেস্টেড স্ক্রল সাপোর্ট বিল্ট-ইন থাকে। তবে, যেসব কম্পোজেবল অটোমেটিক স্ক্রল করা যায় না, যেমন Box বা Column, এই ধরনের কম্পোনেন্টে স্ক্রল ডেল্টা নেস্টেড স্ক্রল সিস্টেমে প্রোপাগেট করবে না এবং ডেল্টা NestedScrollConnection বা পেরেন্ট কম্পোনেন্টে পৌঁছাবে না। এই সমস্যার সমাধান করতে, আপনি nestedScroll ব্যবহার করে কাস্টম কম্পোনেন্ট সহ অন্যান্য কম্পোনেন্টে এই ধরনের সহায়তা প্রদান করতে পারেন।

নেস্ট করা স্ক্রলিং সাইকেল

নেস্টেড স্ক্রল সাইকেল হল স্ক্রল ডেল্টার ফ্লো যা নেস্টেড স্ক্রলিং সিস্টেমের অংশ এমন সব কম্পোনেন্ট (বা নোড) দিয়ে হায়ারার্কি ট্রি-র উপরে ও নিচে পাঠানো হয়, যেমন স্ক্রল করা যায় এমন কম্পোনেন্ট ও মডিফায়ার ব্যবহার করে অথবা nestedScroll।

নেস্টেড স্ক্রলিং সাইকেলের ফেজ

স্ক্রল করা যায় এমন কম্পোনেন্ট যখন কোনও ট্রিগার ইভেন্ট (যেমন, জেসচার) শনাক্ত করে, তখন প্রকৃত স্ক্রলিং অ্যাকশন ট্রিগার হওয়ার আগেই, জেনারেট হওয়া ডেল্টা নেস্টেড স্ক্রল সিস্টেমে পাঠানো হয় এবং তিনটি ফেজের মধ্যে দিয়ে যায়: প্রি-স্ক্রল, নোড কনজাম্পশন এবং পোস্ট-স্ক্রল।

নেস্টেড স্ক্রলিং সাইকেলের
পর্যায়
ছবি ২. নেস্টেড স্ক্রলিং সাইকেলের ফেজ।

প্রথম, প্রি-স্ক্রল ফেজে, যে কম্পোনেন্ট ট্রিগার ইভেন্ট ডেল্টা পেয়েছে, সেটি সেইসব ইভেন্ট হায়ারার্কি ট্রির মাধ্যমে সবচেয়ে উপরের পেরেন্ট কম্পোনেন্টে পাঠাবে। ডেল্টা ইভেন্টগুলি তখন বাবল ডাউন হবে, যার অর্থ হল ডেল্টাগুলি রুট-মোস্ট পেরেন্ট থেকে নেস্টেড স্ক্রল চক্র শুরু করা চাইল্ডের দিকে প্রসারিত হবে।

প্রি-স্ক্রল ফেজ - ডিসপ্যাচিং
আপ
ছবি ৩. প্রি-স্ক্রল ফেজ: ডিসপ্যাচিং আপ।

এটি নেস্টেড স্ক্রল পেরেন্টকে (nestedScroll বা স্ক্রল করা যায় এমন মডিফায়ার ব্যবহার করে কম্পোজেবল) নোড নিজে ব্যবহার করার আগে ডেল্টার সাথে কিছু করার সুযোগ দেয়।

প্রি-স্ক্রল ফেজ - বাবলিং
ডাউন
ছবি ৪. প্রি-স্ক্রল ফেজ - বাবলিং ডাউন।

নোড কনজাম্পশন ফেজে, নোড নিজেই এমন যেকোনও ডেল্টা ব্যবহার করবে যা তার পেরেন্ট ব্যবহার করেনি। স্ক্রল করার সময় এটি আসলে করা হয় এবং দেখা যায়।

Node-এর ব্যবহার
ফেজ
ছবি ৫. নোড কনজাম্পশন ফেজ।

এই পর্যায়ে, বাচ্চা বাকি স্ক্রলের সবটা বা কিছুটা ব্যবহার করার সিদ্ধান্ত নিতে পারে। বাকি থাকা যেকোনও কিছু পোস্ট-স্ক্রল ফেজের মাধ্যমে উপরে ফেরত পাঠানো হবে।

অবশেষে, পোস্ট-স্ক্রল ফেজে, নোড নিজে যা ব্যবহার করেনি তা আবার তার পূর্বপুরুষদের কাছে ব্যবহারের জন্য পাঠানো হবে।

স্ক্রল করার পরবর্তী পর্যায় - উপরে
পাঠানো হচ্ছে
ছবি ৬. স্ক্রল করার পরের পর্যায় - উপরে ডিসপ্যাচ করা হচ্ছে।

পোস্ট-স্ক্রল ফেজ প্রি-স্ক্রল ফেজের মতোই কাজ করে, যেখানে যেকোনও অভিভাবক কন্টেন্ট দেখতে বা না দেখতে পারেন।

পোস্ট-স্ক্রল ফেজ - বাবলিং
ডাউন
ছবি ৭. স্ক্রল করার পরের ফেজ - বাবলিং ডাউন।

স্ক্রল করার মতোই, টেনে আনার জেসচার শেষ হলে, ব্যবহারকারীর উদ্দেশ্যকে একটি গতিতে অনুবাদ করা হতে পারে যা স্ক্রলযোগ্য কন্টেনারকে ফ্লিং (অ্যানিমেশন ব্যবহার করে স্ক্রল) করতে ব্যবহার করা হয়। ফ্লিংও নেস্টেড স্ক্রল সাইকেলের অংশ এবং ড্র্যাগ ইভেন্টের মাধ্যমে তৈরি হওয়া গতিবেগ একই ধরনের ফেজের মধ্যে দিয়ে যায়: প্রি-ফ্লিং, নোড কনজাম্পশন এবং পোস্ট-ফ্লিং। মনে রাখবেন যে ফ্লিং অ্যানিমেশন শুধুমাত্র টাচ জেসচারের সাথে সম্পর্কিত এবং অন্যান্য ইভেন্ট যেমন 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 থেকে পাওয়া যায়।
  • currentImageSize-এর উপর ভিত্তি করে LazyColumn অফসেট।
  • Image, গণনা করা স্কেল প্রয়োগ করতে graphicsLayer মডিফায়ার ব্যবহার করে।
  • graphicsLayer-এর মধ্যে translationY নিশ্চিত করে যে স্কেল করার সময় ছবিটি উল্লম্বভাবে কেন্দ্রেই থাকবে।

ফলাফল

আগের স্নিপেটের ফলে স্ক্রল করার সময় স্কেলিং ইমেজ এফেক্ট দেখা যায়:

ছবি ৮. স্ক্রল করার সময় ছবির স্কেলিং এফেক্ট।

নেস্টেড স্ক্রলিং ইন্টারঅপ

আপনি যখন স্ক্রল করা যায় এমন কম্পোজেবলে স্ক্রল করা যায় এমন View এলিমেন্ট নেস্ট করার চেষ্টা করেন, অথবা অন্যভাবে, আপনি সমস্যার সম্মুখীন হতে পারেন। Most noticeable ones would happen when you scroll the child and reach its start or end bounds and expect the parent to take the scrolling over. তবে, এই প্রত্যাশিত আচরণ হয় ঘটবে না অথবা প্রত্যাশা অনুযায়ী কাজ করবে না।

স্ক্রল করা যায় এমন কম্পোজেবল থেকে যে প্রত্যাশা তৈরি হয়, তার ফলে এই সমস্যা দেখা দেয়। স্ক্রল করা যায় এমন কম্পোজেবলে "ডিফল্ট-হিসেবে-নেস্টেড-স্ক্রল" নিয়ম থাকে, যার অর্থ হল স্ক্রল করা যায় এমন যেকোনও কন্টেনারকে নেস্টেড স্ক্রল চেনে অংশগ্রহণ করতে হবে, NestedScrollConnection-এর মাধ্যমে পেরেন্ট হিসেবে এবং NestedScrollDispatcher-এর মাধ্যমে চাইল্ড হিসেবে। চাইল্ড এলিমেন্ট বাউন্ডে থাকলে, সেটি পেরেন্ট এলিমেন্টের জন্য নেস্টেড স্ক্রল ড্রাইভ করবে। উদাহরণস্বরূপ, এই নিয়ম Compose Pager এবং Compose LazyRow-কে একসাথে ভালভাবে কাজ করার অনুমতি দেয়। তবে, ViewPager2 বা RecyclerView-এর সাথে ইন্টারঅপারেবিলিটি স্ক্রলিং করা হলে, যেহেতু এগুলি NestedScrollingParent3 প্রয়োগ করে না, তাই চাইল্ড থেকে পেরেন্ট পর্যন্ত একটানা স্ক্রল করা সম্ভব নয়।

স্ক্রল করা যায় এমন View এলিমেন্ট এবং স্ক্রল করা যায় এমন কম্পোজেবলের মধ্যে নেস্টেড স্ক্রলিং ইন্টারঅপ API চালু করতে, উভয় দিকে নেস্ট করা, আপনি নিম্নলিখিত পরিস্থিতিতে এই সমস্যাগুলি সমাধান করতে নেস্টেড স্ক্রলিং ইন্টারঅপ API ব্যবহার করতে পারেন।

একটি কো-অপারেটিং পেরেন্ট View যার মধ্যে একটি চাইল্ড ComposeView আছে

সহযোগী পেরেন্ট View হল এমন একটি যা আগে থেকেই NestedScrollingParent3 প্রয়োগ করে এবং তাই সহযোগী নেস্টেড চাইল্ড কম্পোজেবল থেকে স্ক্রলিং ডেল্টা পেতে সক্ষম। এই ক্ষেত্রে ComposeView চাইল্ড হিসেবে কাজ করবে এবং NestedScrollingChild3 (পরোক্ষভাবে) প্রয়োগ করতে হবে। সহযোগিতা করা পেরেন্ট মলিকিউলের একটি উদাহরণ হল androidx.coordinatorlayout.widget.CoordinatorLayout।

স্ক্রল করা যায় এমন View পেরেন্ট কন্টেনার এবং নেস্টেড স্ক্রল করা যায় এমন চাইল্ড কম্পোজেবলের মধ্যে নেস্টেড স্ক্রলিং ইন্টারঅপারেবিলিটি প্রয়োজন হলে, আপনি rememberNestedScrollInteropConnection() ব্যবহার করতে পারেন।

rememberNestedScrollInteropConnection() , NestedScrollConnection -কে অনুমতি দেয় ও মনে রাখে যা View পেরেন্ট, যেটি NestedScrollingParent3 ইমপ্লিমেন্ট করে এবং কম্পোজ চাইল্ডের মধ্যে নেস্টেড স্ক্রল ইন্টারঅপারেবিলিটি চালু করে। এটি nestedScroll মডিফায়ারের সাথে একত্রে ব্যবহার করা উচিত। Compose-এর দিকে নেস্টেড স্ক্রলিং ডিফল্ট হিসেবে চালু থাকে বলে, আপনি এই কানেকশন ব্যবহার করে 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 আছে

এই পরিস্থিতিতে কম্পোজের দিকে নেস্টেড স্ক্রলিং ইন্টারঅপ API-এর প্রয়োগ কভার করা হয় - যখন আপনার কাছে চাইল্ড AndroidView সহ পেরেন্ট কম্পোজেবল থাকে। AndroidView NestedScrollDispatcher প্রয়োগ করে, কারণ এটি কম্পোজ স্ক্রলিং পেরেন্টের চাইল্ড হিসেবে কাজ করে, এর পাশাপাশি NestedScrollingParent3 প্রয়োগ করে, কারণ এটি View স্ক্রলিং চাইল্ডের পেরেন্ট হিসেবে কাজ করে। এরপর, কম্পোজ পেরেন্ট নেস্টেড স্ক্রলযোগ্য চাইল্ড View থেকে নেস্টেড স্ক্রল ডেল্টা রিসিভ করতে পারবে।

নিম্নলিখিত উদাহরণে দেখানো হয়েছে যে কীভাবে আপনি এই পরিস্থিতিতে নেস্টেড স্ক্রলিং ইন্টারঅপারেশন অর্জন করতে পারেন, এর সাথে একটি কম্পোজ কোলাপ্সিং টুলবারও রয়েছে:

@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(
            title = {},
            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 মডিফায়ারের সাথে API ব্যবহার করতে পারবেন:

@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(R.layout.list_item, null)
                    .apply {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(this, true)
                    }
            }
        )
    }
}

এবং সবশেষে, এই উদাহরণে দেখানো হয়েছে যে কীভাবে নেস্টেড স্ক্রলিং ইন্টারঅপ API 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 আছে

একটি নন-কোঅপারেটিং ভিউ হল এমন একটি ভিউ যা View-এর দিকে প্রয়োজনীয় NestedScrolling ইন্টারফেস প্রয়োগ করে না। মনে রাখবেন, এর অর্থ হল এই Views-এর সাথে নেস্টেড স্ক্রলিং ইন্টারঅপারেবিলিটি বক্সের বাইরে কাজ করে না। সহযোগিতা না করা Views হল RecyclerView এবং ViewPager2।

অতিরিক্ত রিসোর্স