Wear OS-এর জন্য Compose-এর মাধ্যমে তালিকা


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) হিসেবে সেট করুন। ফিজিক্যাল ক্রাউন বা বেজেল ব্যবহার করার সময় RotaryScrollableDefaults.snapBehavior(columnState)-এর সাহায্যে ধারাবাহিক অভিজ্ঞতা পেতে rotaryScrollableBehavior সেট করুন।

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, যেখানে আইটেম ১ নিচে এবং উপরের দিকে আইটেমগুলি ডিসেন্ডিং অর্ডারে দেখানো হচ্ছে।
ছবি ২. উল্টো তালিকা লেআউট যেখানে কন্টেন্ট নিচ থেকে উপরের দিকে পূরণ হয়।

তালিকার এজ বোতাম

Material 3-এর ক্ষেত্রে, আপনি একটি 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

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

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

  • স্ক্রল করলে রিসেট করুন: ব্যবহারকারী তালিকা স্ক্রল করলে, সোয়াইপ করে খোলা আইটেমগুলি তাদের কভার করা অবস্থায় রিসেট করুন।
  • উচ্চতা একই রাখা: অ্যাকশন বোতামের উচ্চতা, সোয়াইপ করা আইটেমের (Button বা Card যাই হোক না কেন) ভিতরের উচ্চতার সাথে ম্যাচ করে সেট করুন, যাতে সবকটি বোতামের উচ্চতা একই থাকে।
  • কন্টেনার ট্রান্সফর্ম করা: SwipeToReveal কম্পোনেন্টে transformedHeight মডিফায়ার ও transformationSpec প্রয়োগ করুন।
  • ডবল ট্রান্সফর্ম করবেন না: ভেতরের সোয়াইপ করা আইটেমে (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.transformedHeight আছে) পাস করা modifier প্যারামিটার প্রয়োগ করুন।
    3. কোনও ট্রান্সফর্মেশন না থাকলে ক্লিপের আকার: transformation যদি null হয়, তাহলে ব্যাকগ্রাউন্ড আঁকার Modifier.clip(shape) আগে প্রয়োগ করুন। পেইন্টার createContainerPainter() ক্লিপ করে সেটিকে শেপে ফিরিয়ে আনে, কিন্তু সাধারণ পেইন্টার তা করে না, তাই এটি না থাকলে ব্যাকগ্রাউন্ডটি তালিকার বাইরে স্কোয়ার কোণ দিয়ে আঁকা হয়।
    4. মর্ফিং ব্যাকগ্রাউন্ড পেইন্টার: Modifier.drawBehind ব্যবহার করে কন্টেনার লেয়ারের ভিতরে ব্যাকগ্রাউন্ড আঁকুন এবং createContainerPainter() থেকে তৈরি পেইন্টার ব্যবহার করুন।
    5. কন্টেন্ট ট্রান্সফর্মেশন লেয়ার: কন্টেনার শেপে দ্বিতীয় Modifier.graphicsLayer সহ applyContentTransformation() ও ক্লিপ প্রয়োগ করুন যাতে বেজেলের কাছাকাছি পৌঁছানোর আগেই ভেতরের কন্টেন্ট ফেড-আউট হয়ে যায়।

নিচে দেওয়া স্নিপেট থেকে কীভাবে কাস্টম 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
    }
}
ছবি ৩. তালিকা স্ক্রল করার সময় কাস্টম বোর্ডিং পাস কম্পোজ করার মতো ট্রান্সফর্ম হচ্ছে।

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

@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)
                )
            }
        }
    }
}