Jetpack Compose TabRow与HorizontalPager 联动

2,563 阅读2分钟

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.launchpagerState.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中获取每个标签的位置信息 
  • 结合pagerStatecurrentPagecurrentPageOffsetFraction计算指示器的宽度、偏移位置等 
  • 使用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)
            )
        }
    )
}

效果图

ezgif.com-video-to-gif (1).gif