compose 自定义TabRow的indicator的宽度

2,074 阅读2分钟

在使用compose的TabRow时,其下标的宽度永远是分配好的,并且是平均分配的宽度,但是在某些情况下,使用短一些的下划线会更好看一些。 下划线的滑动及其动画其实官方已经提供了有modifier修饰符,可以看出并没有给出修改的宽度。

fun Modifier.pagerTabIndicatorOffset(
    pagerState: PagerState,
    tabPositions: List<TabPosition>,
): Modifier

但是tabRow的高度我们是可以通过modifier的height来修饰的。
那我们看看具体官方是怎么实现的。

@ExperimentalPagerApi
fun Modifier.pagerTabIndicatorOffset(
    pagerState: PagerState,
    tabPositions: List<TabPosition>,
): Modifier = composed {
    // If there are no pages, nothing to show
    if (pagerState.pageCount == 0) return@composed this

    val targetIndicatorOffset: Dp
    // 通过翻译可知这就是下划线的宽度
    val indicatorWidth: Dp

    val currentTab = tabPositions[minOf(tabPositions.lastIndex, pagerState.currentPage)]
    val targetPage = pagerState.targetPage
    val targetTab = tabPositions.getOrNull(targetPage)

    if (targetTab != null) {
        // The distance between the target and current page. If the pager is animating over many
        // items this could be > 1
        val targetDistance = (targetPage - pagerState.currentPage).absoluteValue
        // Our normalized fraction over the target distance
        val fraction = (pagerState.currentPageOffset / max(targetDistance, 1)).absoluteValue

        targetIndicatorOffset = lerp(currentTab.left, targetTab.left, fraction)
        // 在此计算出下划线的宽度
        indicatorWidth = lerp(currentTab.width, targetTab.width, fraction).absoluteValue
    } else {
        // Otherwise we just use the current tab/page
        targetIndicatorOffset = currentTab.left
        indicatorWidth = currentTab.width
    }

    fillMaxWidth()
        .wrapContentSize(Alignment.BottomStart)
        .offset(x = targetIndicatorOffset)
        // 在此应用,所以说在外边设置width等等都是不行的
        .width(indicatorWidth)
}

private inline val Dp.absoluteValue: Dp
    get() = value.absoluteValue.dp

既然已经知道官方的做法,只需要修改一下下划线宽度就行了呗

@ExperimentalPagerApi
fun Modifier.indicatorOffset(
    pagerState: PagerState,
    tabPositions: List<TabPosition>,
    width: Dp
): Modifier = composed {
    if (pagerState.pageCount == 0) return@composed this

    val targetIndicatorOffset: Dp
    val indicatorWidth: Dp

    val currentTab = tabPositions[minOf(tabPositions.lastIndex, pagerState.currentPage)]
    val targetPage = pagerState.targetPage
    val targetTab = tabPositions.getOrNull(targetPage)

    if (targetTab != null) {
        val targetDistance = (targetPage - pagerState.currentPage).absoluteValue
        val fraction = (pagerState.currentPageOffset / max(targetDistance, 1)).absoluteValue

        targetIndicatorOffset = lerp(currentTab.left, targetTab.left, fraction)
        indicatorWidth = lerp(currentTab.width, targetTab.width, fraction).absoluteValue
    } else {
        targetIndicatorOffset = currentTab.left
        indicatorWidth = currentTab.width
    }

    fillMaxWidth()
        .wrapContentSize(Alignment.BottomStart)
        .offset(x = targetIndicatorOffset)
        .width(width)
}

效果图

Screenshot_2022-03-09-13-11-16-668_com.hua.abstra.jpg 哎?你会发现此时咋这个下划线跑到最开始了,滑动后也对不上位置啊,宽度是对了,位置怎么不对劲。

这是因为官方的wrapContentSize(Alignment.BottomStart)在起作用

别急,之前官方计算的下划线宽度还是有用的,我们可以在设置宽度的同时,设置一下内边距,让其处于当中就行了。 那么我们只需要在这加上计算好的内边距即可。

    fillMaxWidth()
        .wrapContentSize(Alignment.BottomStart)
        .offset(x = targetIndicatorOffset)
        // 水平方向上,也就是左右都有,计算出的宽度减去我们要显示的宽度,再除以2就可以就得出了两边同时要间隔的大小,此时就可以正好在中间了
        .padding(horizontal = (indicatorWidth - width) / 2)
        .width(width)

效果图

Screenshot_2022-03-09-12-39-16-026_com.hua.abstra.jpg