Wear OS के लिए, Compose में मौजूद सूचियां


Wear OS डिवाइसों पर, सूचियों की मदद से उपयोगकर्ता विकल्पों के सेट में से कोई आइटम चुन सकते हैं.

Wear OS वाले कई डिवाइसों में गोल स्क्रीन होती हैं. इस वजह से, स्क्रीन के सबसे ऊपर और सबसे नीचे दिखने वाले आइटम को देखना मुश्किल हो जाता है. इस वजह से, Compose for Wear OS में LazyColumn क्लास का एक वर्शन शामिल है, जिसे TransformingLazyColumn कहा जाता है. यह स्केलिंग और मॉर्फ़िंग ऐनिमेशन के साथ काम करता है. जब आइटम किनारों पर पहुंचते हैं, तो वे छोटे हो जाते हैं और धुंधले दिखने लगते हैं.

सुझाए गए स्केलिंग और स्क्रोलिंग इफ़ेक्ट लागू करने के लिए:

  1. Modifier.transformedHeight का इस्तेमाल करें, ताकि कंपोज़र, स्क्रीन पर आइटम स्क्रोल होने पर ऊंचाई में हुए बदलाव का हिसाब लगा सके.
  2. transformation = SurfaceTransformation(transformationSpec) का इस्तेमाल करके, विज़ुअल इफ़ेक्ट लागू करें. इसमें आइटम के कॉन्टेंट को छोटा करना भी शामिल है.
  3. उन कॉम्पोनेंट के लिए कस्टम TransformationSpec का इस्तेमाल करें जो transformation को पैरामीटर के तौर पर नहीं लेते हैं. जैसे, Text.

नीचे दिए गए ऐनिमेशन में दिखाया गया है कि स्क्रीन के सबसे ऊपर और सबसे नीचे पहुंचने पर, सूची का एलिमेंट कैसे स्केल होता है और उसका आकार कैसे बदलता है:

यहां दिए गए कोड स्निपेट में, 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)
    ) {
        // ...
        // ...
    }
}

रिवर्स लेआउट

डिफ़ॉल्ट रूप से, स्क्रोल की जा सकने वाली सूची, अपने सबसे ऊपरी किनारे पर ऐंकर होती है. अगर कोई उपयोगकर्ता किसी स्टैंडर्ड सूची के सबसे नीचे तक स्क्रोल करता है और सूची के आखिर में कोई नया आइटम जोड़ा जाता है, तो सूची में उपयोगकर्ता को मौजूदा आइटम दिखता रहता है. उदाहरण के लिए, अगर उपयोगकर्ता स्क्रीन पर सबसे नीचे मौजूद आइटम 10 को देख रहा है और आइटम 11 को जोड़ा जाता है, तो व्यू का फ़ोकस आइटम 10 पर ही बना रहता है. साथ ही, आइटम 11, मौजूदा व्यू के नीचे स्क्रीन से बाहर दिखता है.

मैसेजिंग ऐप्लिकेशन या लाइव लॉग जैसे इस्तेमाल के उदाहरणों के लिए, आम तौर पर इस व्यवहार की ज़रूरत नहीं होती. जब नए आइटम आते हैं, तो आम तौर पर उपयोगकर्ता सबसे नीचे मौजूद सूची में, तुरंत नया कॉन्टेंट देखना चाहते हैं. अगर कई आइटम एक साथ पहुंचते हैं, तो सूची में सबसे नया आइटम सबसे नीचे दिखना चाहिए. इसका मतलब है कि कुछ आइटम तब तक नहीं दिखेंगे, जब तक उपयोगकर्ता वापस ऊपर की ओर स्क्रोल नहीं करता.

इन इस्तेमाल के उदाहरणों के लिए, TransformingLazyColumn आपको reverseLayout = true सेट करके लेआउट को उलटने की सुविधा देता है. इससे सूची का ऐंकर, सबसे ऊपर वाले किनारे से बदलकर सबसे नीचे वाला किनारा हो जाता है.

आसानी के लिए, reverseLayout = true सेटिंग से आइटम का विज़ुअल क्रम और स्क्रोल करने के जेस्चर की दिशा भी उलट जाती है:

  • आइटम, नीचे से ऊपर की ओर व्यवस्थित किए जाते हैं. इसका मतलब है कि इंडेक्स 0, स्क्रीन पर सबसे नीचे दिखता है.
  • ऊपर की ओर स्क्रोल करने पर, ज़्यादा इंडेक्स वाले आइटम दिखते हैं.

स्नैप-एंड-फ़्लिंग के साथ-साथ रिवर्स लेआउट जोड़ने के लिए, 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, जिसमें आइटम 1 सबसे ऊपर दिख रहा है और आइटम बढ़ते क्रम में दिख रहे हैं.
पहली इमेज. यह एक स्टैंडर्ड लिस्ट लेआउट है, जिसमें कॉन्टेंट ऊपर से नीचे तक दिखता है.
रिवर्स लेआउट वाला TransformingLazyColumn, जिसमें आइटम 1 सबसे नीचे और बाकी आइटम ऊपर की ओर घटते क्रम में दिख रहे हैं.
दूसरी इमेज. उल्टी सूची वाला लेआउट, जिसमें कॉन्टेंट नीचे से ऊपर की ओर दिखता है.

सूचियों पर मौजूद किनारे वाले बटन

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,
    ) {
        // ...
        // ...
    }
}

Lists में SwipeToReveal

SwipeToReveal कॉम्पोनेंट की मदद से, स्वाइप करके सूची के किसी आइटम से जुड़ी कार्रवाइयां ऐक्सेस की जा सकती हैं. जैसे, Card या Chip. आम तौर पर, स्वाइप करने पर साइड से एक या दो ऐक्शन बटन (जैसे, "मिटाएं" या "ज़्यादा") दिखते हैं.

TransformingLazyColumn में SwipeToReveal का इस्तेमाल करते समय, इन दिशा-निर्देशों का पालन करें:

  • स्क्रोल करने पर रीसेट करें: जब उपयोगकर्ता सूची को स्क्रोल करता है, तो स्वाइप करके खोले गए सभी आइटम को उनकी मूल स्थिति में रीसेट करें.
  • ऊंचाई एक जैसी होना: ऐक्शन बटन की ऊंचाई को स्वाइप किए गए आइटम (चाहे वह Button हो या Card) की ऊंचाई के बराबर सेट करें, ताकि वे एक जैसे दिखें.
  • कंटेनर को ट्रांसफ़ॉर्म करना: SwipeToReveal कॉम्पोनेंट पर transformedHeight मॉडिफ़ायर और transformationSpec लागू करें.
  • दो बार ट्रांसफ़ॉर्म न करें: स्वाइप किए गए अंदरूनी आइटम (SwipeToReveal कंटेनर के अंदर मौजूद कार्ड या बटन) पर transformedHeight या transformation मॉडिफ़ायर लागू न करें.

सूचियों में कस्टम कंपोज़ेबल

TransformingLazyColumn के लिए कस्टम सर्फ़ेस कॉम्पोनेंट बनाते समय, इन सबसे सही तरीकों का पालन करें. इससे आपके कंपोज़ेबल, डिसप्ले के किनारों के पास आसानी से स्केल, फ़ेड, और मॉर्फ़ हो पाएंगे:

  • Expose 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. Morphing background painter: 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 का इस्तेमाल किया जा सकता है. इसके लिए, SurfaceTransformation(transformationSpec) के साथ-साथ Modifier.transformedHeight को पहले मॉडिफ़ायर के तौर पर पास करें:

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