Pager dans Compose

Pour parcourir le contenu de gauche à droite ou de haut en bas, vous pouvez utiliser respectivement les composables HorizontalPager et VerticalPager. Ces composables ont des fonctions similaires à ViewPager dans le système de vue. Par défaut, HorizontalPager occupe toute la largeur de l'écran, VerticalPager occupe toute la hauteur et les composants Pager ne font défiler qu'une page à la fois. Vous pouvez configurer tous ces paramètres par défaut.

HorizontalPager

Pour créer un sélecteur de page qui défile horizontalement vers la gauche et vers la droite, utilisez HorizontalPager :

Figure 1. Démonstration 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

Pour créer un pager qui défile de haut en bas, utilisez VerticalPager :

Figure 2. Démonstration de VerticalPager

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

Création différée

Les pages de HorizontalPager et VerticalPager sont composées de manière différée et mises en page lorsque cela est nécessaire. À mesure que l'utilisateur fait défiler les pages, le composable supprime celles qui ne sont plus nécessaires.

Charger plus de pages hors écran

Par défaut, le pager ne charge que les pages visibles à l'écran. Pour charger plus de pages hors écran, définissez beyondBoundsPageCount sur une valeur supérieure à zéro.

Faire défiler un élément dans le sélecteur de page

Pour faire défiler le pager jusqu'à une page spécifique, créez un objet PagerState à l'aide de rememberPagerState() et transmettez-le en tant que paramètre state au pager. Vous pouvez appeler PagerState#scrollToPage() sur cet état, dans un 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")
}

Si vous souhaitez animer la page, utilisez la fonction 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")
}

Recevoir des notifications concernant les modifications de l'état d'une page

PagerState comporte trois propriétés contenant des informations sur les pages : currentPage, settledPage et targetPage.

  • currentPage : page la plus proche de la position d'accrochage. Par défaut, la position d'accrochage se trouve au début de la mise en page.
  • settledPage : numéro de la page lorsqu'aucune animation ni aucun défilement n'est en cours. Cela diffère de la propriété currentPage, car currentPage est immédiatement mis à jour si la page est suffisamment proche de la position d'accrochage, mais settledPage reste le même jusqu'à ce que toutes les animations soient terminées.
  • targetPage : position d'arrêt proposée pour un mouvement de défilement.

Vous pouvez utiliser la fonction snapshotFlow pour observer les modifications apportées à ces variables et y réagir. Par exemple, pour envoyer un événement Analytics à chaque changement de page, vous pouvez procéder comme suit :

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")
}

Ajouter un indicateur de page

Pour ajouter un indicateur à une page, utilisez l'objet PagerState pour obtenir des informations sur la page sélectionnée par rapport au nombre de pages, puis dessinez votre indicateur personnalisé.

Par exemple, si vous souhaitez un simple indicateur de cercle, vous pouvez répéter le nombre de cercles et modifier la couleur du cercle en fonction de la sélection de la page, à l'aide de 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 affichant un indicateur de cercle sous le contenu
Figure 3. Pager affichant un indicateur de cercle sous le contenu

Appliquer des effets de défilement d'éléments au contenu

Un cas d'utilisation courant consiste à utiliser la position de défilement pour appliquer des effets à vos éléments de pager. Pour connaître la distance entre une page et celle actuellement sélectionnée, vous pouvez utiliser PagerState.currentPageOffsetFraction. Vous pouvez ensuite appliquer des effets de transformation à votre contenu en fonction de la distance par rapport à la page sélectionnée.

Figure 4. Appliquer des transformations au contenu Pager

Par exemple, pour ajuster l'opacité des éléments en fonction de leur distance par rapport au centre, modifiez alpha à l'aide de Modifier.graphicsLayer sur un élément à l'intérieur du 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
    }
}

Tailles de page personnalisées

Par défaut, HorizontalPager et VerticalPager occupent respectivement toute la largeur ou toute la hauteur. Vous pouvez définir la variable pageSize sur une taille Fixed, Fill (par défaut) ou personnalisée.

Par exemple, pour définir une page de largeur fixe de 100.dp :

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

Pour dimensionner les pages en fonction de la taille de la fenêtre d'affichage, utilisez un calcul de taille de page personnalisée. Créez un objet PageSize personnalisé et divisez availableSpace par trois, en tenant compte de l'espacement entre les éléments :

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

Marge intérieure du contenu

HorizontalPager et VerticalPager permettent tous deux de modifier la marge intérieure du contenu, ce qui vous permet d'influencer la taille maximale et l'alignement des pages.

Par exemple, définir la marge intérieure start aligne les pages vers la fin :

Pager avec une marge intérieure de début montrant le contenu aligné sur la fin

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

Si vous définissez la marge intérieure start et end sur la même valeur, l'élément est centré horizontalement :

Pager avec marge intérieure de début et de fin, affichant le contenu centré

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

Définir la marge intérieure end permet d'aligner les pages vers le début :

Pager avec marge intérieure de début et de fin, affichant le contenu aligné sur le début

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

Vous pouvez définir les valeurs top et bottom pour obtenir des effets similaires pour VerticalPager. La valeur 32.dp n'est utilisée ici qu'à titre d'exemple. Vous pouvez définir chacune des dimensions de marge intérieure sur n'importe quelle valeur.

Personnaliser le comportement de défilement

Les composables HorizontalPager et VerticalPager par défaut spécifient le fonctionnement des gestes de défilement avec le sélecteur de page. Toutefois, vous pouvez personnaliser et modifier les valeurs par défaut, comme pagerSnapDistance ou flingBehavior.

Distance d'accrochage

Par défaut, HorizontalPager et VerticalPager définissent le nombre maximal de pages qu'un geste de balayage peut faire défiler sur une page à la fois. Pour modifier ce paramètre, définissez pagerSnapDistance sur 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)
    }
}

Créer un sélecteur de page à défilement automatique

Cette section explique comment créer un sélecteur de page à défilement automatique avec des indicateurs de page dans Compose. La collection d'éléments défile automatiquement horizontalement, mais les utilisateurs peuvent également balayer manuellement les éléments. Si un utilisateur interagit avec le sélecteur de page, la progression automatique s'arrête.

Exemple de base

Ensemble, les extraits suivants créent une implémentation de base du sélecteur de page à défilement automatique avec un indicateur visuel, dans laquelle chaque page s'affiche avec une couleur différente :

@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)
    }
}

Points clés concernant le code

  • La fonction AutoAdvancePager crée une vue de pagination horizontale avec progression automatique. Il prend en entrée une liste d'objets Color, qui sont utilisés comme couleurs d'arrière-plan pour chaque page.
  • pagerState est créé à l'aide de rememberPagerState, qui contient l'état du sélecteur de pages.
  • pagerIsDragged et pageIsPressed permettent de suivre l'interaction des utilisateurs.
  • L'icône LaunchedEffect fait avancer automatiquement le sélecteur de page toutes les deux secondes, sauf si l'utilisateur le fait glisser ou appuie sur l'une des pages.
  • HorizontalPager affiche une liste de pages, chacune avec un composable Text affichant le numéro de page. Le modificateur remplit la page, définit la couleur d'arrière-plan à partir de pageItems et rend la page cliquable.

@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)
                )
            }
        }
    }
}

Points clés concernant le code

  • Un composable Box est utilisé comme élément racine.
    • Dans Box, un composable Row organise les indicateurs de page horizontalement.
  • Un indicateur de page personnalisé s'affiche sous la forme d'une rangée de cercles, où chaque Box coupé dans un circle représente une page.
  • Le cercle de la page actuelle est de couleur DarkGray, tandis que les autres cercles sont LightGray. Le paramètre currentPageIndex détermine le cercle qui s'affiche en gris foncé.

Résultat

Cette vidéo présente le pager à défilement automatique de base à partir des extraits précédents :

Figure 1 : Un pager à défilement automatique avec un délai de deux secondes entre chaque progression de page.

Ressources supplémentaires