Criar layouts roláveis para TV

Para apps de TV, a experiência de navegação depende de uma navegação eficiente baseada em foco. Usando layouts lentos padrão do Compose Foundation, é possível criar listas verticais e horizontais eficientes que processam automaticamente a rolagem orientada por foco para manter os itens ativos em exibição.

Comportamento de rolagem padrão otimizado para TV

A partir do Compose Foundation 1.7.0, layouts lentos padrão (como LazyRow e LazyColumn) incluem suporte integrado para recursos de posicionamento do foco. Essa é a maneira recomendada de criar catálogos para apps de TV, porque ajuda a manter os itens em foco visíveis e posicionados de forma intuitiva para o usuário.

Para implementar uma lista rolável básica, use os componentes lazy padrão. Esses componentes processam automaticamente a navegação com o botão direcional e trazem o item em foco para a tela.

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 */ }
            )
        }
    }
}

Personalizar o comportamento de rolagem com BringIntoViewSpec

Se o design exigir um ponto de "pivô" específico (por exemplo, manter o item em foco exatamente a 30% da borda esquerda), personalize o comportamento de rolagem usando um BringIntoViewSpec. Isso substitui a funcionalidade mais antiga pivotOffsets, permitindo definir exatamente como a janela de visualização deve rolar para acomodar um item em foco.

1. Definir um BringIntoViewSpec personalizado

O elemento combinável auxiliar a seguir permite definir um "pivô" com base em frações de elementos pai e filho. O parentFraction determina onde no contêiner o item deve ficar, e o childFraction determina qual parte do item se alinha com esse ponto.

@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. Aplicar a especificação personalizada

Encapsule seus layouts com o auxiliar para aplicar o posicionamento. Isso é útil para criar uma "linha de foco consistente" em diferentes linhas do seu 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. Desativar layouts aninhados específicos

Se você tiver um layout aninhado específico que precise usar o comportamento de rolagem padrão em vez do seu pivô personalizado, forneça o 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 { ... }
            }
        }
    }
}

Na prática, transmitir um BringIntoViewSpec vazio permite que o comportamento padrão do framework assuma o controle.

Migração da TV Foundation para a Compose Foundation

Os layouts lentos específicos para TV em androidx.tv.foundation foram descontinuados em favor dos layouts padrão do Compose Foundation.

Atualizações de dependências

Verifique se o build.gradle usa a versão 1.7.0 ou mais recente para:

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

Mapeamento de componentes

Para migrar, atualize as importações e remova o prefixo Tv dos componentes:

Componente de TV descontinuado Substituição do Compose Foundation
TvLazyRow LazyRow
TvLazyColumn LazyColumn
TvLazyHorizontalGrid LazyHorizontalGrid
TvLazyVerticalGrid LazyVerticalGrid
pivotOffsets BringIntoViewSpec (via LocalBringIntoViewSpec)