16、Page

3 阅读3分钟

Compose 中的分页器

在 Jetpack Compose 中,可以使用 HorizontalPagerVerticalPager 可组合项实现水平或垂直翻页功能。它们类似于视图系统中的 ViewPager,默认情况下占据整个屏幕宽度或高度,一次只能滑动一页。

HorizontalPager

创建一个可左右水平滚动的分页器:

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

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

HorizontalPager(
    state = pagerState,
    beyondViewportPageCount = 2 // 加载两侧各2个额外页面
) { page ->
    // page content
}

滚动到特定页面

使用 PagerState 滚动到特定页面:

val pagerState = rememberPagerState(pageCount = { 10 })
val coroutineScope = rememberCoroutineScope()

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

Button(
    onClick = {
        coroutineScope.launch {
            // 滚动到第5页(无动画)
            pagerState.scrollToPage(5)
        }
    },
    modifier = Modifier.align(Alignment.BottomCenter)
) {
    Text("Jump to Page 5")
}

添加动画效果:

Button(
    onClick = {
        coroutineScope.launch {
            // 滚动到第5页(带动画)
            pagerState.animateScrollToPage(5)
        }
    },
    modifier = Modifier.align(Alignment.BottomCenter)
) {
    Text("Jump to Page 5")
}

监听页面状态变化

PagerState 有三个重要属性:

  • currentPage:最接近贴靠位置的页面
  • settledPage:未运行动画或滚动时的页面
  • targetPage:滚动动作的建议停止位置

监听页面变化:

val pagerState = rememberPagerState(pageCount = { 10 })
LaunchedEffect(pagerState) {
    // 监听 currentPage 变化
    snapshotFlow { pagerState.currentPage }.collect { page ->
        // 处理页面变化,例如发送分析事件
        Log.d("Page change", "Page changed to $page")
    }
}

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

添加页面指示器

创建圆形页面指示器:

val pagerState = rememberPagerState(pageCount = { 4 })
HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page ->
    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)
        )
    }
}

为内容应用滚动效果

根据滚动位置应用效果,如调整不透明度:

val pagerState = rememberPagerState(pageCount = { 4 })
HorizontalPager(state = pagerState) { page ->
    Card(
        Modifier.size(200.dp).graphicsLayer {
            // 计算当前页面与所选页面的距离
            val pageOffset = ((pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue
            // 根据距离调整不透明度(50%-100%)
            alpha = lerp(start = 0.5f, stop = 1f, fraction = 1f - pageOffset.coerceIn(0f, 1f))
        }
    ) {
        // Card content
    }
}

自定义页面尺寸

默认情况下,分页器占据全部宽度/高度。可以自定义页面大小:

// 设置固定宽度
val pagerState = rememberPagerState(pageCount = { 4 })
HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp) // 每页100dp宽
) { page ->
    // page content
}

根据视口大小自定义计算:

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

HorizontalPager(
    state = pagerState,
    pageSize = threePagesPerViewport
) { page ->
    // page content
}

内容内边距

设置内容内边距来控制页面对齐方式:

// 起始内边距 - 页面向末尾对齐
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(start = 64.dp)
) { page ->
    // page content
}

// 两侧内边距 - 页面水平居中
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(horizontal = 32.dp)
) { page ->
    // page content
}

// 末端内边距 - 页面向开头对齐
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(end = 64.dp)
) { page ->
    // page content
}

自定义滚动行为

调整贴靠距离,控制一次滑动能滚动的最大页面数:

val pagerState = rememberPagerState(pageCount = { 10 })
val fling = PagerDefaults.flingBehavior(
    state = pagerState,
    pagerSnapDistance = PagerSnapDistance.atMost(10) // 一次最多滚动10页
)

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

创建自动翻页器

实现一个自动前进的分页器,用户交互时暂停:

@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()
        
        // 当pager被拖动或页面被按压时停止自动前进
        val autoAdvance = !pagerIsDragged && !pageIsPressed
        
        if (autoAdvance) {
            LaunchedEffect(pagerState, pageInteractionSource) {
                while (true) {
                    delay(2000) // 每2秒切换一次
                    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
                    ) {
                        // 处理页面点击
                    }
                    .wrapContentSize(align = Alignment.Center)
            )
        }
        
        PagerIndicator(pageItems.size, pagerState.currentPage)
    }
}

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

通过这些功能,Compose 分页器提供了灵活且强大的页面导航体验,适用于各种应用场景。