Wear OS için Compose içeren listeler


Listeler, kullanıcıların Wear OS cihazlarda bir dizi seçenek arasından öğe seçmesine olanak tanır.

Birçok Wear OS cihazda yuvarlak ekran kullanıldığından ekranın üst ve alt kısımlarında görünen liste öğelerini görmek daha zordur. Bu nedenle, Wear OS için Oluşturma'da LazyColumn sınıfının TransformingLazyColumn adlı bir sürümü bulunur. Bu sürüm, ölçeklendirme ve şekil değiştirme animasyonlarını destekler. Kenarlara doğru hareket eden öğeler küçülür ve soluklaşır.

Önerilen ölçeklendirme ve kaydırma efektlerini uygulamak için:

  1. Öğe ekranda kaydırılırken Oluştur'un yükseklik değişikliğini hesaplamasına izin vermek için Modifier.transformedHeight kullanın.
  2. Öğe içeriklerini küçültme de dahil olmak üzere görsel efektleri uygulamak için transformation = SurfaceTransformation(transformationSpec) simgesini kullanın.
  3. transformation parametresini almayan bileşenler (ör. Text) için özel bir TransformationSpec kullanın.

Aşağıdaki animasyonda, bir liste öğesinin ekranın üst ve alt kısımlarına yaklaştığında nasıl ölçeklendiği ve şekil değiştirdiği gösterilmektedir:

Aşağıdaki kod snippet'inde, TransformingLazyColumn düzenini kullanarak çeşitli Wear OS ekran boyutlarında harika görünen içerikler oluşturmak için nasıl liste oluşturulacağı gösterilmektedir.

Snippet'te, listenin üst ve alt kısmında doğru dolguyu uygulamak için liste öğelerinde ayarlamanız gereken minimumVerticalContentPadding değiştiricisinin kullanımı da gösterilmektedir.

Kaydırma göstergesini göstermek için ScreenScaffold ile TransformingLazyColumn arasındaki columnState değerini paylaşın:

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

Snap-and-fling efekti ekleme

Sabitleme, kullanıcının kaydırma veya hızlıca kaydırma hareketini tamamladığında listenin, genellikle ekranın ortası olmak üzere belirli bir noktada tam olarak konumlandırılmış bir öğeyle yerleşmesini sağlar. Öğelerin merkezden uzaklaştıkça ölçeklendiği ve şekil değiştirdiği yuvarlak ekranlarda, en alakalı öğenin en iyi görüntüleme alanında tamamen görünür ve okunabilir kalmasını sağlamak için tutturma özelliği özellikle kullanışlıdır.

Bir yapıştır ve fırlat davranışı eklemek için flingBehavior parametresini TransformingLazyColumnDefaults.snapFlingBehavior(columnState) olarak ayarlayın. Fiziksel ayar düğmesi veya çerçeve kullanılırken tutarlı bir deneyim için RotaryScrollableDefaults.snapBehavior(columnState) simgesini kullanarak rotaryScrollableBehavior simgesini eşleşecek şekilde ayarlayın.

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

Ters düzen

Kaydırılabilir bir liste varsayılan olarak üst kenarına sabitlenir. Bir kullanıcı standart bir listenin en altına kaydırdıktan sonra listenin sonuna yeni bir öğe eklenirse liste, kullanıcının mevcut öğe görünümünü korur. Örneğin, kullanıcı ekranın alt kısmındaki 10. öğeyi görüntülerken 11. öğe eklenirse görünüm 10. öğeye odaklanmaya devam eder ve 11. öğe, geçerli görünümün altında ekran dışında görünür.

Mesajlaşma uygulamaları veya canlı günlükler gibi kullanım alanlarında bu davranış genellikle istenmez. Yeni öğeler geldiğinde, listenin en altında olan kullanıcılar genellikle en son içeriği hemen görmek ister. Çok sayıda öğe aynı anda gelirse listede en son öğe en altta gösterilir (yani kullanıcı yukarı kaydırmadığı sürece bazı ara öğeler hiç gösterilmeyebilir).

Bu kullanım alanlarını desteklemek için TransformingLazyColumn, reverseLayout = true ayarını yaparak düzeni tersine çevirmenize olanak tanır. Bu işlem, listenin bağlantı noktasını üst kenardan alt kenara değiştirir.

Kolaylık sağlamak için reverseLayout = true ayarı, öğelerin görsel sırasını ve kaydırma hareketlerinin yönünü de tersine çevirir:

  • Öğeler alttan üste doğru oluşturulur. Yani 0 dizini ekranın en altında görünür.
  • Yukarı kaydırdığınızda dizinleri daha yüksek olan öğeler gösterilir.

Ters düzenle birlikte tutturup fırlatma davranışı eklemek için aşağıdaki snippet'te gösterildiği gibi flingBehavior ve rotaryScrollableBehavior öğelerini birleştirebilirsiniz:

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

Aşağıdaki resimlerde normal liste ile ters liste arasındaki fark gösterilmektedir:

Normal düzene sahip bir TransformingLazyColumn. En üstte 1. öğe, diğer öğeler ise artan sırada gösteriliyor.
Şekil 1. İçeriğin yukarıdan aşağıya doğru doldurulduğu standart liste düzeni.
Ters düzene sahip bir TransformingLazyColumn. Item 1 en altta, diğer öğeler ise yukarıya doğru azalan sırada gösteriliyor.
Şekil 2. İçeriğin alttan üste doğru doldurulduğu ters liste düzeni.

Listelerdeki kenar düğmeleri

Material 3'te, listelerin en altında kenarlara yaslanan bir düğme olan EdgeButton ekleyebilirsiniz. Ancak bunu TransformingLazyColumn içinde bir öğe olarak eklememeye dikkat edin. Bunun yerine ScreenScaffold içindeki edgeButton yuvasını kullanın.

edgeButton yuvasının kullanılması, düğmenin ekranın alt kısmında doğru şekilde konumlandırılmasını ve liste kaydırıldığında uygun şekilde davranmasını sağlar.

Aşağıdaki kod snippet'inde EdgeButton'nin nasıl uygulanacağı gösterilmektedir:

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

Listelerde SwipeToReveal

SwipeToReveal bileşeni, kaydırarak liste öğesiyle ilgili işlemlere (ör. Card veya Chip) erişmenizi sağlar. Kaydırma işlemi genellikle yandan bir veya iki işlem düğmesini (ör. "Sil" veya "Diğer") gösterir.

SwipeToReveal kodunu TransformingLazyColumn içinde kullanırken aşağıdaki yönergelere uyun:

  • Kaydırma sırasında sıfırla: Kullanıcı listede kaydırdığında, kaydırılarak açılan öğeler kapalı durumlarına sıfırlanır.
  • Tutarlı yükseklikler: Tutarlı bir görünüm sağlamak için işlem düğmesi yüksekliklerini, içteki kaydırılan öğeyle (Button veya Card) eşleşecek şekilde ayarlayın.
  • Kapsayıcıyı dönüştürme: transformedHeight değiştiricisini ve transformationSpec değerini SwipeToReveal bileşenine uygulayın.
  • Çift dönüştürme yapmayın: İçteki kaydırılan öğeye (SwipeToReveal kapsayıcısının içindeki kart veya düğme) transformedHeight ya da transformation değiştiricilerini uygulamayın.

Listelerde özel composable'lar

TransformingLazyColumn için özel yüzey bileşenleri oluştururken, composable'ınızın ekran kenarlarında sorunsuz bir şekilde ölçeklenmesi, solması ve dönüşmesi için aşağıdaki en iyi uygulamaları izleyin:

  • Expose SurfaceTransformation: Card ve Button gibi standart Wear Compose Material 3 bileşenleriyle eşleşen, isteğe bağlı bir SurfaceTransformation parametresini (varsayılan olarak null) kabul edin. Bu, bileşenin bir liste dışında normal şekilde çalışmasına izin verirken arayanların TransformingLazyColumn geçmesine SurfaceTransformation(transformationSpec) olanak tanır.
  • Arama kodunda önce Modifier.transformedHeight uygulayın: Özel composable'ınızı TransformingLazyColumn içine yerleştirirken Modifier.transformedHeight(this, transformationSpec) öğesini, arama kodunun değiştirici zincirindeki ilk değiştirici olarak iletin. SurfaceTransformation görsel ölçeklendirme ve solma efektlerini uygularken transformedHeight, liste düzenine öğenin yüksekliğini küçüldükçe yeniden hesaplamasını söylemek için kritik öneme sahiptir.
  • Dönüşüm katmanlarını, arayan modifier ve boyayıcıyı sırayla uygulayın:
    1. Kapsayıcı dönüştürme katmanı: Hem arka planın hem de içeriğin ölçeklendirilmiş, eğilmiş koordinat alanında çizilmesi için kök kapsayıcının değiştirici zincirini Modifier.graphicsLayer ve applyContainerTransformation() ile başlatın.
    2. Arayan modifier: Arayan tarafından iletilen modifier parametresini (Modifier.transformedHeight dahil) önce, herhangi bir dahili boyutlandırma veya dolgudan önce uygulayın.
    3. Dönüşüm olmadığında klibi şekillendirme: transformation null ise arka planı çizmeden önce Modifier.clip(shape) uygulayın. createContainerPainter() tarafından döndürülen boya, şekle göre kırpılır ancak düz bir boya kırpılmaz. Bu nedenle, bu olmadan arka plan, liste dışında kare köşelerle çizilir.
    4. Dönüşen arka plan boyama: Modifier.drawBehind ve createContainerPainter() kullanılarak oluşturulan bir boyama aracıyla arka planı kapsayıcı katmanının içine çizin.
    5. İçerik dönüştürme katmanı: İçerik çerçeveye yaklaştıkça daha erken solması için kapsayıcı şekle ikinci bir Modifier.graphicsLayer ile applyContentTransformation() ve kırpma uygulayın.

Aşağıdaki snippet'te, bu dönüşümleri sırayla uygulayan özel bir BoardingPassCard composable'ın nasıl uygulanacağı gösterilmektedir:

@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
    }
}
Şekil 3. Bir listede gezinirken dönüşen özel biniş kartı composable'ı.

Ardından, BoardingPassCard öğesini TransformingLazyColumn içinde kullanabilirsiniz. Bunun için Modifier.transformedHeight öğesini ilk değiştirici olarak SurfaceTransformation(transformationSpec) ile birlikte iletmeniz gerekir:

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