翻译不一定很好,欢迎阅读 原文 ,也欢迎指出错误!
感激不尽
随着Compose March 2023 release 的发布, HorizontalPager
和 VerticalPager
这两个组件也一同加入了 Compose 家族中。虽然 官方文档 中已经说明了如何使用 pager ,但它毕竟是官方文档,给出的示例还是相对简单了些。接下来让我们一起看一下如何为 pager 组件添加一些有意思的指示器和过渡效果。
页面过渡效果 🔀
在官方文档中提到了如何获取一个页面距离"Snapped Position"的位置差,我们可以利用这个位置差信息在两个 page 之间创建过渡效果。
举个例子,我们想在两个 page 切换的过程中添加一个简单的淡入淡出,只需要添加一个 graphicsLayer
的 modifier 给 Composeable Page,让他调整自己的 alpha
和 translationX
属性:
kotlin
val pagerState = rememberPagerState(pageCount = { 10 })
HorizontalPager(
modifier = modifier.fillMaxSize(),
state = pagerState
) { page ->
Box(Modifier
.graphicsLayer {
// 译者按:截止2023.8.1,Compose 已提供 currentPageOffsetFraction() 用于直接获取offset
val pageOffset = pagerState.calculateCurrentOffsetForPage(page)
// translate the contents by the size of the page, to prevent the pages from sliding in from left or right and stays in the center
translationX = pageOffset * size.width
// apply an alpha to fade the current page in and the old page out
alpha = 1 - pageOffset.absoluteValue
}
.fillMaxSize()) {
Image(
painter = rememberAsyncImagePainter(model = rememberRandomSampleImageUrl
(width = 1200)),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
.padding(16.dp)
.clip(RoundedCornerShape(16.dp)),
)
}
}
// extension method for current page offset
@OptIn(ExperimentalFoundationApi::class)
fun PagerState.calculateCurrentOffsetForPage(page: Int): Float {
return (currentPage - page) + currentPageOffsetFraction
}
我们可以将 graphicsLayer
这个 modifier 提取出来,这样 HorizontalPager
组件就可以复用了:
kotlin
// 译者按:因为calculateCurrentOffsetForPage已经被currentPageOffsetFraction替代
// 所以参数 page 就没有必要再传入了
fun Modifier.pagerFadeTransition(page: Int, pagerState: PagerState) =
graphicsLayer {
val pageOffset = pagerState.calculateCurrentOffsetForPage(page)
translationX = pageOffset * size.width
alpha = 1 - pageOffset.absoluteValue
}
OK,这下我们使用 Compose 也做到了在之前的 View 体系下的使用 ViewPager
能做到的效果。
其他有意思的过渡特效 🪄
除了上面的淡入淡出,一些之前使用ViewPager实现的其他常见效果,其实在Compose下使用Pager组件同样也能实现。再看看下面的例子:
倾斜向内切换
倾斜向外切换
指尖陀螺切换
这些效果的demo可以在这个 repo 中看到。除了上面图片展示的几个效果,repo中还有一些别的效果,可以去看看。
Rebecc,但你刚刚说的这些都只是一些很简单的东西,我们希望Compose不止能做到过去我们在View当中能做到的事情 🙄我知道你很急,但是你先别急。呐,我们现在就来用Compose来做一些在View体系下比较难做到事情
根据不同的位置去调整Pager中的Composable Content
在过去,大家已经习惯于去调整 page 去实现动画,但 Compose 最牛逼的地方在于 ------ 我们还可以调整 page 里面的 content 的 PagerState
。我们可以利用这个属性去实现一些很好玩的、很高级的动画,比如根据页面的滚动状态去隐藏/显示 content,甚至对 content 进行缩放。
让我们来看一下如何利用这个特性去实现一个超级无敌酷炫的动画设计(视频):
首先,我们创建这个动画会用到的组件 ------ HorizontalPager
,并把它放在一个 Box
里。再使用 Image
和 Text
组件放在page内作为卡片的内容。
kotlin
@Preview
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DribbbleInspirationPager() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFECECEC))
) {
val pagerState = rememberPagerState(pageCount = { 10 })
HorizontalPager(
pageSpacing = 16.dp,
beyondBoundsPageCount = 2,
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
Box(modifier = Modifier.fillMaxSize()) {
// Contains Image and Text composables
SongInformationCard(
modifier = Modifier
.padding(32.dp)
.align(Alignment.Center),
pagerState = pagerState,
page = page
)
}
}
}
}
现在我们创建好了所有需要的组件,接下来就开始分析组件中哪些部分会有动画,并调整动画效果对应的属性。最先被注意到的肯定就是卡片内的Image会根据当前是否被选中而缩放;再一个就是卡片的尺寸会变大,并且在变大的时候显示提示文字 "drag to listen"。 为了实现这两个效果,我们可以和前面的例子一样,使用 pagerState.currentPageOffsetFraction
和 pagerState.currentPage
这两个属性对卡片中的产生动画效果部分的属性进行调整。
我们可以使用 Modifier.graphicsLayer { }
去调整page中的 Image
组件:
kotlin
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SongInformationCard(
pagerState: PagerState,
page: Int,
modifier: Modifier = Modifier
) {
Card(
modifier = /*..*/
) {
Column(modifier = /*..*/) {
val pageOffset = pagerState.calculateCurrentOffsetForPage(page)
Image(
modifier = Modifier
/* other modifiers */
.graphicsLayer {
// get a scale value between 1 and 1.75f, 1.75 will be when its resting,
// 1f is the smallest it'll be when not the focused page
val scale = lerp(1f, 1.75f, pageOffset)
// apply the scale equally to both X and Y, to not distort the image
scaleX = scale
scaleY = scale
},
//..
)
SongDetails()
}
}
}
通过使用 pagerState.currentPageOffsetFraction
,我们可以知道 page距离被吸附的位置已经移动了多少距离 。之后可以通过对 pageOffset
进行缩放,把它的值域控制在 1f
到 1.75f
之间,我们可以把这个值同时对 Image
的 scaleX
and scaleY
属性进行操作。当未选中时,缩放比例为 1.75f
;当选中时,缩放比例为 1f
。
结果如下:
接下来就是去实现卡片内展示/隐藏 "drag to listen" 提示文字的部分。这次我们同样使用 pageOffset
的值,并对 Column
和 alpha
进行调整:
kotlin
@Composable
private fun DragToListen(pageOffset: Float) {
Box(
modifier = Modifier
.height(150.dp * (1 - pageOffset))
.fillMaxWidth()
.graphicsLayer {
alpha = 1 - pageOffset
}
) {
Column(
modifier = Modifier.align(Alignment.BottomCenter),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Rounded.MusicNote, contentDescription = "",
modifier = Modifier
.padding(8.dp)
.size(36.dp)
)
Text("DRAG TO LISTEN")
Spacer(modifier = Modifier.size(4.dp))
DragArea()
}
}
}
@Composable
private fun DragArea() {
Box {
Canvas(
modifier = Modifier
.padding(0.dp)
.fillMaxWidth()
.height(60.dp)
.clip(RoundedCornerShape(bottomEnd = 32.dp, bottomStart = 32.dp))
) {
val sizeGap = 16.dp.toPx()
val numberDotsHorizontal = size.width / sizeGap + 1
val numberDotsVertical = size.height / sizeGap + 1
repeat(numberDotsHorizontal.roundToInt()) { horizontal ->
repeat(numberDotsVertical.roundToInt()) { vertical ->
drawCircle(
Color.LightGray.copy(alpha = 0.5f), radius = 2.dp.toPx
(), center =
Offset(horizontal * sizeGap + sizeGap, vertical * sizeGap + sizeGap)
)
}
}
}
Icon(
Icons.Rounded.ExpandMore, "down",
modifier = Modifier
.size(height = 24.dp, width = 48.dp)
.align(Alignment.Center)
.background(Color.White)
)
}
}
我们现在可以看到,卡片的高度会跟着 page 移动而产生变化:
奈斯!--- 我们现在完整实现了设计原稿上的动效!完整的源码可以在 这里 查看
页面指示器 🚦
通过编写前面提到的动画,我们学会了使用 PagerState
属性,并将其与 content 的属性结合到了一起。除此之外,pager 另一个常见的需求是添加一个页面指示器,用来显示现在在列表中的位置. 现在我们试一下用Compose和 PagerState
去实现它,其实非常简单。
我们可以跟随Google提供的文档去做一个简单的页面指示器 ------ 为每个页面画一个小圆圈;也可以创建自定义页面指示器,比如:一个在屏幕下方分段的线条。
让我们来试一下这个效果:
首先,我们在 Box
内创建一个 HorizontalPager
,并且将小圆圈样式的指示器移动到底部,让它和页面内的内容产生重叠:
kotlin
@Preview
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LineIndicatorExample() {
Box(modifier = Modifier.fillMaxSize()) {
val pageCount = 5
val pagerState = rememberPagerState(pageCount = { pageCount })
HorizontalPager(
beyondBoundsPageCount = 2,
state = pagerState) {
PagerSampleItem(page = it)
}
Row(
Modifier
.height(50.dp)
.fillMaxWidth()
.align(Alignment.BottomCenter),
horizontalArrangement = Arrangement.Center
) {
repeat(pageCount) { iteration ->
val color = if (pagerState.currentPage == iteration) Color.White else Color.White.copy(alpha = 0.5f)
Box(
modifier = Modifier
.padding(4.dp)
.clip(CircleShape)
.background(color)
.size(16.dp)
)
}
}
}
}
接下来,我们要把这个指示器做一些更改,让圆形变成一个直线,并且在不同选中状态下展现出不同的颜色和尺寸,线条的初始 weight 为 1f
。
kotlin
@Preview
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LineIndicatorExample() {
Box(modifier = Modifier.fillMaxSize()) {
val pageCount = 5
val pagerState = rememberPagerState(pageCount = { pageCount })
HorizontalPager(
beyondBoundsPageCount = 2,
state = pagerState) {
PagerSampleItem(page = it,
modifier = Modifier.pagerFadeTransition(it, pagerState = pagerState))
}
Row(
Modifier
.height(24.dp)
.padding(start = 4.dp)
.fillMaxWidth()
.align(Alignment.BottomCenter),
horizontalArrangement = Arrangement.Start
) {
repeat(pageCount) { iteration ->
val color = if (pagerState.currentPage == iteration) Color.White else Color.White.copy(alpha = 0.5f)
Box(
modifier = Modifier
.padding(4.dp)
.clip(RoundedCornerShape(2.dp))
.background(color)
.weight(1f)
.height(4.dp)
)
}
}
}
}
现在每一个page都会有一个直线可以被用来表示,而且不用修改它的大小:
我们现在给指示器的长度也加上动效:当被选中时,它应该是最长的。现在给 weight
设置动画,让指示器在"选中页面右侧"的 1f
与"选中"的 1.5f
和"选中页面左侧"的 0.5f
之间进行选择。使用 animateFloatAsState
让 weight
在这些权重之间进行调整:
完整的代码可以在 这里 查看。
结语
正如我们在这篇文章中所提到的,我们可以发现使用 Compose 的 PagerState
可以灵活地创建更复杂的页面交互,而这在以前是很难实现的。利用 pagerState.currentPage
、pagerState.currentPageOffsetFraction
变量,我们可以创建相当复杂的动效和页面指示器。
如果你自己也实现了自定义的page切换动画或者指示器,请务必来 androiddev.social/riggaroo 留个言分享给我
希望你有个愉快的 Compose 体验 👋