Wear OS এর জন্য রচনা সহ তালিকা


Wear OS ডিভাইসে তালিকা ব্যবহারকারীদের বিভিন্ন বিকল্পের মধ্য থেকে কোনো একটি আইটেম বেছে নিতে সাহায্য করে।

অনেক Wear OS ডিভাইসে গোলাকার স্ক্রিন ব্যবহার করা হয়, যার ফলে স্ক্রিনের ওপর ও নিচের দিকের লিস্ট আইটেমগুলো দেখতে অসুবিধা হয়। এই কারণে, Wear OS-এর জন্য Compose-এ LazyColumn ক্লাসের TransformingLazyColumn নামক একটি সংস্করণ অন্তর্ভুক্ত করা হয়েছে, যা স্কেলিং এবং মর্ফিং অ্যানিমেশন সমর্থন করে। আইটেমগুলো যখন প্রান্তের দিকে চলে যায়, তখন সেগুলো ছোট হয়ে যায় এবং ধীরে ধীরে মিলিয়ে যায়।

প্রস্তাবিত স্কেলিং এবং স্ক্রোলিং ইফেক্টগুলো প্রয়োগ করতে:

  1. স্ক্রিনে আইটেমটি স্ক্রল করার সময় Compose-কে উচ্চতার পরিবর্তন গণনা করার সুযোগ দিতে Modifier.transformedHeight ব্যবহার করুন।
  2. আইটেমের বিষয়বস্তু ছোট করা সহ ভিজ্যুয়াল ইফেক্টগুলো প্রয়োগ করতে transformation = SurfaceTransformation(transformationSpec) ব্যবহার করুন।
  3. যেসব কম্পোনেন্ট প্যারামিটার হিসেবে transformation গ্রহণ করে না, যেমন Text সেগুলোর জন্য একটি কাস্টম TransformationSpec ব্যবহার করুন।

নিম্নলিখিত অ্যানিমেশনটি দেখায় কিভাবে একটি তালিকার উপাদান স্ক্রিনের উপরে এবং নীচে যাওয়ার সময় তার আকার ও আকৃতি পরিবর্তন করে:

নিম্নলিখিত কোড স্নিপেটটি দেখায় কিভাবে TransformingLazyColumn লেআউট ব্যবহার করে একটি তালিকা তৈরি করা যায়, যার বিষয়বস্তু বিভিন্ন Wear OS স্ক্রিনের আকারে চমৎকার দেখায়

এই কোড স্নিপেটটিতে minimumVerticalContentPadding মডিফায়ারটির ব্যবহারও দেখানো হয়েছে, যা লিস্টের উপরে ও নীচে সঠিক প্যাডিং প্রয়োগ করার জন্য লিস্ট আইটেমগুলোতে সেট করতে হয়।

স্ক্রোল ইন্ডিকেটর দেখানোর জন্য, ScreenScaffold এবং TransformingLazyColumn এর মধ্যে columnState শেয়ার করুন:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(
    scrollState = columnState
) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding
    ) {
        item {
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text(text = "Header")
            }
        }
        // ... other items
        item {
            Button(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec),
                onClick = { /* ... */ },
                icon = {
                    Icon(
                        imageVector = Icons.Default.Build,
                        contentDescription = "build",
                    )
                },
            ) {
                Text(
                    text = "Build",
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
        }
    }
}

একটি স্ন্যাপ-অ্যান্ড-ফ্লিং এফেক্ট যোগ করুন

স্ন্যাপিং নিশ্চিত করে যে, যখন কোনো ব্যবহারকারী স্ক্রল বা ফ্লিং জেসচার শেষ করেন, তখন তালিকাটির একটি আইটেম একটি নির্দিষ্ট বিন্দুতে, সাধারণত স্ক্রিনের কেন্দ্রে, নির্ভুলভাবে অবস্থান করে স্থির হয়। গোলাকার স্ক্রিনে, যেখানে আইটেমগুলো কেন্দ্র থেকে দূরে সরে গেলে আকার ও আকৃতি পরিবর্তন করে, সেখানে সবচেয়ে প্রাসঙ্গিক আইটেমটি যেন সর্বোত্তম দেখার জায়গায় সম্পূর্ণ দৃশ্যমান ও পাঠযোগ্য থাকে, তা নিশ্চিত করতে স্ন্যাপিং বিশেষভাবে কার্যকর।

স্ন্যাপ-অ্যান্ড-ফ্লিং আচরণ যোগ করতে, flingBehavior প্যারামিটারটিকে TransformingLazyColumnDefaults.snapFlingBehavior(columnState) -এ সেট করুন। ফিজিক্যাল ক্রাউন বা বেজেল ব্যবহার করার সময় একটি সামঞ্জস্যপূর্ণ অভিজ্ঞতার জন্য, rotaryScrollableBehavior টিকেও RotaryScrollableDefaults.snapBehavior(columnState) ব্যবহার করে এর সাথে মিলিয়ে সেট করুন।

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState),
        rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState)
    ) {
        // ...
        // ...
    }
}

বিপরীত বিন্যাস

ডিফল্টরূপে, একটি স্ক্রোলযোগ্য তালিকা তার উপরের প্রান্তে স্থির থাকে। যদি কোনো ব্যবহারকারী একটি সাধারণ তালিকার একেবারে নিচে স্ক্রোল করে যান এবং শেষে একটি নতুন আইটেম যুক্ত করা হয়, তবে তালিকাটি বর্তমান আইটেমটির উপর ব্যবহারকারীর দৃষ্টি বজায় রাখে। উদাহরণস্বরূপ, যদি ব্যবহারকারী স্ক্রিনের নিচের দিকে ১০ নম্বর আইটেমটি দেখে থাকেন এবং ১১ নম্বর আইটেমটি যুক্ত করা হয়, তবে দৃষ্টি ১০ নম্বর আইটেমটির উপরেই নিবদ্ধ থাকবে এবং ১১ নম্বর আইটেমটি বর্তমান দৃশ্যের নিচে স্ক্রিনের বাইরে প্রদর্শিত হবে।

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

এইসব ব্যবহারের ক্ষেত্রকে সমর্থন করার জন্য, TransformingLazyColumn আপনাকে reverseLayout = true সেট করে লেআউটটি উল্টে দেওয়ার সুযোগ দেয়। এর ফলে তালিকার অ্যাঙ্করটি উপরের প্রান্ত থেকে নিচের প্রান্তে স্থানান্তরিত হয়।

সুবিধার জন্য, reverseLayout = true সেট করলে আইটেমগুলোর দৃশ্যমান ক্রম এবং স্ক্রোলিং জেসচারের দিকও উল্টে যায়:

  • আইটেমগুলো নিচ থেকে উপরের দিকে সাজানো থাকে, অর্থাৎ ইনডেক্স ০ স্ক্রিনের নিচে প্রদর্শিত হয়।
  • উপরে স্ক্রল করলে উচ্চতর সূচকের আইটেমগুলো দেখা যায়।

রিভার্স লেআউটের সাথে স্ন্যাপ-অ্যান্ড-ফ্লিং বিহেভিয়ার যোগ করতে, আপনি নিচের কোড স্নিপেটে দেখানো অনুযায়ী flingBehavior এবং rotaryScrollableBehavior একত্রিত করতে পারেন:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding,
        reverseLayout = true,
        modifier = Modifier.fillMaxWidth()
    ) {
        items(10) { index ->
            Button(
                label = {
                    Text(
                        text = "Item ${index + 1}"
                    )
                },
                onClick = {},
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            )
        }
        item {
            // With reverseLayout = true, the last item declared appears at the top.
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text("Header")
            }
        }
    }
}

নিচের ছবিগুলোতে একটি সাধারণ তালিকা এবং একটি বিপরীত তালিকার মধ্যে পার্থক্য দেখানো হয়েছে:

সাধারণ লেআউট সহ একটি TransformingLazyColumn, যা শীর্ষে আইটেম ১ এবং বাকি আইটেমগুলো আরোহী ক্রমে প্রদর্শন করে।
চিত্র ১. একটি আদর্শ তালিকা বিন্যাস যেখানে বিষয়বস্তু উপর থেকে নিচে পূর্ণ হয়।
বিপরীত লেআউট সহ একটি TransformingLazyColumn, যা আইটেম ১-কে নীচে এবং আইটেমগুলিকে অবরোহী ক্রমে উপরের দিকে দেখায়।
চিত্র ২. একটি বিপরীত তালিকা বিন্যাস যেখানে বিষয়বস্তু নিচ থেকে উপরের দিকে পূরণ হয়।

তালিকার প্রান্তের বোতামগুলি

ম্যাটেরিয়াল ৩-এর জন্য, আপনি তালিকার নীচে একটি EdgeButton যোগ করতে পারেন, যা তালিকার কিনারা ঘেঁষে থাকা একটি বাটন। তবে, খেয়াল রাখবেন যেন এটিকে TransformingLazyColumn এর ভেতরের একটি আইটেম হিসেবে যোগ না করে, বরং ScreenScaffold এর edgeButton স্লটটি ব্যবহার করেন।

edgeButton স্লট ব্যবহার করলে বাটনটি স্ক্রিনের নীচে সঠিকভাবে অবস্থান করে এবং তালিকাটি স্ক্রোল করার সময় যথাযথভাবে কাজ করে।

নিম্নলিখিত কোড স্নিপেটটি দেখায় কিভাবে EdgeButton প্রয়োগ করতে হয়:

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(
    scrollState = columnState,
    edgeButton = {
        EdgeButton(
            onClick = { /* TODO */ },
            modifier = Modifier.scrollable(
                columnState,
                orientation = Orientation.Vertical,
                reverseDirection = true,
                // Apply overscroll to the EdgeButton for proper scrolling behavior.
                overscrollEffect = rememberOverscrollEffect(),
            )
        ) {
            Text("More")
        }
    }
) { contentPadding ->
    TransformingLazyColumn(
        contentPadding = contentPadding,
        state = columnState,
    ) {
        // ...
        // ...
    }
}

তালিকায় সোয়াইপ করে দেখুন

SwipeToReveal কম্পোনেন্টটি আপনাকে সোয়াইপ করার মাধ্যমে তালিকার কোনো আইটেমের (যেমন Card বা Chip অ্যাকশনগুলো অ্যাক্সেস করতে দেয়। সোয়াইপ করলে সাধারণত পাশ থেকে এক বা দুটি অ্যাকশন বাটন (যেমন 'ডিলিট' বা 'মোর') দেখা যায়।

TransformingLazyColumn মধ্যে SwipeToReveal ব্যবহার করার সময়, এই নির্দেশিকাগুলো অনুসরণ করুন:

  • স্ক্রোল করলে রিসেট : যখন ব্যবহারকারী তালিকাটি স্ক্রোল করেন, তখন সোয়াইপ করে খোলা আইটেমগুলিকে তাদের ঢাকা অবস্থায় রিসেট করুন।
  • সামঞ্জস্যপূর্ণ উচ্চতা : একটি সামঞ্জস্যপূর্ণ চেহারা নিশ্চিত করতে, অ্যাকশন বাটনের উচ্চতা ভেতরের সোয়াইপ করা আইটেমের (সেটি Button হোক বা Card ) সাথে মিলিয়ে সেট করুন।
  • কন্টেইনারটি রূপান্তর করুন : transformedHeight মডিফায়ার এবং transformationSpec সরাসরি SwipeToReveal কম্পোনেন্টটিতে প্রয়োগ করুন।
  • দ্বৈত রূপান্তর করবেন না : ভেতরের সোয়াইপ করা আইটেমটিতে ( SwipeToReveal কন্টেইনারের ভেতরের কার্ড বা বাটন) transformedHeight বা transformation মডিফায়ার প্রয়োগ করবেন না।

তালিকায় কাস্টম কম্পোজেবল

TransformingLazyColumn এর জন্য কাস্টম সারফেস কম্পোনেন্ট তৈরি করার সময় এই সেরা অনুশীলনগুলি অনুসরণ করুন, যাতে আপনার কম্পোজেবলটি ডিসপ্লের প্রান্তের কাছাকাছি মসৃণভাবে স্কেল, ফেড এবং মর্ফ হয়:

  • SurfaceTransformation প্রকাশ করুন : একটি ঐচ্ছিক SurfaceTransformation প্যারামিটার গ্রহণ করে (যার ডিফল্ট মান null ), যা Card এবং Button মতো স্ট্যান্ডার্ড Wear Compose Material 3 কম্পোনেন্টগুলোর সাথে সামঞ্জস্যপূর্ণ। এটি TransformingLazyColumn ভেতরের কলারদের SurfaceTransformation(transformationSpec) পাস করার সুযোগ দেয়, এবং একই সাথে কম্পোনেন্টটিকে লিস্টের বাইরেও স্বাভাবিকভাবে কাজ করতে দেয়।
  • কলিং কোডে প্রথমে Modifier.transformedHeight প্রয়োগ করুন : যখন আপনার কাস্টম কম্পোজেবল একটি TransformingLazyColumn এ রাখবেন, তখন কলিং কোডের মডিফায়ার চেইনের প্রথম মডিফায়ার হিসেবে Modifier.transformedHeight(this, transformationSpec) পাস করুন। যদিও SurfaceTransformation ভিজ্যুয়াল স্কেলিং এবং ফেডিং এফেক্ট প্রয়োগ করে, আইটেমটি ছোট হওয়ার সাথে সাথে এর উচ্চতা পুনরায় গণনা করার জন্য লিস্ট লেআউটকে নির্দেশ দিতে transformedHeight অত্যন্ত গুরুত্বপূর্ণ।
  • ক্রমানুসারে ট্রান্সফরমেশন লেয়ার, কলার modifier এবং পেইন্টার প্রয়োগ করুন :
    1. কন্টেইনার ট্রান্সফরমেশন লেয়ার : রুট কন্টেইনারের মডিফায়ার চেইন Modifier.graphicsLayer দিয়ে শুরু করুন এবং applyContainerTransformation() , যাতে ব্যাকগ্রাউন্ড এবং কন্টেন্ট উভয়ই স্কেল করা ও হেলানো স্থানাঙ্ক পরিসরের মধ্যে অঙ্কিত হয়।
    2. কলার modifier : কলার দ্বারা প্রেরিত modifier প্যারামিটারটি (যার মধ্যে Modifier.transformedHeight অন্তর্ভুক্ত) যেকোনো অভ্যন্তরীণ সাইজিং বা প্যাডিংয়ের আগে প্রয়োগ করুন।
    3. ট্রান্সফরমেশন না থাকলে শেপ ক্লিপ : যদি transformation null হয়, তাহলে ব্যাকগ্রাউন্ড আঁকার আগে Modifier.clip(shape) প্রয়োগ করুন। createContainerPainter() দ্বারা রিটার্ন করা পেইন্টারটি নিজেকে শেপের সাথে ক্লিপ করে, কিন্তু একটি সাধারণ পেইন্টার তা করে না, তাই এটি ছাড়া ব্যাকগ্রাউন্ডটি একটি লিস্টের বাইরে বর্গাকার কোণাসহ আঁকা হয়।
    4. মর্ফিং ব্যাকগ্রাউন্ড পেইন্টার : Modifier.drawBehind এবং createContainerPainter() থেকে তৈরি একটি পেইন্টার ব্যবহার করে কন্টেইনার লেয়ারের ভিতরে ব্যাকগ্রাউন্ডটি আঁকুন।
    5. কন্টেন্ট ট্রান্সফরমেশন লেয়ার : applyContentTransformation() সহ একটি দ্বিতীয় Modifier.graphicsLayer প্রয়োগ করুন এবং কন্টেইনার শেপে ক্লিপ করুন, যাতে ভেতরের কন্টেন্ট বেজেলের কাছে আসার সাথে সাথে আগেভাগে ফেড আউট হয়ে যায়।

নিম্নলিখিত কোড স্নিপেটটি দেখায় কিভাবে একটি কাস্টম BoardingPassCard কম্পোজেবল প্রয়োগ করতে হয় যা এই রূপান্তরগুলি ক্রমানুসারে প্রয়োগ করে:

@Composable
fun BoardingPassCard(
    flightNumber: String,
    origin: String,
    destination: String,
    gate: String,
    seat: String,
    departureTime: String,
    modifier: Modifier = Modifier,
    transformation: SurfaceTransformation? = null,
    shape: Shape = RoundedCornerShape(18.dp),
    statusBadge: @Composable () -> Unit = {}
) {
    // 1. Create morphing container painter
    val backgroundPainter = ColorPainter(MaterialTheme.colorScheme.surfaceContainer)
    val finalPainter = if (transformation != null) {
        remember(transformation, backgroundPainter, shape) {
            transformation.createContainerPainter(backgroundPainter, shape, border = null)
        }
    } else {
        backgroundPainter
    }

    Column(
        modifier = Modifier
            // 2a. Container layer: Scales, fades, and tilts the whole card surface
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        transformation.run { applyContainerTransformation() }
                    }
                } else Modifier
            )
            // 2b. Caller modifier: Includes Modifier.transformedHeight in a list
            .then(modifier)
            .fillMaxWidth()
            // 2c. Shape clip: Only needed without a transformation, because the
            // painter from createContainerPainter clips itself to the shape
            .then(if (transformation == null) Modifier.clip(shape) else Modifier)
            // 2d. Morphing background: Drawn inside the transformed container layer
            .drawBehind {
                with(finalPainter) {
                    draw(size)
                }
            }
            // 2e. Content layer: Fades content earlier and clips children to shape
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        this.shape = shape
                        this.clip = true
                        transformation.run { applyContentTransformation() }
                    }
                } else Modifier
            )
            .padding(horizontal = 14.dp, vertical = 10.dp)
    ) {
        // Card content goes here
    }
}
চিত্র ৩। একটি তালিকা স্ক্রল করার সাথে সাথে রূপান্তরিত হতে থাকা একটি কাস্টম বোর্ডিং পাস।

এরপর আপনি প্রথম মডিফায়ার হিসেবে Modifier.transformedHeight এবং SurfaceTransformation(transformationSpec) পাস করে একটি TransformingLazyColumn ভিতরে BoardingPassCard ব্যবহার করতে পারেন:

@Composable
fun BoardingPassListSample(flights: List<FlightInfo>) {
    val listState = rememberTransformingLazyColumnState()
    val transformationSpec = rememberTransformationSpec()


    ScreenScaffold(scrollState = listState) { contentPadding ->
        TransformingLazyColumn(
            state = listState,
            contentPadding = contentPadding,
            modifier = Modifier.fillMaxSize()
        ) {
            items(flights.size) { index ->
                val flight = flights[index]
                BoardingPassCard(
                    flightNumber = flight.number,
                    origin = flight.origin,
                    destination = flight.destination,
                    gate = flight.gate,
                    seat = flight.seat,
                    departureTime = flight.time,
                    modifier = Modifier
                        .transformedHeight(this, transformationSpec)
                        .minimumVerticalContentPadding(
                            CardDefaults.minimumVerticalListContentPadding
                        ),
                    transformation = SurfaceTransformation(transformationSpec)
                )
            }
        }
    }
}
{% হুবহু %} {% endverbatim %} {% হুবহু %} {% endverbatim %} ,

Wear OS ডিভাইসে তালিকা ব্যবহারকারীদের বিভিন্ন বিকল্পের মধ্য থেকে কোনো একটি আইটেম বেছে নিতে সাহায্য করে।

অনেক Wear OS ডিভাইসে গোলাকার স্ক্রিন ব্যবহার করা হয়, যার ফলে স্ক্রিনের ওপর ও নিচের দিকের লিস্ট আইটেমগুলো দেখতে অসুবিধা হয়। এই কারণে, Wear OS-এর জন্য Compose-এ LazyColumn ক্লাসের TransformingLazyColumn নামক একটি সংস্করণ অন্তর্ভুক্ত করা হয়েছে, যা স্কেলিং এবং মর্ফিং অ্যানিমেশন সমর্থন করে। আইটেমগুলো যখন প্রান্তের দিকে চলে যায়, তখন সেগুলো ছোট হয়ে যায় এবং ধীরে ধীরে মিলিয়ে যায়।

প্রস্তাবিত স্কেলিং এবং স্ক্রোলিং ইফেক্টগুলো প্রয়োগ করতে:

  1. স্ক্রিনে আইটেমটি স্ক্রল করার সময় Compose-কে উচ্চতার পরিবর্তন গণনা করার সুযোগ দিতে Modifier.transformedHeight ব্যবহার করুন।
  2. আইটেমের বিষয়বস্তু ছোট করা সহ ভিজ্যুয়াল ইফেক্টগুলো প্রয়োগ করতে transformation = SurfaceTransformation(transformationSpec) ব্যবহার করুন।
  3. যেসব কম্পোনেন্ট প্যারামিটার হিসেবে transformation গ্রহণ করে না, যেমন Text সেগুলোর জন্য একটি কাস্টম TransformationSpec ব্যবহার করুন।

নিম্নলিখিত অ্যানিমেশনটি দেখায় কিভাবে একটি তালিকার উপাদান স্ক্রিনের উপরে এবং নীচে যাওয়ার সময় তার আকার ও আকৃতি পরিবর্তন করে:

নিম্নলিখিত কোড স্নিপেটটি দেখায় কিভাবে TransformingLazyColumn লেআউট ব্যবহার করে একটি তালিকা তৈরি করা যায়, যার বিষয়বস্তু বিভিন্ন Wear OS স্ক্রিনের আকারে চমৎকার দেখায়

এই কোড স্নিপেটটিতে minimumVerticalContentPadding মডিফায়ারটির ব্যবহারও দেখানো হয়েছে, যা লিস্টের উপরে ও নীচে সঠিক প্যাডিং প্রয়োগ করার জন্য লিস্ট আইটেমগুলোতে সেট করতে হয়।

স্ক্রোল ইন্ডিকেটর দেখানোর জন্য, ScreenScaffold এবং TransformingLazyColumn এর মধ্যে columnState শেয়ার করুন:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(
    scrollState = columnState
) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding
    ) {
        item {
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text(text = "Header")
            }
        }
        // ... other items
        item {
            Button(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec),
                onClick = { /* ... */ },
                icon = {
                    Icon(
                        imageVector = Icons.Default.Build,
                        contentDescription = "build",
                    )
                },
            ) {
                Text(
                    text = "Build",
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
        }
    }
}

একটি স্ন্যাপ-অ্যান্ড-ফ্লিং এফেক্ট যোগ করুন

স্ন্যাপিং নিশ্চিত করে যে, যখন কোনো ব্যবহারকারী স্ক্রল বা ফ্লিং জেসচার শেষ করেন, তখন তালিকাটির একটি আইটেম একটি নির্দিষ্ট বিন্দুতে, সাধারণত স্ক্রিনের কেন্দ্রে, নির্ভুলভাবে অবস্থান করে স্থির হয়। গোলাকার স্ক্রিনে, যেখানে আইটেমগুলো কেন্দ্র থেকে দূরে সরে গেলে আকার ও আকৃতি পরিবর্তন করে, সেখানে সবচেয়ে প্রাসঙ্গিক আইটেমটি যেন সর্বোত্তম দেখার জায়গায় সম্পূর্ণ দৃশ্যমান ও পাঠযোগ্য থাকে, তা নিশ্চিত করতে স্ন্যাপিং বিশেষভাবে কার্যকর।

স্ন্যাপ-অ্যান্ড-ফ্লিং আচরণ যোগ করতে, flingBehavior প্যারামিটারটিকে TransformingLazyColumnDefaults.snapFlingBehavior(columnState) -এ সেট করুন। ফিজিক্যাল ক্রাউন বা বেজেল ব্যবহার করার সময় একটি সামঞ্জস্যপূর্ণ অভিজ্ঞতার জন্য, rotaryScrollableBehavior টিকেও RotaryScrollableDefaults.snapBehavior(columnState) ব্যবহার করে এর সাথে মিলিয়ে সেট করুন।

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState),
        rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState)
    ) {
        // ...
        // ...
    }
}

বিপরীত বিন্যাস

ডিফল্টরূপে, একটি স্ক্রোলযোগ্য তালিকা তার উপরের প্রান্তে স্থির থাকে। যদি কোনো ব্যবহারকারী একটি সাধারণ তালিকার একেবারে নিচে স্ক্রোল করে যান এবং শেষে একটি নতুন আইটেম যুক্ত করা হয়, তবে তালিকাটি বর্তমান আইটেমটির উপর ব্যবহারকারীর দৃষ্টি বজায় রাখে। উদাহরণস্বরূপ, যদি ব্যবহারকারী স্ক্রিনের নিচের দিকে ১০ নম্বর আইটেমটি দেখে থাকেন এবং ১১ নম্বর আইটেমটি যুক্ত করা হয়, তবে দৃষ্টি ১০ নম্বর আইটেমটির উপরেই নিবদ্ধ থাকবে এবং ১১ নম্বর আইটেমটি বর্তমান দৃশ্যের নিচে স্ক্রিনের বাইরে প্রদর্শিত হবে।

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

এইসব ব্যবহারের ক্ষেত্রকে সমর্থন করার জন্য, TransformingLazyColumn আপনাকে reverseLayout = true সেট করে লেআউটটি উল্টে দেওয়ার সুযোগ দেয়। এর ফলে তালিকার অ্যাঙ্করটি উপরের প্রান্ত থেকে নিচের প্রান্তে স্থানান্তরিত হয়।

সুবিধার জন্য, reverseLayout = true সেট করলে আইটেমগুলোর দৃশ্যমান ক্রম এবং স্ক্রোলিং জেসচারের দিকও উল্টে যায়:

  • আইটেমগুলো নিচ থেকে উপরের দিকে সাজানো থাকে, অর্থাৎ ইনডেক্স ০ স্ক্রিনের নিচে প্রদর্শিত হয়।
  • উপরে স্ক্রল করলে উচ্চতর সূচকের আইটেমগুলো দেখা যায়।

রিভার্স লেআউটের সাথে স্ন্যাপ-অ্যান্ড-ফ্লিং বিহেভিয়ার যোগ করতে, আপনি নিচের কোড স্নিপেটে দেখানো অনুযায়ী flingBehavior এবং rotaryScrollableBehavior একত্রিত করতে পারেন:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding,
        reverseLayout = true,
        modifier = Modifier.fillMaxWidth()
    ) {
        items(10) { index ->
            Button(
                label = {
                    Text(
                        text = "Item ${index + 1}"
                    )
                },
                onClick = {},
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            )
        }
        item {
            // With reverseLayout = true, the last item declared appears at the top.
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text("Header")
            }
        }
    }
}

নিচের ছবিগুলোতে একটি সাধারণ তালিকা এবং একটি বিপরীত তালিকার মধ্যে পার্থক্য দেখানো হয়েছে:

সাধারণ লেআউট সহ একটি TransformingLazyColumn, যা শীর্ষে আইটেম ১ এবং বাকি আইটেমগুলো আরোহী ক্রমে প্রদর্শন করে।
চিত্র ১. একটি আদর্শ তালিকা বিন্যাস যেখানে বিষয়বস্তু উপর থেকে নিচে পূর্ণ হয়।
বিপরীত লেআউট সহ একটি TransformingLazyColumn, যা আইটেম ১-কে নীচে এবং আইটেমগুলিকে অবরোহী ক্রমে উপরের দিকে দেখায়।
চিত্র ২. একটি বিপরীত তালিকা বিন্যাস যেখানে বিষয়বস্তু নিচ থেকে উপরের দিকে পূরণ হয়।

তালিকার প্রান্তের বোতামগুলি

ম্যাটেরিয়াল ৩-এর জন্য, আপনি তালিকার নীচে একটি EdgeButton যোগ করতে পারেন, যা তালিকার কিনারা ঘেঁষে থাকা একটি বাটন। তবে, খেয়াল রাখবেন যেন এটিকে TransformingLazyColumn এর ভেতরের একটি আইটেম হিসেবে যোগ না করে, বরং ScreenScaffold এর edgeButton স্লটটি ব্যবহার করেন।

edgeButton স্লট ব্যবহার করলে বাটনটি স্ক্রিনের নীচে সঠিকভাবে অবস্থান করে এবং তালিকাটি স্ক্রোল করার সময় যথাযথভাবে কাজ করে।

নিম্নলিখিত কোড স্নিপেটটি দেখায় কিভাবে EdgeButton প্রয়োগ করতে হয়:

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(
    scrollState = columnState,
    edgeButton = {
        EdgeButton(
            onClick = { /* TODO */ },
            modifier = Modifier.scrollable(
                columnState,
                orientation = Orientation.Vertical,
                reverseDirection = true,
                // Apply overscroll to the EdgeButton for proper scrolling behavior.
                overscrollEffect = rememberOverscrollEffect(),
            )
        ) {
            Text("More")
        }
    }
) { contentPadding ->
    TransformingLazyColumn(
        contentPadding = contentPadding,
        state = columnState,
    ) {
        // ...
        // ...
    }
}

তালিকায় সোয়াইপ করে দেখুন

SwipeToReveal কম্পোনেন্টটি আপনাকে সোয়াইপ করার মাধ্যমে তালিকার কোনো আইটেমের (যেমন Card বা Chip অ্যাকশনগুলো অ্যাক্সেস করতে দেয়। সোয়াইপ করলে সাধারণত পাশ থেকে এক বা দুটি অ্যাকশন বাটন (যেমন 'ডিলিট' বা 'মোর') দেখা যায়।

TransformingLazyColumn মধ্যে SwipeToReveal ব্যবহার করার সময়, এই নির্দেশিকাগুলো অনুসরণ করুন:

  • স্ক্রোল করলে রিসেট : যখন ব্যবহারকারী তালিকাটি স্ক্রোল করেন, তখন সোয়াইপ করে খোলা আইটেমগুলিকে তাদের ঢাকা অবস্থায় রিসেট করুন।
  • সামঞ্জস্যপূর্ণ উচ্চতা : একটি সামঞ্জস্যপূর্ণ চেহারা নিশ্চিত করতে, অ্যাকশন বাটনের উচ্চতা ভেতরের সোয়াইপ করা আইটেমের (সেটি Button হোক বা Card ) সাথে মিলিয়ে সেট করুন।
  • কন্টেইনারটি রূপান্তর করুন : transformedHeight মডিফায়ার এবং transformationSpec সরাসরি SwipeToReveal কম্পোনেন্টটিতে প্রয়োগ করুন।
  • দ্বৈত রূপান্তর করবেন না : ভেতরের সোয়াইপ করা আইটেমটিতে ( SwipeToReveal কন্টেইনারের ভেতরের কার্ড বা বাটন) transformedHeight বা transformation মডিফায়ার প্রয়োগ করবেন না।

তালিকায় কাস্টম কম্পোজেবল

TransformingLazyColumn এর জন্য কাস্টম সারফেস কম্পোনেন্ট তৈরি করার সময় এই সেরা অনুশীলনগুলি অনুসরণ করুন, যাতে আপনার কম্পোজেবলটি ডিসপ্লের প্রান্তের কাছাকাছি মসৃণভাবে স্কেল, ফেড এবং মর্ফ হয়:

  • SurfaceTransformation প্রকাশ করুন : একটি ঐচ্ছিক SurfaceTransformation প্যারামিটার গ্রহণ করে (যার ডিফল্ট মান null ), যা Card এবং Button মতো স্ট্যান্ডার্ড Wear Compose Material 3 কম্পোনেন্টগুলোর সাথে সামঞ্জস্যপূর্ণ। এটি TransformingLazyColumn ভেতরের কলারদের SurfaceTransformation(transformationSpec) পাস করার সুযোগ দেয়, এবং একই সাথে কম্পোনেন্টটিকে লিস্টের বাইরেও স্বাভাবিকভাবে কাজ করতে দেয়।
  • কলিং কোডে প্রথমে Modifier.transformedHeight প্রয়োগ করুন : যখন আপনার কাস্টম কম্পোজেবল একটি TransformingLazyColumn এ রাখবেন, তখন কলিং কোডের মডিফায়ার চেইনের প্রথম মডিফায়ার হিসেবে Modifier.transformedHeight(this, transformationSpec) পাস করুন। যদিও SurfaceTransformation ভিজ্যুয়াল স্কেলিং এবং ফেডিং এফেক্ট প্রয়োগ করে, আইটেমটি ছোট হওয়ার সাথে সাথে এর উচ্চতা পুনরায় গণনা করার জন্য লিস্ট লেআউটকে নির্দেশ দিতে transformedHeight অত্যন্ত গুরুত্বপূর্ণ।
  • ক্রমানুসারে ট্রান্সফরমেশন লেয়ার, কলার modifier এবং পেইন্টার প্রয়োগ করুন :
    1. কন্টেইনার ট্রান্সফরমেশন লেয়ার : রুট কন্টেইনারের মডিফায়ার চেইন Modifier.graphicsLayer দিয়ে শুরু করুন এবং applyContainerTransformation() , যাতে ব্যাকগ্রাউন্ড এবং কন্টেন্ট উভয়ই স্কেল করা ও হেলানো স্থানাঙ্ক পরিসরের মধ্যে অঙ্কিত হয়।
    2. কলার modifier : কলার দ্বারা প্রেরিত modifier প্যারামিটারটি (যার মধ্যে Modifier.transformedHeight অন্তর্ভুক্ত) যেকোনো অভ্যন্তরীণ সাইজিং বা প্যাডিংয়ের আগে প্রয়োগ করুন।
    3. ট্রান্সফরমেশন না থাকলে শেপ ক্লিপ : যদি transformation null হয়, তাহলে ব্যাকগ্রাউন্ড আঁকার আগে Modifier.clip(shape) প্রয়োগ করুন। createContainerPainter() দ্বারা রিটার্ন করা পেইন্টারটি নিজেকে শেপের সাথে ক্লিপ করে, কিন্তু একটি সাধারণ পেইন্টার তা করে না, তাই এটি ছাড়া ব্যাকগ্রাউন্ডটি একটি লিস্টের বাইরে বর্গাকার কোণাসহ আঁকা হয়।
    4. মর্ফিং ব্যাকগ্রাউন্ড পেইন্টার : Modifier.drawBehind এবং createContainerPainter() থেকে তৈরি একটি পেইন্টার ব্যবহার করে কন্টেইনার লেয়ারের ভিতরে ব্যাকগ্রাউন্ডটি আঁকুন।
    5. কন্টেন্ট ট্রান্সফরমেশন লেয়ার : applyContentTransformation() সহ একটি দ্বিতীয় Modifier.graphicsLayer প্রয়োগ করুন এবং কন্টেইনার শেপে ক্লিপ করুন, যাতে ভেতরের কন্টেন্ট বেজেলের কাছে আসার সাথে সাথে আগেভাগে ফেড আউট হয়ে যায়।

নিম্নলিখিত কোড স্নিপেটটি দেখায় কিভাবে একটি কাস্টম BoardingPassCard কম্পোজেবল প্রয়োগ করতে হয় যা এই রূপান্তরগুলি ক্রমানুসারে প্রয়োগ করে:

@Composable
fun BoardingPassCard(
    flightNumber: String,
    origin: String,
    destination: String,
    gate: String,
    seat: String,
    departureTime: String,
    modifier: Modifier = Modifier,
    transformation: SurfaceTransformation? = null,
    shape: Shape = RoundedCornerShape(18.dp),
    statusBadge: @Composable () -> Unit = {}
) {
    // 1. Create morphing container painter
    val backgroundPainter = ColorPainter(MaterialTheme.colorScheme.surfaceContainer)
    val finalPainter = if (transformation != null) {
        remember(transformation, backgroundPainter, shape) {
            transformation.createContainerPainter(backgroundPainter, shape, border = null)
        }
    } else {
        backgroundPainter
    }

    Column(
        modifier = Modifier
            // 2a. Container layer: Scales, fades, and tilts the whole card surface
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        transformation.run { applyContainerTransformation() }
                    }
                } else Modifier
            )
            // 2b. Caller modifier: Includes Modifier.transformedHeight in a list
            .then(modifier)
            .fillMaxWidth()
            // 2c. Shape clip: Only needed without a transformation, because the
            // painter from createContainerPainter clips itself to the shape
            .then(if (transformation == null) Modifier.clip(shape) else Modifier)
            // 2d. Morphing background: Drawn inside the transformed container layer
            .drawBehind {
                with(finalPainter) {
                    draw(size)
                }
            }
            // 2e. Content layer: Fades content earlier and clips children to shape
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        this.shape = shape
                        this.clip = true
                        transformation.run { applyContentTransformation() }
                    }
                } else Modifier
            )
            .padding(horizontal = 14.dp, vertical = 10.dp)
    ) {
        // Card content goes here
    }
}
চিত্র ৩। একটি তালিকা স্ক্রল করার সাথে সাথে রূপান্তরিত হতে থাকা একটি কাস্টম বোর্ডিং পাস।

এরপর আপনি প্রথম মডিফায়ার হিসেবে Modifier.transformedHeight এবং SurfaceTransformation(transformationSpec) পাস করে একটি TransformingLazyColumn ভিতরে BoardingPassCard ব্যবহার করতে পারেন:

@Composable
fun BoardingPassListSample(flights: List<FlightInfo>) {
    val listState = rememberTransformingLazyColumnState()
    val transformationSpec = rememberTransformationSpec()


    ScreenScaffold(scrollState = listState) { contentPadding ->
        TransformingLazyColumn(
            state = listState,
            contentPadding = contentPadding,
            modifier = Modifier.fillMaxSize()
        ) {
            items(flights.size) { index ->
                val flight = flights[index]
                BoardingPassCard(
                    flightNumber = flight.number,
                    origin = flight.origin,
                    destination = flight.destination,
                    gate = flight.gate,
                    seat = flight.seat,
                    departureTime = flight.time,
                    modifier = Modifier
                        .transformedHeight(this, transformationSpec)
                        .minimumVerticalContentPadding(
                            CardDefaults.minimumVerticalListContentPadding
                        ),
                    transformation = SurfaceTransformation(transformationSpec)
                )
            }
        }
    }
}
{% হুবহু %} {% endverbatim %} {% হুবহু %} {% endverbatim %}