减小Compose中可滚动选项卡之间的间距。

huangapple go评论98阅读模式
英文:

Reduce Spacing between Scrollable tabs in compose

问题

我正在尝试创建一个动画,其中有一个可滚动的组件,可以横向滚动。类似于下面的示例:

我考虑使用可滚动选项卡,并且在某种程度上它确实有效,只是我仍在努力找出如何减小上面gif中看到的裁剪项之间的间距。

我尝试过的内容:

  1. @Composable
  2. fun CropBar(onCropClicked: (Int) -> Unit) {
  3. // ... (代码太长,省略部分)
  4. }

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

期望结果:我应该能够控制选项卡项之间的间距。我不仅寻求使用可滚动选项卡的解决方案。事实上,任何可滚动组件,其中选定的项目具有背景并将背景过渡到新选定的项目都可以。我考虑使用类似于Row的东西,具有图像的drawBehind,位于偏移位置,然后获取点击项的位置并将背景移动到所选项目。还有其他解决方案或想法吗?

如果有帮助的话,可以查看这个链接:https://issuetracker.google.com/issues/234942462

注意:我使用uiautomaterviewer检查了plantix应用程序。他们使用了自定义的水平滚动视图和帧布局。曲线是使用三次贝塞尔曲线的自定义路径。我猜他们计算了点击的裁剪或边界的偏移量,然后将背景视图移动到某个偏移量。

英文:

I am trying to create a animation where there is scrollable component that scrolls horizontally. Something like

减小Compose中可滚动选项卡之间的间距。

I thought of using Scrollable tabs and it works to some extent except, I am still figuring out how to reduce space between the crop items that you see in the above gif

What I have tried?

  1. @Composable
  2. fun CropBar(onCropClicked: (Int) -> Unit) {
  3. var selectedIndex by remember { mutableStateOf(0) }
  4. val pages = listOf("kotlin", "java", "c#", "php", "golang","A","B","C")
  5. val colors = listOf(Color.Yellow, Color.Red, Color.White, Color.Blue, Color.Magenta)
  6. val indicator = @Composable { tabPositions: List<TabPosition> ->
  7. val color = when (selectedIndex) {
  8. 0 -> colors[0]
  9. 1 -> colors[1]
  10. 2 -> colors[2]
  11. 3 -> colors[3]
  12. else -> colors[4]
  13. }
  14. CustomIndicator(tabPositions = tabPositions, selectedIndex = selectedIndex, color)
  15. }
  16. ScrollableTabRow(
  17. modifier = Modifier
  18. .fillMaxWidth()
  19. .height(58.dp),
  20. selectedTabIndex = selectedIndex,
  21. containerColor = Color(0xFF03753C),
  22. indicator = indicator,
  23. edgePadding = 0.dp,
  24. divider = {
  25. },
  26. ) {
  27. pages.forEachIndexed { index, title ->
  28. Tab(
  29. modifier = Modifier
  30. .height(58.dp)
  31. .width(74.dp)
  32. .zIndex(2f),
  33. selected = selectedIndex == index,
  34. onClick = {
  35. selectedIndex = index
  36. onCropClicked(index)
  37. },
  38. interactionSource = NoRippleInteractionSource()
  39. ) {
  40. SampleImage(selectedIndex)
  41. }
  42. }
  43. }
  44. }
  45. @Composable
  46. private fun CustomIndicator(tabPositions: List<TabPosition>, selectedIndex: Int, color: Color) {
  47. val transition = updateTransition(selectedIndex, label = "transition")
  48. val indicatorStart by transition.animateDp(
  49. transitionSpec = {
  50. tween(
  51. durationMillis = 500,
  52. easing = LinearOutSlowInEasing
  53. )
  54. },
  55. label = ""
  56. ) {
  57. tabPositions[it].left
  58. }
  59. val indicatorEnd by transition.animateDp(
  60. transitionSpec = {
  61. tween(
  62. durationMillis = 500,
  63. easing = LinearOutSlowInEasing
  64. )
  65. },
  66. label = "",
  67. ) {
  68. tabPositions[it].right
  69. }
  70. Box(
  71. Modifier
  72. .padding(top = 8.dp)
  73. .offset(x = indicatorStart)
  74. .wrapContentSize(align = Alignment.BottomStart)
  75. .width(indicatorEnd - indicatorStart)
  76. .paint(
  77. // Replace with your image id
  78. painterResource(id = R.drawable.ic_test), // some background vector drawable image
  79. contentScale = ContentScale.FillWidth,
  80. colorFilter = ColorFilter.tint(color) // for tinting
  81. )
  82. .zIndex(1f)
  83. )
  84. }
  85. @Composable
  86. fun SampleImage(selectedIndex: Int) {
  87. BoxWithConstraints(
  88. modifier = Modifier,
  89. ) {
  90. Image(
  91. modifier = Modifier
  92. .padding(top = 8.dp)
  93. .width(42.dp)
  94. .height(42.dp)
  95. .align(Alignment.BottomCenter),
  96. painter = painterResource(id = R.drawable.ic_img_round),
  97. contentDescription = "Image"
  98. )
  99. if(selectedIndex == 1) {
  100. Text(
  101. text = "180 Days",
  102. fontSize = 8.sp,
  103. modifier = Modifier
  104. .align(Alignment.BottomCenter)
  105. .padding(top = 18.dp)
  106. .width(42.dp)
  107. .clip(RoundedCornerShape(10.dp))
  108. .background(Color.Gray)
  109. .graphicsLayer {
  110. translationX = 5f
  111. }
  112. )
  113. }
  114. }
  115. }
  116. class NoRippleInteractionSource : MutableInteractionSource {
  117. override val interactions: Flow<Interaction> = emptyFlow()
  118. override suspend fun emit(interaction: Interaction) {}
  119. override fun tryEmit(interaction: Interaction) = true
  120. }

Result : The code is just a rough sample.

减小Compose中可滚动选项卡之间的间距。

Desired Result : I should be able to control the spacing between tab items. I am not looking for solution using only scrollable tabs. In fact any scrollable component with selected item having a background and transitioning the background to new selected item is okay. I thought of using something like Row with a drawBehind of Image at a offset and then get the clicked item position and move the background to selected Items. Any other solution or ideas?

Just in case it helps : https://issuetracker.google.com/issues/234942462

Note: I check with uiautomaterviewer the plantix app. They use a a custom horizontall scrollview and they use a framelayout. The curves are custom path using cubic bezier curve. I guess the calculate offset of clicked crop or bounds and then move the background view to and from a certain offset.

答案1

得分: 3

以下是您要翻译的代码部分:

  1. private val ScrollableTabRowMinimumTabWidth = 90.dp
  2. 但是可以通过复制粘贴ScrollableTabRow源代码并更改此值或不使用具有最小宽度的约束来更新此值
  3. 顶部的一个是默认宽度的底部的一个是我更改了最小宽度可以测量为0.dp这意味着它可以使用0到最大值之间的任何值来测量
  4. 结果
  5. [![enter image description here][1]][1]
  6. 演示
  7. @Preview
  8. @Composable
  9. private fun Test() {
  10. CropBar() {
  11. }
  12. }
  13. @Composable
  14. fun CropBar(onCropClicked: (Int) -> Unit) {
  15. Column {
  16. Spacer(modifier = Modifier.height(20.dp))
  17. var selectedIndex by remember { mutableStateOf(0) }
  18. val pages = listOf("kotlin", "java", "c#", "php", "golang", "A", "B", "C")
  19. val colors = listOf(Color.Yellow, Color.Red, Color.White, Color.Blue, Color.Magenta)
  20. val indicator = @Composable { tabPositions: List<TabPosition> ->
  21. val color = when (selectedIndex) {
  22. 0 -> colors[0]
  23. 1 -> colors[1]
  24. 2 -> colors[2]
  25. 3 -> colors[3]
  26. else -> colors[4]
  27. }
  28. CustomIndicator(tabPositions = tabPositions, selectedIndex = selectedIndex, color)
  29. }
  30. MyScrollableTabRow(
  31. modifier = Modifier
  32. .fillMaxWidth()
  33. .height(58.dp),
  34. selectedTabIndex = selectedIndex,
  35. backgroundColor = Color(0xFF03753C),
  36. indicator = indicator,
  37. edgePadding = 0.dp,
  38. divider = {
  39. },
  40. ) {
  41. pages.forEachIndexed { index, title ->
  42. Tab(
  43. modifier = Modifier
  44. .height(58.dp)
  45. .width(74.dp)
  46. .zIndex(2f),
  47. selected = selectedIndex == index,
  48. onClick = {
  49. selectedIndex = index
  50. onCropClicked(index)
  51. },
  52. interactionSource = NoRippleInteractionSource()
  53. ) {
  54. SampleImage(selectedIndex)
  55. }
  56. }
  57. }
  58. Spacer(modifier = Modifier.height(20.dp))
  59. MyScrollableTabRow(
  60. modifier = Modifier
  61. .fillMaxWidth()
  62. .height(58.dp),
  63. selectedTabIndex = selectedIndex,
  64. backgroundColor = Color(0xFF03753C),
  65. indicator = indicator,
  66. minItemWidth = 0.dp,
  67. edgePadding = 0.dp,
  68. divider = {
  69. },
  70. ) {
  71. pages.forEachIndexed { index, title ->
  72. Tab(
  73. modifier = Modifier
  74. .height(58.dp)
  75. .width(74.dp)
  76. .zIndex(2f),
  77. selected = selectedIndex == index,
  78. onClick = {
  79. selectedIndex = index
  80. onCropClicked(index)
  81. },
  82. interactionSource = NoRippleInteractionSource()
  83. ) {
  84. SampleImage(selectedIndex)
  85. }
  86. }
  87. }
  88. }
  89. }
  90. // 其他代码部分已省略

请注意,代码中包含一些HTML实体,如&quot;,这些应该在使用代码时转义或修复。

英文:

Unfortunately, minumum width tabs are measured with is a fixed value

  1. private val ScrollableTabRowMinimumTabWidth = 90.dp

but this can be updated by copy pasting ScrollableTabRow source code and changing this or not using a Constraints with minimum width.

The one on top is with default width and for the one at the bottom i changed minimum width a Measurable can be measured to 0.dp

which means it can be measured with any value between 0-and max

Result

减小Compose中可滚动选项卡之间的间距。

Demo

  1. @Preview
  2. @Composable
  3. private fun Test() {
  4. CropBar() {
  5. }
  6. }
  7. @Composable
  8. fun CropBar(onCropClicked: (Int) -&gt; Unit) {
  9. Column {
  10. Spacer(modifier = Modifier.height(20.dp))
  11. var selectedIndex by remember { mutableStateOf(0) }
  12. val pages = listOf(&quot;kotlin&quot;, &quot;java&quot;, &quot;c#&quot;, &quot;php&quot;, &quot;golang&quot;, &quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
  13. val colors = listOf(Color.Yellow, Color.Red, Color.White, Color.Blue, Color.Magenta)
  14. val indicator = @Composable { tabPositions: List&lt;TabPosition&gt; -&gt;
  15. val color = when (selectedIndex) {
  16. 0 -&gt; colors[0]
  17. 1 -&gt; colors[1]
  18. 2 -&gt; colors[2]
  19. 3 -&gt; colors[3]
  20. else -&gt; colors[4]
  21. }
  22. CustomIndicator(tabPositions = tabPositions, selectedIndex = selectedIndex, color)
  23. }
  24. MyScrollableTabRow(
  25. modifier = Modifier
  26. .fillMaxWidth()
  27. .height(58.dp),
  28. selectedTabIndex = selectedIndex,
  29. backgroundColor = Color(0xFF03753C),
  30. indicator = indicator,
  31. edgePadding = 0.dp,
  32. divider = {
  33. },
  34. ) {
  35. pages.forEachIndexed { index, title -&gt;
  36. Tab(
  37. modifier = Modifier
  38. .height(58.dp)
  39. .width(74.dp)
  40. .zIndex(2f),
  41. selected = selectedIndex == index,
  42. onClick = {
  43. selectedIndex = index
  44. onCropClicked(index)
  45. },
  46. interactionSource = NoRippleInteractionSource()
  47. ) {
  48. SampleImage(selectedIndex)
  49. }
  50. }
  51. }
  52. Spacer(modifier = Modifier.height(20.dp))
  53. MyScrollableTabRow(
  54. modifier = Modifier
  55. .fillMaxWidth()
  56. .height(58.dp),
  57. selectedTabIndex = selectedIndex,
  58. backgroundColor = Color(0xFF03753C),
  59. indicator = indicator,
  60. minItemWidth = 0.dp,
  61. edgePadding = 0.dp,
  62. divider = {
  63. },
  64. ) {
  65. pages.forEachIndexed { index, title -&gt;
  66. Tab(
  67. modifier = Modifier
  68. .height(58.dp)
  69. .width(74.dp)
  70. .zIndex(2f),
  71. selected = selectedIndex == index,
  72. onClick = {
  73. selectedIndex = index
  74. onCropClicked(index)
  75. },
  76. interactionSource = NoRippleInteractionSource()
  77. ) {
  78. SampleImage(selectedIndex)
  79. }
  80. }
  81. }
  82. }
  83. }

Implementation

  1. @Composable
  2. @UiComposable
  3. fun MyScrollableTabRow(
  4. selectedTabIndex: Int,
  5. modifier: Modifier = Modifier,
  6. minItemWidth:Dp =ScrollableTabRowMinimumTabWidth,
  7. backgroundColor: Color = MaterialTheme.colors.primarySurface,
  8. contentColor: Color = contentColorFor(backgroundColor),
  9. edgePadding: Dp = TabRowDefaults.ScrollableTabRowPadding,
  10. indicator: @Composable @UiComposable
  11. (tabPositions: List&lt;TabPosition&gt;) -&gt; Unit = @Composable { tabPositions -&gt;
  12. TabRowDefaults.Indicator(
  13. Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
  14. )
  15. },
  16. divider: @Composable @UiComposable () -&gt; Unit =
  17. @Composable {
  18. TabRowDefaults.Divider()
  19. },
  20. tabs: @Composable @UiComposable () -&gt; Unit
  21. ) {
  22. Surface(
  23. modifier = modifier,
  24. color = backgroundColor,
  25. contentColor = contentColor
  26. ) {
  27. val scrollState = rememberScrollState()
  28. val coroutineScope = rememberCoroutineScope()
  29. val scrollableTabData = remember(scrollState, coroutineScope) {
  30. ScrollableTabData(
  31. scrollState = scrollState,
  32. coroutineScope = coroutineScope
  33. )
  34. }
  35. SubcomposeLayout(
  36. Modifier.fillMaxWidth()
  37. .wrapContentSize(align = Alignment.CenterStart)
  38. .horizontalScroll(scrollState)
  39. .selectableGroup()
  40. .clipToBounds()
  41. ) { constraints -&gt;
  42. // &#128293; Change this to 0 or
  43. val minTabWidth = minItemWidth.roundToPx()
  44. val padding = edgePadding.roundToPx()
  45. // &#128293;or use constraints to measure each tab with its own width or
  46. // a another value instead of them having at least 90.dp
  47. val tabConstraints = constraints.copy(minWidth = minTabWidth)
  48. val tabPlaceables = subcompose(com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.TabSlots.Tabs, tabs)
  49. .map { it.measure(tabConstraints) }
  50. var layoutWidth = padding * 2
  51. var layoutHeight = 0
  52. tabPlaceables.forEach {
  53. layoutWidth += it.width
  54. layoutHeight = maxOf(layoutHeight, it.height)
  55. }
  56. // Position the children.
  57. layout(layoutWidth, layoutHeight) {
  58. // Place the tabs
  59. val tabPositions = mutableListOf&lt;TabPosition&gt;()
  60. var left = padding
  61. tabPlaceables.forEach {
  62. it.placeRelative(left, 0)
  63. tabPositions.add(TabPosition(left = left.toDp(), width = it.width.toDp()))
  64. left += it.width
  65. }
  66. // The divider is measured with its own height, and width equal to the total width
  67. // of the tab row, and then placed on top of the tabs.
  68. subcompose(com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.TabSlots.Divider, divider).forEach {
  69. val placeable = it.measure(
  70. constraints.copy(
  71. minHeight = 0,
  72. minWidth = layoutWidth,
  73. maxWidth = layoutWidth
  74. )
  75. )
  76. placeable.placeRelative(0, layoutHeight - placeable.height)
  77. }
  78. // The indicator container is measured to fill the entire space occupied by the tab
  79. // row, and then placed on top of the divider.
  80. subcompose(com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.TabSlots.Indicator) {
  81. indicator(tabPositions)
  82. }.forEach {
  83. it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0)
  84. }
  85. scrollableTabData.onLaidOut(
  86. density = this@SubcomposeLayout,
  87. edgeOffset = padding,
  88. tabPositions = tabPositions,
  89. selectedTab = selectedTabIndex
  90. )
  91. }
  92. }
  93. }
  94. }
  95. @Immutable
  96. class TabPosition internal constructor(val left: Dp, val width: Dp) {
  97. val right: Dp get() = left + width
  98. override fun equals(other: Any?): Boolean {
  99. if (this === other) return true
  100. if (other !is TabPosition) return false
  101. if (left != other.left) return false
  102. if (width != other.width) return false
  103. return true
  104. }
  105. override fun hashCode(): Int {
  106. var result = left.hashCode()
  107. result = 31 * result + width.hashCode()
  108. return result
  109. }
  110. override fun toString(): String {
  111. return &quot;TabPosition(left=$left, right=$right, width=$width)&quot;
  112. }
  113. }
  114. object TabRowDefaults {
  115. /**
  116. * Default [Divider], which will be positioned at the bottom of the [TabRow], underneath the
  117. * indicator.
  118. *
  119. * @param modifier modifier for the divider&#39;s layout
  120. * @param thickness thickness of the divider
  121. * @param color color of the divider
  122. */
  123. @Composable
  124. fun Divider(
  125. modifier: Modifier = Modifier,
  126. thickness: Dp = DividerThickness,
  127. color: Color = LocalContentColor.current.copy(alpha = DividerOpacity)
  128. ) {
  129. androidx.compose.material.Divider(modifier = modifier, thickness = thickness, color = color)
  130. }
  131. /**
  132. * Default indicator, which will be positioned at the bottom of the [TabRow], on top of the
  133. * divider.
  134. *
  135. * @param modifier modifier for the indicator&#39;s layout
  136. * @param height height of the indicator
  137. * @param color color of the indicator
  138. */
  139. @Composable
  140. fun Indicator(
  141. modifier: Modifier = Modifier,
  142. height: Dp = IndicatorHeight,
  143. color: Color = LocalContentColor.current
  144. ) {
  145. Box(
  146. modifier
  147. .fillMaxWidth()
  148. .height(height)
  149. .background(color = color)
  150. )
  151. }
  152. /**
  153. * [Modifier] that takes up all the available width inside the [TabRow], and then animates
  154. * the offset of the indicator it is applied to, depending on the [currentTabPosition].
  155. *
  156. * @param currentTabPosition [TabPosition] of the currently selected tab. This is used to
  157. * calculate the offset of the indicator this modifier is applied to, as well as its width.
  158. */
  159. fun Modifier.tabIndicatorOffset(
  160. currentTabPosition: TabPosition
  161. ): Modifier = composed(
  162. inspectorInfo = debugInspectorInfo {
  163. name = &quot;tabIndicatorOffset&quot;
  164. value = currentTabPosition
  165. }
  166. ) {
  167. val currentTabWidth by animateDpAsState(
  168. targetValue = currentTabPosition.width,
  169. animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
  170. )
  171. val indicatorOffset by animateDpAsState(
  172. targetValue = currentTabPosition.left,
  173. animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
  174. )
  175. fillMaxWidth()
  176. .wrapContentSize(Alignment.BottomStart)
  177. .offset(x = indicatorOffset)
  178. .width(currentTabWidth)
  179. }
  180. /**
  181. * Default opacity for the color of [Divider]
  182. */
  183. const val DividerOpacity = 0.12f
  184. /**
  185. * Default thickness for [Divider]
  186. */
  187. val DividerThickness = 1.dp
  188. /**
  189. * Default height for [Indicator]
  190. */
  191. val IndicatorHeight = 2.dp
  192. /**
  193. * The default padding from the starting edge before a tab in a [ScrollableTabRow].
  194. */
  195. val ScrollableTabRowPadding = 52.dp
  196. }
  197. private enum class TabSlots {
  198. Tabs,
  199. Divider,
  200. Indicator
  201. }
  202. /**
  203. * Class holding onto state needed for [ScrollableTabRow]
  204. */
  205. private class ScrollableTabData(
  206. private val scrollState: ScrollState,
  207. private val coroutineScope: CoroutineScope
  208. ) {
  209. private var selectedTab: Int? = null
  210. fun onLaidOut(
  211. density: Density,
  212. edgeOffset: Int,
  213. tabPositions: List&lt;TabPosition&gt;,
  214. selectedTab: Int
  215. ) {
  216. // Animate if the new tab is different from the old tab, or this is called for the first
  217. // time (i.e selectedTab is `null`).
  218. if (this.selectedTab != selectedTab) {
  219. this.selectedTab = selectedTab
  220. tabPositions.getOrNull(selectedTab)?.let {
  221. // Scrolls to the tab with [tabPosition], trying to place it in the center of the
  222. // screen or as close to the center as possible.
  223. val calculatedOffset = it.calculateTabOffset(density, edgeOffset, tabPositions)
  224. if (scrollState.value != calculatedOffset) {
  225. coroutineScope.launch {
  226. scrollState.animateScrollTo(
  227. calculatedOffset,
  228. animationSpec = ScrollableTabRowScrollSpec
  229. )
  230. }
  231. }
  232. }
  233. }
  234. }
  235. /**
  236. * @return the offset required to horizontally center the tab inside this TabRow.
  237. * If the tab is at the start / end, and there is not enough space to fully centre the tab, this
  238. * will just clamp to the min / max position given the max width.
  239. */
  240. private fun TabPosition.calculateTabOffset(
  241. density: Density,
  242. edgeOffset: Int,
  243. tabPositions: List&lt;TabPosition&gt;
  244. ): Int = with(density) {
  245. val totalTabRowWidth = tabPositions.last().right.roundToPx() + edgeOffset
  246. val visibleWidth = totalTabRowWidth - scrollState.maxValue
  247. val tabOffset = left.roundToPx()
  248. val scrollerCenter = visibleWidth / 2
  249. val tabWidth = width.roundToPx()
  250. val centeredTabOffset = tabOffset - (scrollerCenter - tabWidth / 2)
  251. // How much space we have to scroll. If the visible width is &lt;= to the total width, then
  252. // we have no space to scroll as everything is always visible.
  253. val availableSpace = (totalTabRowWidth - visibleWidth).coerceAtLeast(0)
  254. return centeredTabOffset.coerceIn(0, availableSpace)
  255. }
  256. }
  257. private val ScrollableTabRowMinimumTabWidth = 90.dp
  258. /**
  259. * [AnimationSpec] used when scrolling to a tab that is not fully visible.
  260. */
  261. private val ScrollableTabRowScrollSpec: AnimationSpec&lt;Float&gt; = tween(
  262. durationMillis = 250,
  263. easing = FastOutSlowInEasing
  264. )

答案2

得分: 1

我可以通过设置行的背景并动画偏移背景来实现这一点。

接受的答案适用于标签,但使用lazy row,我可以根据需要间隔项目。这正是问题的GIF中显示的Plantix应用程序的工作原理。

  1. @Composable
  2. fun EquiRow() {
  3. val selectedIndex = remember { mutableStateOf(0) }
  4. val colors = listOf(
  5. Color.Magenta,
  6. Color.Red,
  7. Color.Green,
  8. Color.Yellow,
  9. Color.Magenta,
  10. Color.Black,
  11. Color.Red
  12. )
  13. val first = remember {
  14. mutableStateOf(true)
  15. }
  16. val index = remember {
  17. mutableStateOf(4)
  18. }
  19. val scrollState = rememberScrollState()
  20. val radius = with(LocalDensity.current) { 40.dp.toPx() }
  21. val initialX = if (index.value == 0) {
  22. with(LocalDensity.current) { 27.dp.toPx() }
  23. } else {
  24. with(LocalDensity.current) { ((index.value * 54.dp.toPx()) + (14.dp.toPx() * index.value) + (27.dp.toPx())) }
  25. }
  26. val initialY = with(LocalDensity.current) { 75.dp.toPx() }
  27. var offsetX by remember { mutableStateOf(initialX) }
  28. var offsetY by remember { mutableStateOf(initialY) }
  29. val offsetAnim = remember { Animatable(0f) }
  30. val scrollToPosition by remember { mutableStateOf(initialX) }
  31. val mapRemem = remember { mutableMapOf<Int, Offset>() }
  32. LaunchedEffect(key1 = offsetX) {
  33. offsetAnim.animateTo(
  34. targetValue = offsetX, animationSpec = tween(
  35. durationMillis = 500,
  36. easing = LinearEasing
  37. )
  38. )
  39. }
  40. val animValue = if (first.value) {
  41. first.value = false
  42. offsetX
  43. } else {
  44. offsetAnim.value
  45. }
  46. Row(
  47. modifier = Modifier
  48. .horizontalScroll(scrollState)
  49. .fillMaxWidth()
  50. .height(150.dp)
  51. .padding(start = 16.dp, end = 16.dp)
  52. .drawBehind {
  53. drawCircle(
  54. color = Color.LightGray,
  55. radius = radius,
  56. center =
  57. Offset(animValue, offsetY)
  58. )
  59. },
  60. horizontalArrangement = Arrangement.spacedBy(14.dp)
  61. ) {
  62. // scroll row only first time initially to the selected index
  63. LaunchedEffect(key1 = scrollToPosition) {
  64. scrollState.animateScrollTo(scrollToPosition.roundToInt())
  65. }
  66. colors.forEachIndexed { index, color ->
  67. LogCompositions(tag = "For Loop", msg = "Running")
  68. Box(
  69. modifier = Modifier
  70. .align(Alignment.CenterVertically)
  71. .width(54.dp)
  72. .height(54.dp)
  73. .clip(CircleShape)
  74. .background(colors[index])
  75. .onGloballyPositioned { layoutCoordinates ->
  76. println("⚡⚡ XXXXXXXX : ${layoutCoordinates.positionInParent().x}")
  77. mapRemem[index] = Offset(
  78. layoutCoordinates.boundsInParent().center.x,
  79. layoutCoordinates.boundsInParent().center.y
  80. )
  81. }
  82. .clickable {
  83. offsetX = mapRemem[index]?.x!!
  84. offsetY = mapRemem[index]?.y!!
  85. selectedIndex.value = index
  86. println("⚡⚡ POSITION : $offsetX")
  87. }
  88. )
  89. }
  90. }
  91. }

结果:

点击这里查看gif

英文:

I was able to achieve this with setting a background to the row and animating the offset of the background.

The accepted answer works with tabs but using lazy row i can space the item with whatever padding i need. This is exactly how the plantix app works as shown in the gif of the question.

  1. @Composable
  2. fun EquiRow() {
  3. val selectedIndex = remember { mutableStateOf(0) }
  4. val colors = listOf(
  5. Color.Magenta,
  6. Color.Red,
  7. Color.Green,
  8. Color.Yellow,
  9. Color.Magenta,
  10. Color.Black,
  11. Color.Red
  12. )
  13. val first = remember {
  14. mutableStateOf(true)
  15. }
  16. val index = remember {
  17. mutableStateOf(4)
  18. }
  19. val scrollState = rememberScrollState()
  20. val radius = with(LocalDensity.current) { 40.dp.toPx() }
  21. val initialX = if (index.value == 0) {
  22. with(LocalDensity.current) { 27.dp.toPx() }
  23. } else {
  24. with(LocalDensity.current) { ((index.value * 54.dp.toPx()) + (14.dp.toPx() * index.value) + (27.dp.toPx())) }
  25. }
  26. val initialY = with(LocalDensity.current) { 75.dp.toPx() }
  27. var offsetX by remember { mutableStateOf(initialX) }
  28. var offsetY by remember { mutableStateOf(initialY) }
  29. val offsetAnim = remember { Animatable(0f) }
  30. val scrollToPosition by remember { mutableStateOf(initialX) }
  31. val mapRemem = remember { mutableMapOf&lt;Int, Offset&gt;() }
  32. LaunchedEffect(key1 = offsetX) {
  33. offsetAnim.animateTo(
  34. targetValue = offsetX, animationSpec = tween(
  35. durationMillis = 500,
  36. easing = LinearEasing
  37. )
  38. )
  39. }
  40. val animValue = if (first.value) {
  41. first.value = false
  42. offsetX
  43. } else {
  44. offsetAnim.value
  45. }
  46. Row(
  47. modifier = Modifier
  48. .horizontalScroll(scrollState)
  49. .fillMaxWidth()
  50. .height(150.dp)
  51. .padding(start = 16.dp, end = 16.dp)
  52. .drawBehind {
  53. drawCircle(
  54. color = Color.LightGray,
  55. radius = radius,
  56. center =
  57. Offset(animValue, offsetY)
  58. )
  59. },
  60. horizontalArrangement = Arrangement.spacedBy(14.dp)
  61. ) {
  62. // scroll row only first time initially to the selected index
  63. LaunchedEffect(key1 = scrollToPosition) {
  64. scrollState.animateScrollTo(scrollToPosition.roundToInt())
  65. }
  66. colors.forEachIndexed { index, color -&gt;
  67. LogCompositions(tag = &quot;For Loop&quot;, msg = &quot;Running&quot;)
  68. Box(
  69. modifier = Modifier
  70. .align(Alignment.CenterVertically)
  71. .width(54.dp)
  72. .height(54.dp)
  73. .clip(CircleShape)
  74. .background(colors[index])
  75. .onGloballyPositioned { layoutCoordinates -&gt;
  76. println(&quot;&#128293;&#128293; XXXXXXXX : ${layoutCoordinates.positionInParent().x}&quot;)
  77. mapRemem[index] = Offset(
  78. layoutCoordinates.boundsInParent().center.x,
  79. layoutCoordinates.boundsInParent().center.y
  80. )
  81. }
  82. .clickable {
  83. offsetX = mapRemem[index]?.x!!
  84. offsetY = mapRemem[index]?.y!!
  85. selectedIndex.value = index
  86. println(&quot;&#128293;&#128293; POSITION : $offsetX&quot;)
  87. }
  88. )
  89. }
  90. }
  91. }

Result

Clcik here to view the gif

huangapple
  • 本文由 发表于 2023年6月19日 12:23:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/76503595.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定