我试图创建一个动画,其中有可滚动的组件,水平滚动.像这样的东西

enter image description here

我想使用可滚动的标签,它在一定程度上是有效的,但我仍然在想如何减少你在上面gif中看到的裁剪项之间的间距.

我都试了些什么?

@Composable
fun CropBar(onCropClicked: (Int) -> Unit) {
    var selectedIndex by remember { mutableStateOf(0) }
    val pages = listOf("kotlin", "java", "c#", "php", "golang","A","B","C")
    val colors = listOf(Color.Yellow, Color.Red, Color.White, Color.Blue, Color.Magenta)

    val indicator = @Composable { tabPositions: List<TabPosition> ->
        val color = when (selectedIndex) {
            0 -> colors[0]
            1 -> colors[1]
            2 -> colors[2]
            3 -> colors[3]
            else -> colors[4]
        }
        CustomIndicator(tabPositions = tabPositions, selectedIndex = selectedIndex, color)
    }
    ScrollableTabRow(
        modifier = Modifier
            .fillMaxWidth()
            .height(58.dp),
        selectedTabIndex = selectedIndex,
        containerColor = Color(0xFF03753C),
        indicator = indicator,
        edgePadding = 0.dp,
        divider = {
        },

        ) {
        pages.forEachIndexed { index, title ->

            Tab(
                modifier = Modifier
                    .height(58.dp)
                    .width(74.dp)
                    .zIndex(2f),
                selected = selectedIndex == index,
                onClick = {
                    selectedIndex = index
                    onCropClicked(index)
                },
                interactionSource = NoRippleInteractionSource()
            ) {

                SampleImage(selectedIndex)
            }
        }
    }

}

@Composable
private fun CustomIndicator(tabPositions: List<TabPosition>, selectedIndex: Int, color: Color) {

    val transition = updateTransition(selectedIndex, label = "transition")

    val indicatorStart by transition.animateDp(
        transitionSpec = {
            tween(
                durationMillis = 500,
                easing = LinearOutSlowInEasing
            )
        },
        label = ""
    ) {
        tabPositions[it].left
    }

    val indicatorEnd by transition.animateDp(
        transitionSpec = {
            tween(
                durationMillis = 500,
                easing = LinearOutSlowInEasing
            )
        },
        label = "",
    ) {
        tabPositions[it].right
    }
    Box(
        Modifier
            .padding(top = 8.dp)
            .offset(x = indicatorStart)
            .wrapContentSize(align = Alignment.BottomStart)
            .width(indicatorEnd - indicatorStart)
            .paint(
                // Replace with your image id
                painterResource(id = R.drawable.ic_test), // some background vector drawable image
                contentScale = ContentScale.FillWidth,
                colorFilter = ColorFilter.tint(color) // for tinting
            )
            .zIndex(1f)
    )
}

@Composable
fun SampleImage(selectedIndex: Int) {

    BoxWithConstraints(
        modifier = Modifier,
    ) {
        Image(
            modifier = Modifier
                .padding(top = 8.dp)
                .width(42.dp)
                .height(42.dp)
                .align(Alignment.BottomCenter),
            painter = painterResource(id = R.drawable.ic_img_round),
            contentDescription = "Image"
        )

        if(selectedIndex == 1) {
            Text(
                text = "180 Days",
                fontSize = 8.sp,
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .padding(top = 18.dp)
                    .width(42.dp)
                    .clip(RoundedCornerShape(10.dp))
                    .background(Color.Gray)
                    .graphicsLayer {
                        translationX = 5f
                    }
            )
        }
    }
}

class NoRippleInteractionSource : MutableInteractionSource {
    override val interactions: Flow<Interaction> = emptyFlow()
    override suspend fun emit(interaction: Interaction) {}
    override fun tryEmit(interaction: Interaction) = true
}

结果:代码只是一个粗略的样本.

enter image description here

预期结果:我应该能够控制选项卡项之间的间距.我不是在寻找只使用可滚动标签的解决方案.事实上,任何具有选定项的可滚动组件都有背景,并且可以将背景转换为新的选定项.我想到了在偏移量上使用类似于Row的方法,在一个偏移量处使用一个draBehind of Image,然后获得点击的项目位置,并将背景移到选定的项目上.还有其他解决方案或 idea 吗?

以防万一:https://issuetracker.google.com/issues/234942462

注:我向uiAutomaterview查看了Plantix应用程序.他们使用一个定制的水平高度滚动视图和一个边框布局.这些曲线是使用三次Bezier曲线的自定义路径.我猜测计算点击的裁剪或边界的偏移量,然后将背景视图移动到某个偏移量或从某个偏移量移出.

推荐答案

遗憾的是,tabRowWidth是一个固定值

private val ScrollableTabRowMinimumTabWidth = 90.dp

但这可以通过复制粘贴ScrollableTabRow源代码以及更改或不使用最小宽度的约束来轻松解决.

顶部的是默认宽度,底部的是我更改的最小宽度,可测量的宽度可以测量到0.dp

这意味着它可以用0-和最大值之间的任何值进行测量

结果

enter image description here

演示

@Preview
@Composable
private fun Test() {
    CropBar() {

    }
}

@Composable
fun CropBar(onCropClicked: (Int) -> Unit) {
    Column {

        Spacer(modifier = Modifier.height(20.dp))
        var selectedIndex by remember { mutableStateOf(0) }
        val pages = listOf("kotlin", "java", "c#", "php", "golang", "A", "B", "C")
        val colors = listOf(Color.Yellow, Color.Red, Color.White, Color.Blue, Color.Magenta)

        val indicator = @Composable { tabPositions: List<TabPosition> ->
            val color = when (selectedIndex) {
                0 -> colors[0]
                1 -> colors[1]
                2 -> colors[2]
                3 -> colors[3]
                else -> colors[4]
            }
            CustomIndicator(tabPositions = tabPositions, selectedIndex = selectedIndex, color)
        }
        MyScrollableTabRow(
            modifier = Modifier
                .fillMaxWidth()
                .height(58.dp),
            selectedTabIndex = selectedIndex,
            backgroundColor = Color(0xFF03753C),
            indicator = indicator,
            edgePadding = 0.dp,
            divider = {
            },

            ) {
            pages.forEachIndexed { index, title ->

                Tab(
                    modifier = Modifier
                        .height(58.dp)
                        .width(74.dp)
                        .zIndex(2f),
                    selected = selectedIndex == index,
                    onClick = {
                        selectedIndex = index
                        onCropClicked(index)
                    },
                    interactionSource = NoRippleInteractionSource()
                ) {

                    SampleImage(selectedIndex)
                }
            }
        }

        Spacer(modifier = Modifier.height(20.dp))

        MyScrollableTabRow(
            modifier = Modifier
                .fillMaxWidth()
                .height(58.dp),
            selectedTabIndex = selectedIndex,
            backgroundColor = Color(0xFF03753C),
            indicator = indicator,
            minItemWidth = 0.dp,
            edgePadding = 0.dp,
            divider = {
            },

            ) {
            pages.forEachIndexed { index, title ->

                Tab(
                    modifier = Modifier
                        .height(58.dp)
                        .width(74.dp)
                        .zIndex(2f),
                    selected = selectedIndex == index,
                    onClick = {
                        selectedIndex = index
                        onCropClicked(index)
                    },
                    interactionSource = NoRippleInteractionSource()
                ) {

                    SampleImage(selectedIndex)
                }
            }
        }
    }
}

实施

@Composable
@UiComposable
fun MyScrollableTabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    minItemWidth:Dp =ScrollableTabRowMinimumTabWidth,
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    edgePadding: Dp = TabRowDefaults.ScrollableTabRowPadding,
    indicator: @Composable @UiComposable
        (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
        TabRowDefaults.Indicator(
            Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
        )
    },
    divider: @Composable @UiComposable () -> Unit =
        @Composable {
            TabRowDefaults.Divider()
        },
    tabs: @Composable @UiComposable () -> Unit
) {
    Surface(
        modifier = modifier,
        color = backgroundColor,
        contentColor = contentColor
    ) {
        val scrollState = rememberScrollState()
        val coroutineScope = rememberCoroutineScope()
        val scrollableTabData = remember(scrollState, coroutineScope) {
            ScrollableTabData(
                scrollState = scrollState,
                coroutineScope = coroutineScope
            )
        }
        SubcomposeLayout(
            Modifier.fillMaxWidth()
                .wrapContentSize(align = Alignment.CenterStart)
                .horizontalScroll(scrollState)
                .selectableGroup()
                .clipToBounds()
        ) { constraints ->

            // ? Change this to 0 or
            val minTabWidth = minItemWidth.roundToPx()
            val padding = edgePadding.roundToPx()
            // ?or use constraints to measure each tab with its own width or
            // a another value instead of them having at least 90.dp
            val tabConstraints = constraints.copy(minWidth = minTabWidth)

            val tabPlaceables = subcompose(com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.TabSlots.Tabs, tabs)
                .map { it.measure(tabConstraints) }

            var layoutWidth = padding * 2
            var layoutHeight = 0
            tabPlaceables.forEach {
                layoutWidth += it.width
                layoutHeight = maxOf(layoutHeight, it.height)
            }

            // Position the children.
            layout(layoutWidth, layoutHeight) {
                // Place the tabs
                val tabPositions = mutableListOf<TabPosition>()
                var left = padding
                tabPlaceables.forEach {
                    it.placeRelative(left, 0)
                    tabPositions.add(TabPosition(left = left.toDp(), width = it.width.toDp()))
                    left += it.width
                }

                // The divider is measured with its own height, and width equal to the total width
                // of the tab row, and then placed on top of the tabs.
                subcompose(com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.TabSlots.Divider, divider).forEach {
                    val placeable = it.measure(
                        constraints.copy(
                            minHeight = 0,
                            minWidth = layoutWidth,
                            maxWidth = layoutWidth
                        )
                    )
                    placeable.placeRelative(0, layoutHeight - placeable.height)
                }

                // The indicator container is measured to fill the entire space occupied by the tab
                // row, and then placed on top of the divider.
                subcompose(com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.TabSlots.Indicator) {
                    indicator(tabPositions)
                }.forEach {
                    it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0)
                }

                scrollableTabData.onLaidOut(
                    density = this@SubcomposeLayout,
                    edgeOffset = padding,
                    tabPositions = tabPositions,
                    selectedTab = selectedTabIndex
                )
            }
        }
    }
}

@Immutable
class TabPosition internal constructor(val left: Dp, val width: Dp) {
    val right: Dp get() = left + width

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is TabPosition) return false

        if (left != other.left) return false
        if (width != other.width) return false

        return true
    }

    override fun hashCode(): Int {
        var result = left.hashCode()
        result = 31 * result + width.hashCode()
        return result
    }

    override fun toString(): String {
        return "TabPosition(left=$left, right=$right, width=$width)"
    }
}

object TabRowDefaults {
    /**
     * Default [Divider], which will be positioned at the bottom of the [TabRow], underneath the
     * indicator.
     *
     * @param modifier modifier for the divider's layout
     * @param thickness thickness of the divider
     * @param color color of the divider
     */
    @Composable
    fun Divider(
        modifier: Modifier = Modifier,
        thickness: Dp = DividerThickness,
        color: Color = LocalContentColor.current.copy(alpha = DividerOpacity)
    ) {
        androidx.compose.material.Divider(modifier = modifier, thickness = thickness, color = color)
    }

    /**
     * Default indicator, which will be positioned at the bottom of the [TabRow], on top of the
     * divider.
     *
     * @param modifier modifier for the indicator's layout
     * @param height height of the indicator
     * @param color color of the indicator
     */
    @Composable
    fun Indicator(
        modifier: Modifier = Modifier,
        height: Dp = IndicatorHeight,
        color: Color = LocalContentColor.current
    ) {
        Box(
            modifier
                .fillMaxWidth()
                .height(height)
                .background(color = color)
        )
    }

    /**
     * [Modifier] that takes up all the available width inside the [TabRow], and then animates
     * the offset of the indicator it is applied to, depending on the [currentTabPosition].
     *
     * @param currentTabPosition [TabPosition] of the currently selected tab. This is used to
     * calculate the offset of the indicator this modifier is applied to, as well as its width.
     */
    fun Modifier.tabIndicatorOffset(
        currentTabPosition: TabPosition
    ): Modifier = composed(
        inspectorInfo = debugInspectorInfo {
            name = "tabIndicatorOffset"
            value = currentTabPosition
        }
    ) {
        val currentTabWidth by animateDpAsState(
            targetValue = currentTabPosition.width,
            animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
        )
        val indicatorOffset by animateDpAsState(
            targetValue = currentTabPosition.left,
            animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
        )
        fillMaxWidth()
            .wrapContentSize(Alignment.BottomStart)
            .offset(x = indicatorOffset)
            .width(currentTabWidth)
    }

    /**
     * Default opacity for the color of [Divider]
     */
    const val DividerOpacity = 0.12f

    /**
     * Default thickness for [Divider]
     */
    val DividerThickness = 1.dp

    /**
     * Default height for [Indicator]
     */
    val IndicatorHeight = 2.dp

    /**
     * The default padding from the starting edge before a tab in a [ScrollableTabRow].
     */
    val ScrollableTabRowPadding = 52.dp
}

private enum class TabSlots {
    Tabs,
    Divider,
    Indicator
}

/**
 * Class holding onto state needed for [ScrollableTabRow]
 */
private class ScrollableTabData(
    private val scrollState: ScrollState,
    private val coroutineScope: CoroutineScope
) {
    private var selectedTab: Int? = null

    fun onLaidOut(
        density: Density,
        edgeOffset: Int,
        tabPositions: List<TabPosition>,
        selectedTab: Int
    ) {
        // Animate if the new tab is different from the old tab, or this is called for the first
        // time (i.e selectedTab is `null`).
        if (this.selectedTab != selectedTab) {
            this.selectedTab = selectedTab
            tabPositions.getOrNull(selectedTab)?.let {
                // Scrolls to the tab with [tabPosition], trying to place it in the center of the
                // screen or as close to the center as possible.
                val calculatedOffset = it.calculateTabOffset(density, edgeOffset, tabPositions)
                if (scrollState.value != calculatedOffset) {
                    coroutineScope.launch {
                        scrollState.animateScrollTo(
                            calculatedOffset,
                            animationSpec = ScrollableTabRowScrollSpec
                        )
                    }
                }
            }
        }
    }

    /**
     * @return the offset required to horizontally center the tab inside this TabRow.
     * If the tab is at the start / end, and there is not enough space to fully centre the tab, this
     * will just clamp to the min / max position given the max width.
     */
    private fun TabPosition.calculateTabOffset(
        density: Density,
        edgeOffset: Int,
        tabPositions: List<TabPosition>
    ): Int = with(density) {
        val totalTabRowWidth = tabPositions.last().right.roundToPx() + edgeOffset
        val visibleWidth = totalTabRowWidth - scrollState.maxValue
        val tabOffset = left.roundToPx()
        val scrollerCenter = visibleWidth / 2
        val tabWidth = width.roundToPx()
        val centeredTabOffset = tabOffset - (scrollerCenter - tabWidth / 2)
        // How much space we have to scroll. If the visible width is <= to the total width, then
        // we have no space to scroll as everything is always visible.
        val availableSpace = (totalTabRowWidth - visibleWidth).coerceAtLeast(0)
        return centeredTabOffset.coerceIn(0, availableSpace)
    }
}

private val ScrollableTabRowMinimumTabWidth = 90.dp

/**
 * [AnimationSpec] used when scrolling to a tab that is not fully visible.
 */
private val ScrollableTabRowScrollSpec: AnimationSpec<Float> = tween(
    durationMillis = 250,
    easing = FastOutSlowInEasing
)

Android相关问答推荐

以编程方式更改Android应用程序上的文件许可

即使安装了Chrome和YouTube,Android对action_view a YouTube URL的意图也是空的

Android Studio中的Kotlin版本不兼容错误:需要元数据1.9.0,但找到1.6.0

strings.xml中字符串数组中的占位符

房间@嵌入式VS一对一关系

Android Studio SQLite 错误:列不正确(没有这样的列:_id)

如何在卡片视图右侧添加箭头

有没有办法让协程通道在接收时遵循特定的顺序而不是先进先出

块不起作用,出现 "只有安全的 (?.)... " 错误

Android Jetpack Compose全宽度抽屉优化

Jetpack Compose with Paging 3 发出太多网络请求

如何在 Jetpack Compose 中创建无限pager

验证硬编码密码

Jetpack Compose Material3 禁用 ListItem

使用 capacitor cordova 插件的 Android Studio 错误

Android Studio xml 预览问题无法初始化编辑器

Android Compose 创建抖动动画

在android studio中使用wifi配对设备的问题

禁用通知权限后启动前台服务导致崩溃(Android 13)

如何在 Kotlin 的片段中制作图像列表?