在传统的 Android View 体系中,实现轮播图通常需要使用 ViewPager 或 ViewPager2 配合 Handler 或 Timer。而在 Jetpack Compose 中,得益于声明式 UI 和强大的 HorizontalPager,我们可以用非常精简的代码实现一个功能完备、自带动画效果的轮播图组件。
本文将带你一步步实现一个支持自动轮播 、手指拖拽暂停 、无限循环 (伪无限)以及指示器的 Carousel 组件。
1. 核心组件:HorizontalPager
从 Compose 1.4.0 (BOM 2023.03.00) 开始,Pager 相关的 API 已经正式从 Accompanist 迁移到了 androidx.compose.foundation.pager 包中,因此我们不需要额外引入第三方库,直接使用官方的基础库即可。
基础依赖
确保你的 build.gradle 中 Compose 相关的依赖是比较新的版本:
kotlin
implementation("androidx.compose.foundation:foundation:1.6.0") // 或更高版本
2. 简易版轮播图实现
首先,我们实现一个最基础的滑动展示图片的 Pager。
kotlin
@Composable
fun SimpleCarousel(
imageUrls: List<String>,
modifier: Modifier = Modifier
) {
val pagerState = rememberPagerState(pageCount = { imageUrls.size })
HorizontalPager(
state = pagerState,
modifier = modifier.fillMaxWidth().height(200.dp)
) { page ->
// 这里可以使用 Coil 或 Glide 加载图片
// AsyncImage(model = imageUrls[page], ...)
Box(
modifier = Modifier
.fillMaxSize()
.background(if (page % 2 == 0) Color.Blue else Color.Red),
contentAlignment = Alignment.Center
) {
Text(text = "Page $page", color = Color.White)
}
}
}
3. 进阶功能实现
一个成熟的轮播图通常需要具备以下特性:
- 自动轮播:每隔几秒自动切换下一页。
- 无限循环:滑到最后一张时能无缝衔接到第一张。
- 指示器:显示当前页面的位置。
- 交互优化:用户触摸时暂停轮播。
3.1 无限循环的技巧
实现"无限循环"的一种常见 trick 是将 pageCount 设置为一个非常大的数字(例如 Int.MAX_VALUE),然后通过取模运算映射到实际的数据索引上。
kotlin
// 实际数据索引 = pageIndex % list.size
val actualIndex = page % imageUrls.size
初始位置建议设置在中间的某个位置,确保用户一开始既可以向左滑也可以向右滑。
3.2 自动轮播与交互暂停
我们可以使用 LaunchedEffect 来处理定时任务。为了实现"用户触摸时暂停",我们需要检测用户的拖拽状态。PagerState 提供了 isScrollInProgress 属性,或者我们可以通过 collectIsDraggedAsState() 来监听。
3.3 完整代码实现
下面是一个封装好的通用 BannerCarousel 组件:
kotlin
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BannerCarousel(
dataList: List<String>, // 这里可以是任意数据模型
modifier: Modifier = Modifier,
autoScrollDelay: Long = 3000L,
content: @Composable (String) -> Unit // 自定义 Item 内容
) {
if (dataList.isEmpty()) return
// 1. 设置一个超大的页数来实现伪无限循环
// 初始页面定在中间某个位置,保证左右都能滑
val initialPage = Int.MAX_VALUE / 2
// 修正初始页,使其对应数据列表的第一个元素 (index 0)
val startIndex = initialPage - (initialPage % dataList.size)
val pagerState = rememberPagerState(
initialPage = startIndex,
pageCount = { Int.MAX_VALUE }
)
// 2. 监听用户是否正在拖拽
val isDragged by pagerState.interactionSource.collectIsDraggedAsState()
// 3. 自动轮播逻辑
// 当 isDragged 为 true 时,Key 发生变化,LaunchedEffect 重启/取消
// 这里利用 isDragged 作为 key,当用户拖拽时,自动轮播协程会被取消;停止拖拽后重新开始
LaunchedEffect(isDragged) {
if (!isDragged) {
while (true) {
delay(autoScrollDelay)
try {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
} catch (e: Exception) {
// 忽略异常或处理页面销毁时的取消异常
}
}
}
}
Box(modifier = modifier) {
// 4. 轮播主体
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { pageIndex ->
// 使用取模运算获取实际的数据索引
val actualIndex = pageIndex % dataList.size
content(dataList[actualIndex])
}
// 5. 指示器 (Indicator)
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
val currentActualIndex = pagerState.currentPage % dataList.size
repeat(dataList.size) { index ->
val isSelected = currentActualIndex == index
Box(
modifier = Modifier
.size(if (isSelected) 8.dp else 6.dp)
.clip(CircleShape)
.background(if (isSelected) Color.White else Color.White.copy(alpha = 0.5f))
)
}
}
}
}
4. 使用示例
在你的页面中直接调用上面封装好的组件:
kotlin
@Composable
fun HomeScreen() {
val banners = listOf(
"https://example.com/1.jpg",
"https://example.com/2.jpg",
"https://example.com/3.jpg"
)
BannerCarousel(
dataList = banners,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
) { imageUrl ->
// 这里展示具体的图片内容
// 示例:使用 Coil 加载
/*
AsyncImage(
model = imageUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
*/
// 演示用的占位符
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Gray),
contentAlignment = Alignment.Center
) {
Text("Image: $imageUrl")
}
}
}
5. 总结
在 Jetpack Compose 中实现轮播图非常直观:
- HorizontalPager:处理核心的滑动逻辑。
- Int.MAX_VALUE + 取模:轻松实现无限循环。
- LaunchedEffect + delay:利用协程实现自动轮播定时器。
- interactionSource:精准捕捉用户手势,处理暂停/恢复逻辑。
这种方式不仅代码量少,而且充分利用了 Compose 的重组机制和协程特性,性能优异且易于维护。