Listas con Compose para Wear OS


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 desvanecen.

Para aplicar los efectos de escalamiento y desplazamiento recomendados, haz lo siguiente:

  1. Usa Modifier.transformedHeight para permitir que Compose calcule el cambio de altura a medida que el elemento se desplaza por la pantalla.
  2. Usa transformation = SurfaceTransformation(transformationSpec) para aplicar los efectos visuales, incluido el escalamiento del contenido del elemento.
  3. Usa un TransformationSpec personalizado para los componentes que no toman transformation como parámetro, como Text.

En la siguiente animación, se muestra cómo un elemento de la lista se escala y cambia de forma 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 TransformingLazyColumn diseño para crear contenido que se vea bien en una variedad de tamaños de pantalla de Wear OS.

El fragmento también muestra el uso del modificador minimumVerticalContentPadding, que debes configurar 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 ScreenScaffold y 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 termina un gesto de desplazamiento o deslizamiento, la lista se asiente con un elemento posicionado con precisión en un punto específico, por lo general, el centro de la pantalla. En pantallas redondas, donde los elementos se escalan 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 ajustar y deslizar, establece el parámetro flingBehavior en TransformingLazyColumnDefaults.snapFlingBehavior(columnState). Establece rotaryScrollableBehavior para que coincida con RotaryScrollableDefaults.snapBehavior(columnState) para una experiencia coherente cuando se usa la corona o el bisel físico.

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 ancla a su borde superior. Si un usuario se desplazó hasta la parte inferior 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 permanece enfocada en el elemento 10 y el elemento 11 aparece fuera de la pantalla debajo de la vista actual.

Para casos de uso como aplicaciones de mensajería o registros en vivo, por lo general, no se desea este comportamiento. 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 para mostrar el elemento más reciente en la parte inferior (lo que significa que es posible que no se muestren algunos elementos intermedios, a menos que el usuario vuelva a desplazarse hacia arriba).

Para admitir estos casos de uso, TransformingLazyColumn te permite invertir el diseño configurando reverseLayout = true. Esto cambia el anclaje de la lista del borde superior al borde inferior.

Para mayor comodidad, configurar 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.
  • Desplazarse hacia arriba revela elementos con índices más altos.

Para agregar un comportamiento de ajustar y deslizar 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 lista invertida:

Un TransformingLazyColumn con diseño normal, que muestra el elemento 1 en la parte superior y los elementos en orden ascendente.
Figura 1. Un diseño de lista estándar en el que el contenido se completa de arriba abajo
Un TransformingLazyColumn con diseño inverso, que muestra el elemento 1 en la parte inferior y los elementos en orden descendente hacia la parte superior.
Figura 2. Un diseño de lista invertida en el que el contenido se completa de abajo hacia arriba

Botones perimetrales en listas

Para Material 3, puedes agregar un EdgeButton, que es un botón perimetral en la parte inferior de las listas. Sin embargo, ten cuidado de no agregarlo como un elemento dentro de TransformingLazyColumn, sino de usar el espacio edgeButton en ScreenScaffold.

Usar el espacio edgeButton garantiza que el botón esté posicionado 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,
    ) {
        // ...
        // ...
    }
}

SwipeToReveal en listas

El componente SwipeToReveal te permite acceder a las acciones de un elemento de la lista, como un Card o un Chip, deslizando el dedo. Por lo general, al deslizar el dedo, se revelan uno o dos botones de acción (como "Borrar" o "Más") desde el costado.

Cuando uses SwipeToReveal dentro de un TransformingLazyColumn, sigue estas instrucciones:

  • Restablecer al desplazarse: Cuando el usuario desplaza la lista, restablece los elementos abiertos con el dedo 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 Button o una Card) para garantizar una apariencia coherente.
  • Transformar el contenedor: Aplica el modificador transformedHeight y transformationSpec al componente SwipeToReveal en sí.
  • No realizar una doble transformación: No apliques transformedHeight ni transformation modificadores al elemento interno deslizado (la tarjeta o el botón dentro del contenedor SwipeToReveal).