Jetpack Compose提供了一系列易于组合的UI元素来构建完整的Android应用界面。其中,ScrollableTabRow可以实现水平滑动的标签页,HorizontalPager可以实现ViewPager的效果。今天我们实现这两个Compose元素实现标签页和HorizontalPager的联动效果。
(上篇文章Jetpack Compose开发的本地笔记本)的首页动画效果的实现
创建CoroutineScope和rememberPagerState
val pagerState = rememberPagerState(initialPage = AppSystemSetManage.homeTabRememberPage)
val scope = rememberCoroutineScope()
rememberPagerState
用于管理HorizontalPager的状态,initialPage
指定初始化页面索引
rememberCoroutineScope
创建协程作用域,用于后续在标签点击事件中发起协程切换ViewPager页面
监听pagerState变化并保存到持久化数据
val currentIndex = pagerState.currentPage
LaunchedEffect(currentIndex) {
launch(Dispatchers.IO) {
AppSystemSetManage.homeTabRememberPage = currentIndex
}
}
- 使用
LaunchedEffect
监听pagerState
的当前页面currentIndex
的变化 - 一旦页面变化,会在
IO
调度器中将当前页面索引保存到AppSystemSetManage.homeTabRememberPage
- 这样当再次启动时,可以恢复到上一次选择的页面索引
设置ScrollableTabRow的选中标签和onClick监听
ScrollableTabRow(
//...
selectedTabIndex = pagerState.currentPage,
onClick = {
scope.launch {
pagerState.animateScrollToPage(index)
}
}
)
- selectedTabIndex
设置为ViewPager
的当前页面索引pagerState.currentPage
,这样选中对应标签
- onClick
监听每个标签的点击事件,在事件中调用scope.launch
和pagerState.animateScrollToPage
切换ViewPager
的目标页面
设置HorizontalPager的页面数量和状态
HorizontalPager(
pageCount = noteFolders.size,
state = pagerState,
)
pageCount
设置为标签数量,state
设置为管理HorizontalPager状态的pagerState
- 在每个页面中显示内容,这里使用
Box
充满全屏幕和HomeItemCard
自定义PagerTabIndicator绘制指示器
fun PagerTabIndicator(
tabPositions: List<TabPosition>,
pagerState: PagerState,
color: Color = WordsFairyTheme.colors.themeUi,
@FloatRange(from = 0.0, to = 1.0) percent: Float = 0.6f,
height: Dp = 5.dp,
)
- tabPositions
:标签位置信息列表
- pagerState
:管理HorizontalPager状态
- color
:指示器颜色
- percent
:指示器宽度百分比
- height
:指示器高度
val currentPage by rememberUpdatedState(newValue = pagerState.currentPage)
val fraction by rememberUpdatedState(newValue = pagerState.currentPageOffsetFraction)
- 使用
rememberUpdatedState
记住currentPage
为HorizontalPager的当前页面索引pagerState.currentPage
- 记住
fraction
为HorizontalPager的当前页面偏移比例pagerState.currentPageOffsetFraction
val currentTab = tabPositions[currentPage]
val previousTab = tabPositions.getOrNull(currentPage - 1)
val nextTab = tabPositions.getOrNull(currentPage + 1)
- 获取当前页面标签
currentTab
、前一个页面标签previousTab
和下一页面标签nextTab
的位置信息
val indicatorWidth = currentTab.width.toPx() * percent
val indicatorOffset = if (fraction > 0 && nextTab != null) {
lerp(currentTab.left, nextTab.left, fraction).toPx()
// 如果 fraction > 0 且存在下一标签,通过lerp计算指示器偏移位置
} else if (fraction < 0 && previousTab != null) {
lerp(currentTab.left, previousTab.left, -fraction).toPx()
// 如果 fraction < 0 且存在前一标签,通过lerp计算指示器偏移位置
} else {
currentTab.left.toPx() // 否则指示器偏移位置为当前标签的left值
}
- 根据标签宽度
currentTab.width
和百分比percent
计算指示器宽度indicatorWidth
- 根据
fraction
、当前标签位置currentTab
、前一个标签位置previousTab
和下一标签位置nextTab
计算指示器偏移位置indicatorOffset
绘制圆角矩形的指示器
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)
)
- 使用
Canvas.drawRoundRect()
在界面上绘制圆角矩形的指示器 - 该函数从
tabPositions
中获取每个标签的位置信息 - 结合
pagerState
的currentPage
和currentPageOffsetFraction
计算指示器的宽度、偏移位置等 - 使用
Canvas
绘制round rect
形状的指示器
完整代码
@OptIn(
ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
)
@Composable
fun HomeTab(
noteFolders: List<String>,
currentFolderCallback : (String) -> Unit ={},
itemOnClick: (entity: String, offset: IntOffset, cardSize: IntSize) -> Unit
) {
// 创建 CoroutineScope
val pagerState = rememberPagerState(initialPage = AppSystemSetManage.homeTabRememberPage)
val scope = rememberCoroutineScope()
ScrollableTabRow(
modifier = Modifier.fillMaxWidth(),
selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp,
indicator = { tabPositions ->
if (tabPositions.isNotEmpty()) {
PagerTabIndicator(tabPositions = tabPositions, pagerState = pagerState)
}
},
containerColor = WordsFairyTheme.colors.background,
divider = {
}
) {
noteFolders.forEachIndexed { index, title ->
val selected = (pagerState.currentPage == index)
Tab(
selected = selected,
selectedContentColor = WordsFairyTheme.colors.textPrimary,
unselectedContentColor = WordsFairyTheme.colors.textSecondary,
onClick = {
scope.launch {
pagerState.animateScrollToPage(index)
}
}
) {
Text(
text = title,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(9.dp)
)
}
}
}
HorizontalPager(
pageCount = noteFolders.size,
state = pagerState,
beyondBoundsPageCount = 20,
modifier = Modifier.fillMaxSize()
) { page ->
Box(modifier = Modifier.fillMaxSize()) {
StaggeredVerticalGrid(
maxColumnWidth = 220.dp,
modifier = Modifier
.padding(4.dp)
.verticalScroll(rememberScrollState())
) {
Card(){
....
}
}
}
}
}
PagerTabIndicator
@Composable
fun PagerTabIndicator(
tabPositions: List<TabPosition>,
pagerState: PagerState,
color: Color = WordsFairyTheme.colors.themeUi,
@FloatRange(from = 0.0, to = 1.0) percent: Float = 0.6f,
height: Dp = 5.dp,
) {
val currentPage by rememberUpdatedState(newValue = pagerState.currentPage)
val fraction by rememberUpdatedState(newValue = pagerState.currentPageOffsetFraction)
val currentTab = tabPositions[currentPage]
val previousTab = tabPositions.getOrNull(currentPage - 1)
val nextTab = tabPositions.getOrNull(currentPage + 1)
Canvas(
modifier = Modifier.fillMaxSize(),
onDraw = {
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()
}
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)
)
}
)
}