在最近思索如何使用Compose方式改进我的开源TMDB电影列表应用程序的主屏幕时,一个激动人心的概念浮现在我的脑海中——为什么不整合一个吸引人的轮播图来展示即将上映的电影呢?在本文中,我将分享我的开发和实现自定义轮播图的经历,提供涉及不同步骤的见解。
首先,我搜索了现有的解决方案,考虑到Material Design 3文档中有一个轮播图组件的可用性。然而,我很快发现它尚未发布用于Jetpack Compose,而且仅在最新的MDC-Android(视图系统)Alpha版本中可访问。这迫使我决定开发自己的轮播图,利用HorizontalPager
,这是一个Compose组件,允许水平滚动和页面变换。
为了有效地解决开发过程,我将问题分为四个不同的部分:
HorizontalPager
转换为实现所需的旋转木马效果。HorizontalPager
页面实现自动滚动。HorizontalPager
转换为实现所需的旋转木马效果为了创建所需的旋转木马效果,我们必须首先自定义HorizontalPager
以部分显示前一个和后一个页面。
为此,HorizontalPager
提供了两个参数:
contentPadding
:允许我们从每个轴应用边距,创建所需的页面周围间距。pageSpacing
:在两个相邻页面之间引入间隙,确保视觉上吸引人的布局。HorizontalPager(
contentPadding = PaddingValues(horizontal = 32.dp),
pageSpacing = 16.dp
) { page ->
...
CarouselItem(item)
}
这个任务很容易完成。我只需要将HorizontalPager
置于Box布局中,并将DotIndicators
合成到Box底部对齐即可。
Box {
HorizontalPager(pageCount = pageCount, state = pagerState) { page ->
CarouselItem(itemList[page])
}
DotIndicators(
pageCount = pageCount,
pagerState = pagerState,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
我们创建了一个名为DotIndicators
的可组合组件。它需要两个参数:页数(pageCount
)和页面状态(pageState
)。我们使用页数(pageCount
)添加相同数量的指示器,而使用页面状态(pageState
)使选定的点显示更暗。内部实现如下:
@Composable
fun DotIndicators(
...,
modifier: Modifier
) {
Row(modifier = modifier) {
repeat(pageCount) { iteration ->
val color = if (pagerState.currentPage == iteration) selectedColor else unselectedColor
Box(
modifier = Modifier
.clip(CircleShape)
.background(color)
)
}
}
}
目前,我们的设置要求用户手动水平滑动以导航到下一页。为了增强用户体验,我们可以实现自动滚动。我们通过利用HorizontalPager提供的Coroutine、LaunchedEffect和PagerState来实现这一点。
以下是一个完全具备功能的示例实现:
@Composable
fun AwesomeCarousel(
pageCount: Int = 10,
pagerState: PagerState = rememberPagerState(),
autoScrollDuration: Long = 3000L
) {
val isDragged by pagerState.interactionSource.collectIsDraggedAsState()
if (isDragged.not()) {
with(pagerState) {
var currentPageKey by remember { mutableStateOf(0) }
LaunchedEffect(key1 = currentPageKey) {
launch {
delay(timeMillis = autoScrollDuration)
val nextPage = (currentPage + 1).mod(pageCount)
animateScrollToPage(page = nextPage)
currentPageKey = nextPage
}
}
}
}
HorizontalPager(pageCount = pageCount, state = pagerState) { page ->
CarouselItem(itemList[page])
}
}
代码片段中的主要要点是:
LaunchedEffect
:LaunchedEffect
是 Jetpack Compose 提供的一种特殊效果,允许我们在协程中执行副作用,当特定键值发生改变时触发。在这种情况下,key1
参数设置为 currentPageKey
,这意味着只要 currentPageKey
的值发生更改,该效果就会被触发。在LaunchedEffect
块内,我们使用 launch
函数启动一个新的协程。这个协程在合成过程中异步执行并且独立于合成进程。
delay(timeMillis = autoScrollDuration)
:这一行引入了一个延迟,协程会在 autoScrollDuration 变量指定的持续时间内暂停,然后继续执行下一行代码。在暂停后,我们再次更改键,这导致协程重新启动,因此这意味着 LaunchedEffect
内部的块将无限次地执行。
isDragged.not()
:此代码段确保仅在不拖动页面时执行后续操作。它控制自动滚动行为,避免了用户交互和自动滚动过程之间的冲突。例如,如果用户正在滑动页面,自动滚动将停止。
实现的最后一部分着重于增强轮播页面的视觉过渡效果。通过利用graphicLayer修饰器,我们可以实现平滑无缝的过渡效果。关键动画包括在淡出旧项的同时淡入新项,以及在项接近或远离中心时进行缩放。
以下代码计算当前页面和目标页面之间的偏移量,然后使用此偏移量在0.7和1之间插值出一个转换值。然后将此转换值应用于graphicLayer
的alpha
和scaleY
属性。
fun Modifier.carouselTransition(page: Int, pagerState: PagerState) =
graphicsLayer {
val pageOffset =
((pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue
val transformation =
lerp(
start = 0.7f,
stop = 1f,
fraction = 1f - pageOffset.coerceIn(0f, 1f)
)
alpha = transformation
scaleY = transformation
}
pagerState.currentPage-page
)和分数(pagerState.currentPageOffsetFraction
),后者表示在页面之间转换的进度。绝对值确保偏移量始终为正数。lerp
函数用于基于分数在起始点和结束点之间插值计算一个值。在这种情况下,起始值为0.7f,停止值为1f,而分数则被计算为1f - pageOffset.coerceIn(0f, 1f)
。coerceIn函数可以确保分数保持在0到1的范围内。alpha = transformation
将transformation
的值分配给了graphicLayer
的alpha
属性。将alpha设置为介于0和1之间的值可以确定图层的透明度。值为1表示图层完全不透明,而值为0表示它完全透明。scaleY = transformation
同样,这行将transformation
的值分配给了graphicLayer
的scaleY
属性。调整scaleY
属性会垂直缩放图层。值为1表示图层的原始大小,而小于1的值会减少垂直比例,大于1的值会增加垂直比例。HorizontalPager
的item modifier
上利用graphicLayer
扩展函数。这使我们可以对每个旋转木马项目应用变换并创建效果。HorizontalPager() { page ->
val page = list[page]
CarouselItem(
item = page,
modifier = Modifier.carouselTransition(page, pagerState)
)
}
https://github.com/TheSomeshKumar/Flixplorer