Cómo crear diseños desplazables para TVs

En el caso de las apps para TVs, la experiencia de navegación se basa en una navegación eficiente basada en el enfoque. Con los diseños perezosos estándar de Compose Foundation, puedes crear listas verticales y horizontales de alto rendimiento que controlan automáticamente el desplazamiento controlado por el enfoque para mantener los elementos activos a la vista.

Comportamiento de desplazamiento predeterminado optimizado para la TV

A partir de Compose Foundation 1.7.0, los diseños diferidos estándar (como LazyRow y LazyColumn) incluyen compatibilidad integrada con las funciones de posicionamiento del enfoque. Esta es la forma recomendada de crear catálogos para apps de TV, ya que ayuda a que los elementos enfocados permanezcan visibles y se posicionen de forma intuitiva para el usuario.

Para implementar una lista desplazable básica, usa los componentes lazy estándar. Estos componentes controlan automáticamente la navegación con el pad direccional y muestran el elemento enfocado.

import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items

@Composable
fun MovieCatalog(movies: List<Movie>) {
    LazyRow {
        items(movies) { movie ->
            MovieCard(
                movie = movie,
                onClick = { /* Handle click */ }
            )
        }
    }
}

Personaliza el comportamiento de desplazamiento con BringIntoViewSpec

Si tu diseño requiere un punto de "pivote" específico (por ejemplo, mantener el elemento enfocado exactamente a un 30% del borde izquierdo), puedes personalizar el comportamiento de desplazamiento con un BringIntoViewSpec. Esto reemplaza la funcionalidad anterior de pivotOffsets, ya que te permite definir exactamente cómo debe desplazarse el viewport para adaptarse a un elemento enfocado.

1. Cómo definir un BringIntoViewSpec personalizado

El siguiente elemento componible auxiliar te permite definir un "pivote" basado en fracciones secundarias y principales. El parentFraction determina en qué lugar del contenedor debe colocarse el elemento, y el childFraction determina qué parte del elemento se alinea con ese punto.

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PositionFocusedItemInLazyLayout(
    parentFraction: Float = 0.3f,
    childFraction: Float = 0f,
    content: @Composable () -> Unit,
) {
    val bringIntoViewSpec = remember(parentFraction, childFraction) {
        object : BringIntoViewSpec {
            override fun calculateScrollDistance(
                offset: Float,       // Item's initial position
                size: Float,         // Item's size
                containerSize: Float // Container's size
            ): Float {
                // Calculate the offset position of the item's leading edge.
                val initialTargetForLeadingEdge =
                    parentFraction * containerSize - (childFraction * size)
                // If the item fits in the container, and scrolling would cause
                // its trailing edge to be clipped, adjust targetForLeadingEdge
                // to prevent over-scrolling near the end of list.
                val targetForLeadingEdge = if (size <= containerSize &&
                    (containerSize - initialTargetForLeadingEdge) < size) {
                    // If clipped, align the item's trailing edge with the
                    // container's trailing edge.
                    containerSize - size
                } else {
                    initialTargetForLeadingEdge
                }
                // Return scroll distance relative to initial item position.
                return offset - targetForLeadingEdge
            }
        }
    }

    // Apply the spec to all scrollables in the hierarchy
    CompositionLocalProvider(
        LocalBringIntoViewSpec provides bringIntoViewSpec,
        content = content,
    )
}

2. Aplica la especificación personalizada

Encapsula tus diseños con el asistente para aplicar el posicionamiento. Esto es útil para crear una "línea de enfoque coherente" en diferentes filas de tu catálogo.

PositionFocusedItemInLazyLayout(
    parentFraction = 0.3f, // Pivot 30% from the edge
    childFraction = 0.5f   // Center of the item aligns with the pivot
) {
    LazyColumn {
        items(sectionList) { section ->
            // This row and its items will respect the 30% pivot
            LazyRow { ... }
        }
    }
}

3. Cómo inhabilitar diseños anidados específicos

Si tienes un diseño anidado específico que debería usar el comportamiento de desplazamiento estándar en lugar de tu pivote personalizado, proporciona DefaultBringIntoViewSpec:

private val DefaultBringIntoViewSpec = object : BringIntoViewSpec {}

PositionFocusedItemInLazyLayout {
    LazyColumn {
        item {
            // This row will ignore the custom pivot and use default behavior
            CompositionLocalProvider(LocalBringIntoViewSpec provides DefaultBringIntoViewSpec) {
                LazyRow { ... }
            }
        }
    }
}

En efecto, pasar un BringIntoViewSpec vacío permite que se haga cargo el comportamiento predeterminado del framework.

Migración de TV Foundation a Compose Foundation

Los diseños diferidos específicos para TVs en androidx.tv.foundation quedaron obsoletos en favor de los diseños estándar de Compose Foundation.

Actualizaciones de dependencias

Verifica que tu build.gradle use la versión 1.7.0 o una posterior para lo siguiente:

  • androidx.compose.foundation
  • androidx.compose.runtime

Asignación de componentes

Para migrar, actualiza tus importaciones y quita el prefijo Tv de tus componentes:

Componente de TV obsoleto Reemplazo de Compose Foundation
TvLazyRow LazyRow
TvLazyColumn LazyColumn
TvLazyHorizontalGrid LazyHorizontalGrid
TvLazyVerticalGrid LazyVerticalGrid
pivotOffsets BringIntoViewSpec (a través de LocalBringIntoViewSpec)