Listy umożliwiają użytkownikom wybieranie elementów z zestawu opcji na urządzeniach z Wear OS.
Wiele urządzeń z Wear OS ma okrągłe ekrany, co utrudnia wyświetlanie elementów listy w górnej i dolnej części ekranu. Z tego powodu Compose na Wear OS zawiera wersję klasy LazyColumn o nazwie TransformingLazyColumn, która obsługuje animacje skalowania i przekształcania.
Gdy elementy przesuwają się na krawędzie, zmniejszają się i znikają.
Aby zastosować zalecane efekty skalowania i przewijania:
- Użyj
Modifier.transformedHeight, aby funkcja Compose mogła obliczyć zmianę wysokości podczas przewijania elementu na ekranie. - Użyj
transformation = SurfaceTransformation(transformationSpec), aby zastosować efekty wizualne, w tym zmniejszyć zawartość elementu. - Użyj niestandardowego
TransformationSpecw przypadku komponentów, które nie przyjmujątransformationjako parametru, np.Text.
Poniższa animacja pokazuje, jak element listy skaluje się i zmienia kształt, gdy zbliża się do górnej i dolnej krawędzi ekranu:
Poniższy fragment kodu pokazuje, jak utworzyć listę za pomocą układuTransformingLazyColumn, aby utworzyć treści, które świetnie wyglądają na ekranach Wear OS o różnych rozmiarach.
Fragment kodu pokazuje też użycie modyfikatora minimumVerticalContentPadding, który należy ustawić w elementach listy, aby zastosować prawidłowe dopełnienie u góry i u dołu listy.
Aby wyświetlić wskaźnik przewijania, podziel columnState między ScreenScaffold a TransformingLazyColumn:
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, ) } } } }
Dodawanie efektu pstryknięcia i odrzucenia
Przyciąganie zapewnia, że gdy użytkownik zakończy przewijanie lub gest machnięcia, lista zatrzyma się z elementem umieszczonym dokładnie w określonym punkcie, zwykle na środku ekranu. Na okrągłych ekranach, na których elementy skalują się i przekształcają w miarę oddalania się od środka, przyciąganie jest szczególnie przydatne, ponieważ zapewnia, że najbardziej odpowiedni element pozostaje w pełni widoczny i czytelny w optymalnym obszarze wyświetlania.Aby dodać zachowanie przyciągania i przesuwania, ustaw parametr flingBehavior na wartość TransformingLazyColumnDefaults.snapFlingBehavior(columnState).
Ustaw rotaryScrollableBehavior tak, aby pasowało do RotaryScrollableDefaults.snapBehavior(columnState), aby zapewnić spójność podczas korzystania z fizycznego pokrętła lub ramki.
val columnState = rememberTransformingLazyColumnState() ScreenScaffold(scrollState = columnState) { contentPadding -> TransformingLazyColumn( state = columnState, flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState), rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState) ) { // ... // ... } }
Układ odwrócony
Domyślnie lista z możliwością przewijania jest zakotwiczona u góry. Jeśli użytkownik przewinie standardową listę do dołu i na końcu zostanie dodany nowy element, lista zachowa widok użytkownika na bieżący element. Jeśli na przykład użytkownik wyświetla element 10 u dołu ekranu i dodany zostanie element 11, widok pozostanie skupiony na elemencie 10, a element 11 pojawi się poza ekranem poniżej bieżącego widoku.
W przypadku aplikacji do przesyłania wiadomości lub dzienników na żywo takie działanie jest zwykle niepożądane. Gdy pojawią się nowe elementy, użytkownicy zwykle chcą od razu zobaczyć najnowsze treści, jeśli są już na dole listy. Jeśli wiele elementów pojawi się jednocześnie, lista powinna przejść do wyświetlania najnowszego elementu na dole (co oznacza, że niektóre elementy pośrednie mogą nie być w ogóle wyświetlane, chyba że użytkownik przewinie listę w górę).
Aby obsługiwać te przypadki użycia, TransformingLazyColumn umożliwia odwrócenie układu przez ustawienie reverseLayout = true. Spowoduje to zmianę punktu zakotwiczenia listy z górnej krawędzi na dolną.
Ustawienie reverseLayout = true odwraca też kolejność wizualną elementów i kierunek gestów przewijania:
- Elementy są ułożone od dołu do góry, co oznacza, że indeks 0 znajduje się u dołu ekranu.
- Przewijanie w górę powoduje wyświetlanie elementów o wyższych indeksach.
Aby dodać zachowanie przyciągania i przesuwania wraz z odwrotnym układem, możesz połączyć atrybuty flingBehavior i rotaryScrollableBehavior, jak pokazano w tym fragmencie kodu:
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") } } } }
Poniższe obrazy pokazują różnicę między zwykłą listą a listą odwróconą:
Przyciski na krawędziach list
W przypadku Material 3 możesz dodać EdgeButton, czyli przycisk przylegający do krawędzi u dołu listy. Nie dodawaj go jednak jako elementu w TransformingLazyColumn, ale użyj zamiast tego gniazda edgeButton w ScreenScaffold.
Użycie boksu edgeButton zapewnia prawidłowe umieszczenie przycisku u dołu ekranu i odpowiednie działanie podczas przewijania listy.
Poniższy fragment kodu pokazuje, jak wdrożyć 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 w przypadku list
Komponent SwipeToReveal umożliwia dostęp do działań dotyczących elementu listy, takich jak Card lub Chip, za pomocą przesunięcia. Przesunięcie zwykle powoduje wyświetlenie z boku 1 lub 2 przycisków działania (np. „Usuń” lub „Więcej”).
Podczas używania SwipeToReveal w TransformingLazyColumn postępuj zgodnie z tymi wskazówkami:
- Resetuj po przewinięciu: gdy użytkownik przewinie listę, zresetuj wszystkie otwarte elementy do stanu zakrytego.
- Spójna wysokość: ustaw wysokość przycisków polecenia tak, aby pasowała do wewnętrznego przesuniętego elementu (
ButtonlubCard), aby zapewnić spójny wygląd. - Przekształć kontener: zastosuj modyfikator
transformedHeightitransformationSpecdo samego komponentuSwipeToReveal. - Nie stosuj podwójnych przekształceń: nie stosuj modyfikatorów
transformedHeightanitransformationdo wewnętrznego elementu przesuwanego (karty lub przycisku wewnątrz konteneraSwipeToReveal).
Komponenty niestandardowe na listach
Podczas tworzenia niestandardowych komponentów powierzchni dla TransformingLazyColumn postępuj zgodnie z tymi sprawdzonymi metodami, aby komponent kompozycyjny płynnie skalował się, zanikał i przekształcał w pobliżu krawędzi wyświetlacza:
- Expose
SurfaceTransformation: akceptuje opcjonalny parametrSurfaceTransformation(domyślnienull), który pasuje do standardowych komponentów Wear Compose Material 3, takich jakCardiButton. Dzięki temu wywołujący mogą przekazaćTransformingLazyColumnSurfaceTransformation(transformationSpec), a komponent może działać normalnie poza listą. - Zastosuj
Modifier.transformedHeightjako pierwszy w kodzie wywołania: umieszczając własny komponent wTransformingLazyColumn, przekażModifier.transformedHeight(this, transformationSpec)jako pierwszy modyfikator w łańcuchu modyfikatorów kodu wywołania. Podczas gdySurfaceTransformationstosuje efekty wizualne skalowania i zanikania,transformedHeightjest niezbędny do poinformowania układu listy, aby ponownie obliczył wysokość elementu w miarę jego zmniejszania. - Zastosuj warstwy przekształceń, wywołującego
modifieri malującego w odpowiedniej kolejności:- Warstwa przekształcenia kontenera: rozpocznij łańcuch modyfikatorów kontenera głównego od
Modifier.graphicsLayeriapplyContainerTransformation(), aby zarówno tło, jak i treść były rysowane w skalowanej i pochylonej przestrzeni współrzędnych. - Wywołujący
modifier: zastosuj parametrmodifierprzekazany przez wywołującego (który zawieraModifier.transformedHeight) przed wewnętrznym określeniem rozmiaru lub dopełnieniem. - Kształtowanie klipu, gdy nie ma przekształcenia: jeśli
transformationma wartośćnull, zastosujModifier.clip(shape)przed narysowaniem tła. Obiekt zwracany przezcreateContainerPainter()przycina się do kształtu, ale zwykły obiekt nie, więc bez tego tło jest rysowane z kwadratowymi rogami poza listą. - Obiekt malujący tło z efektem morphingu: rysuje tło w warstwie kontenera za pomocą funkcji
Modifier.drawBehindi obiektu malującego utworzonego na podstawie funkcjicreateContainerPainter(). - Warstwa przekształcania treści: zastosuj drugą
Modifier.graphicsLayerzapplyContentTransformation()i przytnij do kształtu kontenera, aby wewnętrzne treści zanikały wcześniej, gdy zbliżają się do ramki.
- Warstwa przekształcenia kontenera: rozpocznij łańcuch modyfikatorów kontenera głównego od
Poniższy fragment kodu pokazuje, jak wdrożyć niestandardowy komponent BoardingPassCard, który stosuje te przekształcenia w podanej kolejności:
@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 } }
Następnie możesz użyć BoardingPassCard w TransformingLazyColumn, przekazując Modifier.transformedHeight jako pierwszy modyfikator wraz z 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) ) } } } }
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Ćwiczenia z Compose na Wear OS
- Listy i siatki
- Używanie widoków w Compose