1. Jetpack Compose 的viewPager
用accompanist pager库,这里是它github的地址
这里是用的v0.24.13-rc,对应的comopse版本是1.2.0-rc3
//HorizontalPager and VerticalPager
implementation "com.google.accompanist:accompanist-pager:$accompanist_version"
// If using indicators, also depend on
implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version"
这是官网的TabRow 和 HorizontalPager的示例代码:
val pagerState = rememberPagerState()
TabRow(
// Our selected tab is our current page
selectedTabIndex = pagerState.currentPage,
// Override the indicator, using the provided pagerTabIndicatorOffset modifier
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions)
)
}
) {
// Add tabs for all of our pages
pages.forEachIndexed { index, title ->
Tab(
text = { Text(title) },
selected = pagerState.currentPage == index,
onClick = { /* TODO */ },
)
}
}
HorizontalPager(
count = pages.size,
state = pagerState,
) { page ->
// TODO: page content
}
2.自定义Tab指示器动画
根据官网的示例代码稍微修改下运行:
然后想把这个指示器设为圆角,不用它占满整个tab的宽度。
貌似怎么去配置修改这个TabRowDefaults.Indicator的Modifier都达不到想要的效果。
那就只能自定义TabIndicator了。 开始提出自定义需求:
- 指示器为圆角
- 指示器可以更具percent占满tab的宽度
- 指示器的动画要和下面的HorizontalPager的滑动保持一致,不能等下面的pager滑动动画完了再做动画。
/**
* PagerTap 指示器
* @param percent 指示器占用整个tab宽度的比例
* @param height 指示器的高度
* @param color 指示器的颜色
*/
@OptIn(ExperimentalPagerApi::class)
@Composable
fun PagerTabIndicator(
tabPositions: List<TabPosition>,
pagerState: PagerState,
color: Color = MaterialTheme.colors.primarySurface,
@FloatRange(from = 0.0, to = 1.0) percent: Float = 0.4f,
height: Dp = 4.dp,
) {
Canvas(modifier = Modifier.fillMaxSize()) {
val currentPage = minOf(tabPositions.lastIndex, pagerState.currentPage)
val currentTab = tabPositions[currentPage]
val previousTab = tabPositions.getOrNull(currentPage - 1)
val nextTab = tabPositions.getOrNull(currentPage + 1)
val fraction = pagerState.currentPageOffset
val indicatorWidth = currentTab.width.toPx() * percent
val indicatorOffset = if (fraction > 0 && nextTab != null) {
lerp(currentTab.left, nextTab.left, fraction).toPx()
} else if (fraction < 0 && previousTab != null) {
lerp(currentTab.left, previousTab.left, -fraction).toPx()
} else {
currentTab.left.toPx()
}
/*Log.i(
"hj",
"fraction = ${fraction} , indicatorOffset = ${indicatorOffset}"
)*/
val canvasHeight = size.height
drawRoundRect(
color = color,
topLeft = Offset(
indicatorOffset + (currentTab.width.toPx() * (1 - percent) / 2),
canvasHeight - height.toPx()
),
size = Size(indicatorWidth + indicatorWidth * abs(fraction), height.toPx()),
cornerRadius = CornerRadius(50f)
)
}
}
这里上面indicatorOffset 就是pager的滑动的指示器横向的偏移量,它会跟随pager的滑动不断变化,进而对这个comopse进行重组,所以大家在这里看不到任何animation的api。
indicatorOffset这里的代码我之直接copy Modifier.pagerTabIndicatorOffset方法里面的。
这里用了Canvas,没用Box 、Driver,这种圆角线能用Canvas实现就用Canvas去实现,因为性能比Box等更好一些,这在jectpack compose 官网有讲到:
3. 自定义Tab
官网示例代码中的tab文字任何动画都没有,只看到字体大小和颜色在滑动的过程中瞬间改变。
提出自定义tab的需求:
- Tab字体大小随着pager的滑动和动画而改变。
- Tab字体的weight随着pager的滑动和动画而改变。
- Tab字体的颜色随着pager的滑动和动画而渐变。
这里我们就要得到每个tab对应滑动的progress 它的值0f到1f,0f表示该tab未选中的状态,1f表示该tab选中的状态,然后就要根据progress得到对应的fontSize,fontWeight,color了。
/**
* 自定义 PagerTab
* @param index 对应第几个tab 从0开始
* @param pageCount page的总个数
* @param selectedContentColor tab选中时的颜色
* @param unselectedContentColor tab没选中时的颜色
* @param selectedFontSize tab选中时的文字大小
* @param unselectedFontSize tab没选中时的文字大小
* @param selectedFontWeight tab选中时的文字比重
* @param unselectedFontWeight tab没选中时的文字比重
*/
@OptIn(ExperimentalPagerApi::class)
@Composable
fun PagerTab(
modifier: Modifier = Modifier,
pagerState: PagerState,
index: Int,
pageCount: Int,
text: String,
selectedContentColor: Color = MaterialTheme.colors.primary,
unselectedContentColor: Color = MaterialTheme.colors.onSurface,
selectedFontSize: TextUnit = 18.sp,
unselectedFontSize: TextUnit = 15.sp,
selectedFontWeight: FontWeight = FontWeight.Bold,
unselectedFontWeight: FontWeight = FontWeight.Normal,
) {
val previousIndex = max(index - 1, 0)
val nextIndex = min(index + 1, pageCount - 1)
val currentIndexPlusOffset = pagerState.currentPage + pagerState.currentPageOffset
val progress =
if (currentIndexPlusOffset >= previousIndex && currentIndexPlusOffset <= nextIndex) {
1f - abs(index - currentIndexPlusOffset)
} else {
0f
}
val fontSize = androidx.compose.ui.unit.lerp(unselectedFontSize, selectedFontSize, progress)
val fontWeight =
androidx.compose.ui.text.font.lerp(unselectedFontWeight, selectedFontWeight, progress)
val color =
androidx.compose.ui.graphics.lerp(unselectedContentColor, selectedContentColor, progress)
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Text(text = text, color = color, fontSize = fontSize, fontWeight = fontWeight)
}
}
大家可以打印看currentIndexPlusOffset ,当从第一页滑动到第二页 ,它是从0f到1f来改变,第二页滑动到第一页就变成了从1f到0f,第二页滑动到第三页,它就是从1f到2f,以此类推;通过它就可以判断到底哪两个页面的tab需要做动画了
if (currentIndexPlusOffset >= previousIndex && currentIndexPlusOffset <= nextIndex)
然后就可以通过currentIndexPlusOffset去得到每个tab的progress了。
最后是官方都有api 去根据fraction(progress)得到对应value的方法。
val fontSize = androidx.compose.ui.unit.lerp(unselectedFontSize, selectedFontSize, progress)
val fontWeight =
androidx.compose.ui.text.font.lerp(unselectedFontWeight, selectedFontWeight, progress)
val color =
androidx.compose.ui.graphics.lerp(unselectedContentColor, selectedContentColor, progress)
4. 最后的使用的代码
val pages = arrayOf("Home", "Shows", "Books")
@OptIn(ExperimentalPagerApi::class)
@Composable
fun PagerScreen() {
Scaffold(topBar = {
CenterTopAppBar(title ="Pager")
}) {
Column(Modifier.padding(it)) {
val pagerState = rememberPagerState()
TabRow(
modifier = Modifier.fillMaxWidth(),
// Our selected tab is our current page
selectedTabIndex = pagerState.currentPage,
// Override the indicator, using the provided pagerTabIndicatorOffset modifier
indicator = { tabPositions ->
PagerTabIndicator(tabPositions = tabPositions, pagerState = pagerState)
},
backgroundColor = MaterialTheme.colors.surface
) {
val scope: CoroutineScope = rememberCoroutineScope()
// Add tabs for all of our pages
pages.forEachIndexed { index, title ->
PagerTab(pagerState = pagerState,
index = index,
pageCount = pages.size,
text = title,
modifier = Modifier
.height(50.dp)
.clickable {
scope.launch {
pagerState.animateScrollToPage(index)
}
})
}
}
HorizontalPager(
count = pages.size,
state = pagerState,
) { page ->
PagerContent(page)
}
}
}
}