减少撰写中可滚动选项卡之间的间距

2024-03-10

我正在尝试创建一个动画,其中有水平滚动的可滚动组件。就像是

我想过使用可滚动选项卡,它在某种程度上有效,除了我仍在研究如何减少您在上面的 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
}

结果:代码只是一个粗略的示例。

期望的结果:我应该能够控制选项卡项目之间的间距。我不是在寻找仅使用可滚动选项卡的解决方案。事实上,任何具有背景的选定项目的可滚动组件并将背景转换为新选定的项目都是可以的。我想到使用像 Row 这样的东西,在图像的后面绘制一个偏移量,然后获取单击的项目位置并将背景移动到选定的项目。还有其他解决方案或想法吗?

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

注意:我使用 uiautomaterviewer 检查 plantix 应用程序。他们使用自定义水平滚动视图并使用框架布局。这些曲线是使用三次贝塞尔曲线的自定义路径。我猜想计算单击的裁剪或边界的偏移量,然后将背景视图移至某个偏移量或从某个偏移量移开。


不幸的是,最小宽度选项卡是用固定值测量的

private val ScrollableTabRowMinimumTabWidth = 90.dp

但这可以通过复制粘贴 ScrollableTabRow 源代码并更改它或不使用最小宽度的约束来更新。

顶部的一个具有默认宽度,而对于底部的一个,我更改了最小宽度,可测量的可测量为 0.dp

这意味着它可以用 0 到最大值之间的任何值来测量

Result

Demo

@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
)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

减少撰写中可滚动选项卡之间的间距 的相关文章

  • 如何使用android canvas绘制一个只有左上角和右上角为圆角的矩形?

    我找到了一个用于所有 4 个角均为圆角的矩形的函数 但我只想将顶部 2 个角设为圆角 我能做些什么 canvas drawRoundRect new RectF 0 100 100 300 6 6 paint 对于 API 21 及更高版本
  • 在 JellyBean 上使用 LogCat

    我有一个非常随机发生的错误 所以我依赖LogCat我从 Play 商店购买的监控应用程序 以查看发生时设备上抛出的异常 自从使用 Jelly Bean 以来 我没有看到任何日志记录 我读过 使用 Jelly Bean 应用程序只能看到Log
  • ProGuard SimException

    经过大约 4 个小时的随机尝试 我正在尝试让 ProGuard 正常工作 尝试让这个令人惊叹的软件正常工作 我的项目使用 LibGDX 和 KryoNet 这是我当前的 ProGuard 配置 verbose dontobfuscate d
  • 如何在 Android 上以编程方式截取屏幕截图?

    如何不通过任何程序而是通过代码截取手机屏幕的选定区域的屏幕截图 以下代码允许将我的屏幕截图存储在 SD 卡上 并在以后满足您的任何需求 首先 您需要添加适当的权限来保存文件
  • 使用react-native测量音频的响度

    我正在创建一个应用程序 Android 来使用本机反应录制手机中的语音 一项要求是实时测量声音的响度并基于它制作动画 我尝试使用react native audio库 但问题是响度监控仅在IOS中支持 我检查了世博会音频库 但找不到方法 有
  • Android 全屏对话框片段(如日历应用程序)

    我正在尝试实现如下图所示的全屏对话框 我能够显示全屏对话框 但是当显示对话框时 状态栏颜色变为黑色并且不保留原色深色 这是我的对话片段 public class IconsDialogFragment extends DialogFragm
  • 从本机代码访问 AsyncStorage

    我需要使用 JS 代码中的 AsyncStorage 将数据保存在本地存储中 我想知道是否有一种方法可以从本机代码 Objective C 或 Java 访问 AsyncStorage 存储的数据 Thanks 如果你导入RCTAsyncL
  • 按下按钮时清除编辑文本焦点并隐藏键盘

    我正在制作一个带有编辑文本和按钮的应用程序 当我在 edittext 中输入内容然后单击按钮时 我希望键盘和焦点在 edittext 上消失 但我似乎无法做到这一点 我在 XML 中插入了这两行代码 android focusable tr
  • 从 Bitmap 类创建 .bmp 图像文件

    我创建了一个使用套接字的应用程序 客户端在其中接收图像并将图像数据存储在 Bitmap 类中 谁能告诉我如何创建一个名为我的图像 png or 我的图像 bmp来自此 Bitmap 对象 String base64Code dataInpu
  • Gradle 控制台 - 获取更多日志输出

    我正在使用Android Studio 在 gradle 控制台中收到以下错误消息 编译错误 查看日志了解更多详情 尝试 使用 stacktrace 选项运行以获取堆栈跟踪 使用 info 或 debug 选项运行以获得更多日志输出 构建失
  • 将 REST 服务与 Android 应用程序同步

    我使用一个 REST 服务来填充数据库中的信息 稍后由我的应用程序使用 我已经阅读了有关此事的多个主题 现在必须决定如何在 REST 服务和数据库之间实现同步 想象一个应用程序 它从谷歌金融 API 获取有关股票的信息并将其存储在数据库中
  • Android Studio 无法解析存储库

    在我的项目中 我尝试使用设计支持库 我的 Gradle 文件中有 dependencies compile com android support design 当我尝试构建这个时 我收到错误 通常我会点击Install Repositor
  • TextView位于屏幕中央[重复]

    这个问题在这里已经有答案了 可能的重复 如何在 Android 中的 TextView 中水平和垂直居中文本 https stackoverflow com questions 432037 how do i center text hor
  • 从 Android 模拟器使用 WebView WebGL

    据我了解 WebGL 仅在 Android Lollipop 中的 WebView 更新 Play 商店中的 WebView 组件 和较新版本 无需 Play 商店更新 中受支持 但是 我有一个使用 Android 7 1 1 的模拟器 并
  • 提交后折叠搜索视图

    我在我的应用程序中使用 searchview 没有操作栏 提交查询文本后如何折叠搜索视图 我有这些听众 Override public boolean onQueryTextSubmit String query InputMethodMa
  • java.lang.UnsupportedOperationException:无法解析索引 13 处的属性:TypedValue{t=0x2/d=0x7f010046 a=-1}

    我在 android attrs xml 文件中添加了一个用于不同色调的属性 在 styles xml 文件中 我为这些属性指定了颜色 因此每种样式的它们都不同 Attrs xml
  • 使用远程数据编写 Android、iPad、iPhone 客户端的技术

    我需要探索世界 你写了一个杀手级应用程序 但你有 Android iPhone iPad 客户端吗 我的问题是 1 向这些设备发送数据的最佳方式是什么 按照建议进行肥皂和休息here https stackoverflow com ques
  • 文本转语音无法在 Android 设备上运行

    下面是我的代码 我无法在 Kitkat 设备中听到声音 Toast 出现 但声音没有播放 我正在遵循本教程 https www tutorialspoint com android android text to speech htm ht
  • 在 Android 中使用 Fragment 时处理后按

    我在应用程序中使用 Android 滑动菜单和导航抽屉 并且在应用程序中使用片段而不是活动 当我打开抽屉时 单击一个项目会出现一个片段 我使用以下代码从一个片段移动到另一个片段 Fragment fragment null fragment
  • Android 2.2 中不带预览的相机捕获

    我需要捕获图像而不显示预览 我想在后台作为服务来完成它 可以这样做吗 是有可能实现的 您应该定义一个处理 Camera 对象的类 例如调用 Camera open 等 不要为相机对象提供以下行以禁用预览 mCamera setPreview

随机推荐