Compose 中的 Pager

要以左右或上下方式翻阅内容,您可以使用 HorizontalPagerVerticalPager 可组合项。这些可组合项与视图系统中的 ViewPager 具有相似的功能。默认情况下,HorizontalPager 占据屏幕的整个宽度,VerticalPager 占据整个高度,并且 Pager 每次仅可滑动一页。这些默认设置都是可配置的。

HorizontalPager

要创建可左右水平滚动的 Pager,请使用 HorizontalPager

图 1. HorizontalPager 演示

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

VerticalPager

要创建可上下滚动的 Pager,请使用 VerticalPager

图 2. VerticalPager 演示

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

延迟创建

HorizontalPagerVerticalPager 中的页面在需要时会延迟组合和布局。当用户滚动页面时,可组合项会移除所有不再需要的页面。

加载更多屏幕外页面

默认情况下,Pager 仅加载屏幕上可见的页面。要加载更多屏幕外页面,请将 beyondBoundsPageCount 设置为大于零的值。

滚动到 Pager 中的某个项目

要滚动到 Pager 中的特定页面,请使用 rememberPagerState() 创建一个 PagerState 对象,并将其作为 state 参数传递给 Pager。您可以在 CoroutineScope 内对此状态调用 PagerState#scrollToPage()

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

如果您想以动画方式滚动到该页面,请使用 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")
}

获取页面状态更改通知

PagerState 具有三个属性,其中包含页面信息:currentPagesettledPagetargetPage

  • currentPage:最接近吸附位置的页面。默认情况下,吸附位置位于布局的起始处。
  • settledPage:没有动画或滚动正在运行时所在的页面编号。这与 currentPage 属性不同,currentPage 会在页面足够接近吸附位置时立即更新,而 settledPage 则会保持不变,直到所有动画运行完毕。
  • targetPage:滚动移动的建议停止位置。

您可以使用 snapshotFlow 函数来观察这些变量的变化并对其作出反应。例如,要在每次页面更改时发送分析事件,您可以执行以下操作

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

添加页面指示器

要向页面添加指示器,请使用 PagerState 对象获取有关从多个页面中选择的页面信息,然后绘制您的自定义指示器。

例如,如果您想要一个简单的圆形指示器,您可以使用 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 showing a circle indicator below the content
图 3. Pager 在内容下方显示一个圆形指示器

将项目滚动效果应用于内容

一种常见的用例是使用滚动位置将效果应用于 Pager 项。要了解页面与当前选定页面的距离,您可以使用 PagerState.currentPageOffsetFraction。然后,您可以根据与选定页面的距离,将转换效果应用于您的内容。

图 4. 将转换应用于 Pager 内容

例如,要根据项目与中心的距离调整其不透明度,请使用 Pager 中项目的 Modifier.graphicsLayer 更改 alpha

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

自定义页面大小

默认情况下,HorizontalPagerVerticalPager 分别占据整个宽度或整个高度。您可以将 pageSize 变量设置为 FixedFill(默认)或自定义尺寸计算。

例如,要设置一个固定宽度为 100.dp 的页面

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

要根据视口大小调整页面大小,请使用自定义页面大小计算。创建一个自定义的 PageSize 对象,然后将 availableSpace 除以三,同时考虑到项目之间的间距

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

内容内边距

HorizontalPagerVerticalPager 都支持更改内容内边距,这让您可以影响页面的最大尺寸和对齐方式。

例如,设置 start 内边距会将页面向末尾对齐

Pager with start padding showing the content aligned towards the end

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

startend 内边距都设置为相同的值会使项目水平居中

Pager with start and end padding showing the content centered

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

设置 end 内边距会将页面向起始对齐

Pager with start and end padding showing the content aligned to the start

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

您可以设置 topbottom 值,以实现 VerticalPager 的类似效果。这里仅以 32.dp 值作为示例;您可以将每个内边距尺寸设置为任何值。

自定义滚动行为

默认的 HorizontalPagerVerticalPager 可组合项指定了滚动手势如何与 Pager 配合使用。但是,您可以自定义和更改默认设置,例如 pagerSnapDistanceflingBehavior

吸附距离

默认情况下,HorizontalPagerVerticalPager 会将快速滑动可滚动的最大页面数设置为每次一页。要更改此设置,请在 flingBehavior 上设置 pagerSnapDistance

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

创建自动前进的 Pager

本部分介绍了如何在 Compose 中创建带有页面指示器的自动前进 Pager。项目集合会自动水平滚动,但用户也可以手动在项目之间滑动。如果用户与 Pager 互动,它将停止自动前进。

基本示例

以下代码段共同创建了一个带有视觉指示器的基本自动前进 Pager 实现,其中每个页面都呈现为不同的颜色

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

代码要点

  • The AutoAdvancePager function creates a horizontally paging view with automatic advancement. It takes a list of Color objects as input, which are used as background colors for each page.
  • pagerState 是使用 rememberPagerState 创建的,它保存了 Pager 的状态。
  • pagerIsDraggedpageIsPressed 用于跟踪用户互动。
  • 除非用户拖动 Pager 或按下其中一个页面,否则 LaunchedEffect 会每两秒自动前进 Pager。
  • HorizontalPager 显示一个页面列表,每个页面都有一个 Text 可组合项,用于显示页码。该修饰符会填充页面,从 pageItems 设置背景颜色,并使页面可点击。

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

代码要点

  • 一个 Box 可组合项用作根元素。
    • Box 内部,一个 Row 可组合项水平排列页面指示器。
  • 自定义页面指示器显示为一排圆形,其中每个裁剪为 circleBox 代表一个页面。
  • 当前页面的圆形显示为 DarkGray,而其他圆形显示为 LightGraycurrentPageIndex 参数决定哪个圆形渲染为深灰色。

结果

此视频显示了前面代码段中的基本自动前进 Pager

图 1. 一个自动前进的 Pager,每个页面前进之间有两秒延迟。

其他资源