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

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

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

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

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

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

অটোমেটিক নেস্টেড স্ক্রলিং কাজ করে এবং 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 এলিমেন্ট নেস্ট করার চেষ্টা করেন অথবা এর বিপরীত করেন, তখন আপনি সমস্যার সম্মুখীন হতে পারেন। সবচেয়ে বেশি চোখে পড়ার মতো ঘটনা হল, আপনি যখন চাইল্ড স্ক্রল করে তার শুরু বা শেষ সীমানায় পৌঁছান এবং আশা করেন যে প্যারেন্ট স্ক্রল করার দায়িত্ব নেবে। তবে, এই প্রত্যাশিত আচরণ হয়ত হবে না বা প্রত্যাশা অনুযায়ী কাজ করবে না।

এই সমস্যাটি স্ক্রল করা যায় এমন কম্পোজিবলে তৈরি প্রত্যাশার ফলাফল। স্ক্রল করা যায় এমন কম্পোজ করার উপযুক্ত আইটেমে "নেস্টেড-স্ক্রল-বাই-ডিফল্ট" নিয়ম থাকে, যার অর্থ হল যেকোনও স্ক্রল করা যায় এমন কন্টেনারকে নেস্টেড স্ক্রল চেনে অংশগ্রহণ করতে হবে, এটি 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পাবে।

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 মডিফায়ার সহ 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(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 নেস্টেড স্ক্রলিং ইন্টারওপ API ব্যবহার করা হয়:

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

সহযোগিতা না করা অভিভাবক View যার কাছে বাচ্চা আছে ComposeView

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

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