Pager no Compose

Para navegar pelo conteúdo horizontal ou verticalmente, use os elementos combináveis HorizontalPager e VerticalPager. Eles têm funções semelhantes a ViewPager no sistema de visualização. Por padrão, HorizontalPager ocupa toda a largura da tela, e VerticalPager ocupa toda a altura. Os pagers também só mostram uma página por vez. Todos esses padrões são configuráveis.

HorizontalPager

Para criar um pager que rola horizontalmente para a esquerda e para a direita, use HorizontalPager:

Figura 1. Demonstração de HorizontalPager

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

VerticalPager

Para criar um pager que rola para cima e para baixo, use VerticalPager:

Figura 2. Demonstração de VerticalPager

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
VerticalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

Criação lenta

As páginas em HorizontalPager e VerticalPager são compostas de forma lenta e dispostas quando necessário. Conforme o usuário rola as páginas, o elemento que pode ser composto remove as que não são mais necessárias.

Carregar mais páginas fora da tela

Por padrão, o pager só carrega as páginas visíveis na tela. Para carregar mais páginas fora da tela, defina beyondBoundsPageCount como um valor maior que zero.

Rolar até um item no pager

Para rolar até uma determinada página no pager, crie um objeto PagerState usando rememberPagerState() e transmita-o como o parâmetro state para o pager. É possível chamar PagerState#scrollToPage() nesse estado, dentro de um CoroutineScope:

val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.scrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

Se você quiser animar para a página, use a função PagerState#animateScrollToPage():

val pagerState = rememberPagerState(pageCount = {
    10
})

HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.animateScrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

Receber notificações sobre mudanças no estado da página

PagerState tem três propriedades com informações sobre páginas: currentPage, settledPage e targetPage.

  • currentPage: a página mais próxima da posição de ajuste. Por padrão, a posição de ajuste fica no início do layout.
  • settledPage: o número da página quando nenhuma animação ou rolagem está em execução. Isso é diferente da propriedade currentPage, em que o currentPage é atualizado imediatamente se a página estiver perto o suficiente da posição de ajuste, mas settledPage permanece o mesmo até que todas as animações terminem de ser executadas.
  • targetPage: a posição de parada proposta para um movimento de rolagem.

Use a função snapshotFlow para observar as mudanças nessas variáveis e reagir a elas. Por exemplo, para enviar um evento de análise em cada mudança de página, faça o seguinte:

val pagerState = rememberPagerState(pageCount = {
    10
})

LaunchedEffect(pagerState) {
    // Collect from the a snapshotFlow reading the currentPage
    snapshotFlow { pagerState.currentPage }.collect { page ->
        // Do something with each page change, for example:
        // viewModel.sendPageSelectedEvent(page)
        Log.d("Page change", "Page changed to $page")
    }
}

VerticalPager(
    state = pagerState,
) { page ->
    Text(text = "Page: $page")
}

Adicionar um indicador de página

Para adicionar um indicador a uma página, use o objeto PagerState para receber informações sobre qual página está selecionada entre o número de páginas e desenhe o indicador personalizado.

Por exemplo, para criar um indicador de círculo, você pode repetir o número de círculos e mudar a cor deles com base na seleção da página, usando pagerState.currentPage:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    modifier = Modifier.fillMaxSize()
) { page ->
    // Our page content
    Text(
        text = "Page: $page",
    )
}
Row(
    Modifier
        .wrapContentHeight()
        .fillMaxWidth()
        .align(Alignment.BottomCenter)
        .padding(bottom = 8.dp),
    horizontalArrangement = Arrangement.Center
) {
    repeat(pagerState.pageCount) { iteration ->
        val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
        Box(
            modifier = Modifier
                .padding(2.dp)
                .clip(CircleShape)
                .background(color)
                .size(16.dp)
        )
    }
}

Pager mostrando um indicador de círculo abaixo do conteúdo
Figura 3. Pager mostrando um indicador de círculo abaixo do conteúdo

Aplicar efeitos de rolagem de itens ao conteúdo

Um caso de uso comum é usar a posição de rolagem para aplicar efeitos aos itens do pager. Para descobrir a distância de uma página em relação à página selecionada, use PagerState.currentPageOffsetFraction. Em seguida, é possível aplicar efeitos de transformação ao conteúdo com base na distância da página selecionada.

Figura 4. Como aplicar transformações ao conteúdo do Pager

Por exemplo, para ajustar a opacidade dos itens com base na distância do centro, mude o alpha usando Modifier.graphicsLayer em um item dentro do pager:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(state = pagerState) { page ->
    Card(
        Modifier
            .size(200.dp)
            .graphicsLayer {
                // Calculate the absolute offset for the current page from the
                // scroll position. We use the absolute value which allows us to mirror
                // any effects for both directions
                val pageOffset = (
                    (pagerState.currentPage - page) + pagerState
                        .currentPageOffsetFraction
                    ).absoluteValue

                // We animate the alpha, between 50% and 100%
                alpha = lerp(
                    start = 0.5f,
                    stop = 1f,
                    fraction = 1f - pageOffset.coerceIn(0f, 1f)
                )
            }
    ) {
        // Card content
    }
}

Tamanhos de página personalizados

Por padrão, HorizontalPager e VerticalPager ocupam toda a largura ou altura, respectivamente. É possível definir a variável pageSize para ter um Fixed, Fill (padrão) ou um cálculo de tamanho personalizado.

Por exemplo, para definir uma página de largura fixa de 100.dp:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp)
) { page ->
    // page content
}

Para dimensionar as páginas com base no tamanho da janela de visualização, use um cálculo personalizado de tamanho de página. Crie um objeto PageSize personalizado e divida o availableSpace por três, considerando o espaçamento entre os itens:

private val threePagesPerViewport = object : PageSize {
    override fun Density.calculateMainAxisPageSize(
        availableSpace: Int,
        pageSpacing: Int
    ): Int {
        return (availableSpace - 2 * pageSpacing) / 3
    }
}

Padding de conteúdo

HorizontalPager e VerticalPager oferecem suporte à mudança do padding de conteúdo, o que permite influenciar o tamanho máximo e o alinhamento das páginas.

Por exemplo, definir o padding start alinha as páginas ao final:

Pager com padding inicial mostrando o conteúdo alinhado ao final
Figura 5. Pager com padding inicial.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(start = 64.dp),
) { page ->
    // page content
}

Definir o padding start e end com o mesmo valor centraliza o item horizontalmente:

Pager com padding inicial e final mostrando o conteúdo centralizado
Figura 6. Pager com padding horizontal.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(horizontal = 32.dp),
) { page ->
    // page content
}

Definir o padding end alinha as páginas em direção ao início:

Pager com padding de início e fim mostrando o conteúdo alinhado ao início
Figura 7. Pager com padding final.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(end = 64.dp),
) { page ->
    // page content
}

É possível definir os valores top e bottom para alcançar efeitos semelhantes para VerticalPager. O valor 32.dp é usado aqui apenas como exemplo. É possível definir cada uma das dimensões de padding como qualquer valor.

Personalizar o comportamento de rolagem

Os elementos combináveis padrão HorizontalPager e VerticalPager especificam como os gestos de rolagem funcionam com o pager. No entanto, é possível personalizar e alterar os padrões, como pagerSnapDistance ou flingBehavior.

Distância de ajuste

Por padrão, HorizontalPager e VerticalPager definem o número máximo de páginas que um gesto de deslizar pode rolar para uma página por vez. Para mudar isso, defina pagerSnapDistance no flingBehavior:

val pagerState = rememberPagerState(pageCount = { 10 })

val fling = PagerDefaults.flingBehavior(
    state = pagerState,
    pagerSnapDistance = PagerSnapDistance.atMost(10)
)

Column(modifier = Modifier.fillMaxSize()) {
    HorizontalPager(
        state = pagerState,
        pageSize = PageSize.Fixed(200.dp),
        beyondViewportPageCount = 10,
        flingBehavior = fling
    ) {
        PagerSampleItem(page = it)
    }
}

Criar um pager de avanço automático

Nesta seção, descrevemos como criar um pager de avanço automático com indicadores de página no Compose. A coleção de itens é rolada automaticamente na horizontal, mas os usuários também podem deslizar manualmente entre os itens. Se um usuário interagir com o pager, a progressão automática será interrompida.

Exemplo básico

Juntos, os snippets a seguir criam uma implementação básica de pager de avanço automático com um indicador visual, em que cada página é renderizada com uma cor diferente:

@Composable
fun AutoAdvancePager(pageItems: List<Color>, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        val pagerState = rememberPagerState(pageCount = { pageItems.size })
        val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState()

        val pageInteractionSource = remember { MutableInteractionSource() }
        val pageIsPressed by pageInteractionSource.collectIsPressedAsState()

        // Stop auto-advancing when pager is dragged or one of the pages is pressed
        val autoAdvance = !pagerIsDragged && !pageIsPressed

        if (autoAdvance) {
            LaunchedEffect(pagerState, pageInteractionSource) {
                while (true) {
                    delay(2000)
                    val nextPage = (pagerState.currentPage + 1) % pageItems.size
                    pagerState.animateScrollToPage(nextPage)
                }
            }
        }

        HorizontalPager(
            state = pagerState
        ) { page ->
            Text(
                text = "Page: $page",
                textAlign = TextAlign.Center,
                modifier = modifier
                    .fillMaxSize()
                    .background(pageItems[page])
                    .clickable(
                        interactionSource = pageInteractionSource,
                        indication = LocalIndication.current
                    ) {
                        // Handle page click
                    }
                    .wrapContentSize(align = Alignment.Center)
            )
        }

        PagerIndicator(pageItems.size, pagerState.currentPage)
    }
}

Pontos principais sobre o código

  • A função AutoAdvancePager cria uma visualização de paginação horizontal com avanço automático. Ele usa uma lista de objetos Color como entrada, que são usados como cores de plano de fundo para cada página.
  • pagerState é criado usando rememberPagerState, que mantém o estado do pager.
  • pagerIsDragged e pageIsPressed rastreiam a interação do usuário.
  • O LaunchedEffect avança automaticamente o pager a cada dois segundos, a menos que o usuário arraste o pager ou pressione uma das páginas.
  • HorizontalPager mostra uma lista de páginas, cada uma com um elemento combinável Text que mostra o número da página. O modificador preenche a página, define a cor do plano de fundo de pageItems e torna a página clicável.

@Composable
fun PagerIndicator(pageCount: Int, currentPageIndex: Int, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        Row(
            modifier = Modifier
                .wrapContentHeight()
                .fillMaxWidth()
                .align(Alignment.BottomCenter)
                .padding(bottom = 8.dp),
            horizontalArrangement = Arrangement.Center
        ) {
            repeat(pageCount) { iteration ->
                val color = if (currentPageIndex == iteration) Color.DarkGray else Color.LightGray
                Box(
                    modifier = modifier
                        .padding(2.dp)
                        .clip(CircleShape)
                        .background(color)
                        .size(16.dp)
                )
            }
        }
    }
}

Pontos principais sobre o código

  • Um elemento combinável Box atua como o elemento raiz e contém um Row para organizar os indicadores de página horizontalmente.
  • Um indicador de página personalizado é mostrado como uma linha de círculos, em que cada Box cortado em um CircleShape representa uma página.
  • O círculo da página atual é colorido como um DarkGray, enquanto os outros círculos são LightGray. O parâmetro currentPageIndex determina qual círculo é renderizado em cinza-escuro.

Resultado

Este vídeo mostra o pager básico de avanço automático dos snippets anteriores:

Figura 8. Um pager de avanço automático com um atraso de dois segundos entre cada progressão de página.

Outros recursos