Compose 中的 Pager

要以左右或上下方式翻阅内容,您可以分别使用 HorizontalPagerVerticalPager 可组合项。这些可组合项的功能与视图系统中的 ViewPager 类似。默认情况下,HorizontalPager 占据屏幕的全部宽度,VerticalPager 占据全部高度,并且翻页器每次只翻动一页。这些默认值都是可配置的。

HorizontalPager

要创建水平左右滚动的翻页器,请使用 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

要创建上下滚动的翻页器,请使用 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 中的页面在需要时会 延迟组合 和布局。当用户滚动浏览页面时,可组合项会移除不再需要的任何页面。

加载屏幕外的更多页面

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

滚动到翻页器中的某个项目

要滚动到翻页器中的特定页面,请使用 rememberPagerState() 创建一个 PagerState 对象,并将其作为 state 参数传递给翻页器。您可以在 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. 翻页器在内容下方显示圆形指示器

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

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

图 4. 将转换应用于翻页器内容

例如,要根据项目距离中心的距离调整其不透明度,请使用翻页器内项目上的 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 除以 3,同时考虑项目之间的间距

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 可组合项指定了滚动手势如何与翻页器配合使用。但是,您可以自定义和更改默认值,例如 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),
        beyondBoundsPageCount = 10,
        flingBehavior = fling
    ) {
        PagerSampleItem(page = it)
    }
}