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

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

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

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

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

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

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

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