Las listas les permiten a los usuarios seleccionar un elemento de un conjunto de opciones en dispositivos Wear OS.
Muchos dispositivos Wear OS usan pantallas redondas, lo que dificulta la visualización de los elementos de la lista que aparecen cerca de la parte inferior y superior de la pantalla. Por este motivo, Compose para Wear OS incluye una versión de la clase LazyColumn llamada TransformingLazyColumn, que admite animaciones de escalamiento y transformación.
Cuando los elementos se mueven hacia los bordes, se hacen más pequeños y se atenúan.
Para aplicar los efectos de desplazamiento y escalamiento recomendados, sigue estos pasos:
- Usa
Modifier.transformedHeightpara permitir que Compose calcule el cambio de altura a medida que el elemento se desplaza por la pantalla. - Usa
transformation = SurfaceTransformation(transformationSpec)para aplicar los efectos visuales, incluido el ajuste del tamaño del contenido del elemento. - Usa un
TransformationSpecpersonalizado para los componentes que no tomantransformationcomo parámetro, comoText.
En la siguiente animación, se muestra cómo un elemento de lista cambia de forma y escala cuando se acerca a la parte superior e inferior de la pantalla:
En el siguiente fragmento de código, se muestra cómo crear una lista con el diseñoTransformingLazyColumn para crear contenido que se vea excelente en una variedad de tamaños de pantalla de Wear OS.
El fragmento también demuestra el uso del modificador minimumVerticalContentPadding, que debes establecer en los elementos de la lista para aplicar el padding correcto en la parte superior e inferior de la lista.
Para mostrar el indicador de desplazamiento, comparte el columnState entre el ScreenScaffold y el 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, ) } } } }
Cómo agregar un efecto de ajustar y deslizar.
El ajuste garantiza que, cuando un usuario finaliza un gesto de desplazamiento o deslizamiento, la lista se detiene con un elemento posicionado con precisión en un punto específico, por lo general, el centro de la pantalla. En las pantallas redondas, en las que los elementos se ajustan y transforman a medida que se alejan del centro, el ajuste es particularmente útil para garantizar que el elemento más relevante permanezca completamente visible y legible en el área de visualización óptima.Para agregar un comportamiento de ajuste y desplazamiento, establece el parámetro flingBehavior en TransformingLazyColumnDefaults.snapFlingBehavior(columnState).
Configura el rotaryScrollableBehavior para que coincida con el uso de RotaryScrollableDefaults.snapBehavior(columnState) para una experiencia coherente cuando uses la corona o el bisel físicos.
val columnState = rememberTransformingLazyColumnState() ScreenScaffold(scrollState = columnState) { contentPadding -> TransformingLazyColumn( state = columnState, flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState), rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState) ) { // ... // ... } }
Diseño inverso
De forma predeterminada, una lista desplazable se fija a su borde superior. Si un usuario se desplazó hasta el final de una lista estándar y se agrega un elemento nuevo al final, la lista mantiene la vista del usuario en el elemento actual. Por ejemplo, si el usuario está viendo el elemento 10 en la parte inferior de la pantalla y se agrega el elemento 11, la vista sigue enfocada en el elemento 10, y el elemento 11 aparece fuera de la pantalla debajo de la vista actual.
En casos de uso como aplicaciones de mensajería o registros en vivo, este comportamiento no suele ser deseable. Cuando llegan elementos nuevos, los usuarios suelen querer ver el contenido más reciente de inmediato si ya están en la parte inferior de la lista. Si llegan muchos elementos a la vez, la lista debe omitir algunos para mostrar el más reciente en la parte inferior (es decir, es posible que no se muestren algunos elementos intermedios a menos que el usuario se desplace hacia arriba).
Para admitir estos casos de uso, TransformingLazyColumn te permite invertir el diseño estableciendo reverseLayout = true. Esto cambia el ancla de la lista del borde superior al borde inferior.
Para mayor comodidad, establecer reverseLayout = true también invierte el orden visual de los elementos y la dirección de los gestos de desplazamiento:
- Los elementos se componen de abajo hacia arriba, lo que significa que el índice 0 aparece en la parte inferior de la pantalla.
- Si te desplazas hacia arriba, se revelan elementos con índices más altos.
Para agregar un comportamiento de ajuste y desplazamiento junto con el diseño inverso, puedes combinar flingBehavior y rotaryScrollableBehavior, como se muestra en el siguiente fragmento:
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") } } } }
En las siguientes imágenes, se muestra la diferencia entre una lista normal y una invertida:

Botones de borde en las listas
En Material 3, puedes agregar un EdgeButton, que es un botón que se ajusta al borde en la parte inferior de las listas. Sin embargo, ten cuidado de no agregar esto como un elemento dentro de TransformingLazyColumn, sino de usar la ranura edgeButton en ScreenScaffold.
Usar la ranura edgeButton garantiza que el botón se posicione correctamente en la parte inferior de la pantalla y se comporte de manera adecuada cuando se desplaza la lista.
En el siguiente fragmento de código, se muestra cómo implementar 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, ) { // ... // ... } }
Deslizar para revelar en Listas
El componente SwipeToReveal te permite acceder a acciones para un elemento de lista, como Card o Chip, con un deslizamiento. Por lo general, deslizar el dedo revela uno o dos botones de acción (como “Borrar” o “Más”) desde el costado.
Cuando uses SwipeToReveal dentro de un TransformingLazyColumn, sigue estos lineamientos:
- Restablecer al desplazarse: Cuando el usuario se desplaza por la lista, restablece los elementos abiertos con un deslizamiento a su estado cubierto.
- Alturas coherentes: Establece las alturas de los botones de acción para que coincidan con el elemento interno deslizado (ya sea un
Buttono unCard) y garantizar una apariencia coherente. - Transforma el contenedor: Aplica el modificador
transformedHeightytransformationSpecal componenteSwipeToReveal. - No apliques una doble transformación: No apliques modificadores
transformedHeightnitransformational elemento interno deslizado (la tarjeta o el botón dentro del contenedorSwipeToReveal).
Elementos componibles personalizados en listas
Cuando compiles componentes de superficie personalizados para un TransformingLazyColumn, sigue estas prácticas recomendadas para que tu elemento componible se ajuste, se desvanezca y se transforme sin problemas cerca de los bordes de la pantalla:
- Exponer
SurfaceTransformation: Acepta un parámetroSurfaceTransformationopcional (con el valor predeterminadonull) que coincide con los componentes estándar de Wear Compose Material 3, comoCardyButton. Esto permite que las personas que llaman en un pase deTransformingLazyColumnSurfaceTransformation(transformationSpec)y, al mismo tiempo, permite que el componente funcione normalmente fuera de una lista. - Aplica
Modifier.transformedHeightprimero en el código de llamada: Cuando coloques tu elemento componible personalizado en unTransformingLazyColumn, pasaModifier.transformedHeight(this, transformationSpec)como el primer modificador en la cadena de modificadores del código de llamada. Si bienSurfaceTransformationaplica los efectos visuales de ajuste y atenuación,transformedHeightes fundamental para indicarle al diseño de la lista que vuelva a calcular la altura del elemento a medida que se reduce. - Aplica las capas de transformación, el llamador
modifiery el pintor en orden:- Capa de transformación del contenedor: Inicia la cadena de modificadores del contenedor raíz con
Modifier.graphicsLayeryapplyContainerTransformation(), de modo que el fondo y el contenido se dibujen dentro del espacio de coordenadas inclinado y escalado. - Llamador
modifier: Aplica el parámetromodifierque pasa el llamador (que incluyeModifier.transformedHeight) a continuación, antes de cualquier relleno o tamaño interno. - Recorte de forma cuando no hay transformación: Si
transformationesnull, aplicaModifier.clip(shape)antes de dibujar el fondo. El objeto Painter que devuelvecreateContainerPainter()se recorta a la forma, pero un objeto Painter simple no lo hace, por lo que, sin esto, el fondo se dibuja con esquinas cuadradas fuera de una lista. - Pintor de fondo de transformación: Dibuja el fondo dentro de la capa del contenedor con
Modifier.drawBehindy un pintor creado a partir decreateContainerPainter(). - Capa de transformación de contenido: Aplica un segundo
Modifier.graphicsLayerconapplyContentTransformation()y recorta el contenido según la forma del contenedor para que el contenido interno se desvanezca antes a medida que se acerca al bisel.
- Capa de transformación del contenedor: Inicia la cadena de modificadores del contenedor raíz con
En el siguiente fragmento, se muestra cómo implementar un elemento BoardingPassCard personalizado componible que aplica estas transformaciones en orden:
@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 } }
Luego, puedes usar BoardingPassCard dentro de un TransformingLazyColumn pasando Modifier.transformedHeight como el primer modificador, junto con 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) ) } } } }
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Codelab de Compose para Wear OS
- Listas y cuadrículas
- Cómo usar Views en Compose