我想实现一个滑动按钮菜单,当在中心时变得更大和更亮,其他的变得更小和更暗.并希望在喷气背包组合中实现它.期待解决方案

Swipe button menu

推荐答案

这是可能的与pager 灵感从https://docs.flutter.dev/cookbook/effects/photo-filter-carousel.我认为pager 的问题并不是一帆风顺的.除此之外,它符合用例.

@OptIn(ExperimentalPagerApi::class, ExperimentalSnapperApi::class)
@Composable
fun PagerDemo(modifier: Modifier = Modifier) {
    BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
        val contentPadding = (maxWidth - 50.dp) / 2
        val offSet = maxWidth / 5
        val itemSpacing = offSet - 50.dp
        val pagerState = rememberPagerState()
        val scope = rememberCoroutineScope()

        HorizontalPager(
            count = 30,
            contentPadding = PaddingValues(horizontal = contentPadding),
            modifier = modifier,
            itemSpacing = itemSpacing,
            state = pagerState
        ) { page ->
            Box(
                modifier = Modifier
                    .size(50.dp)
                    .graphicsLayer {
                        val pageOffset = calculateCurrentOffsetForPage(page).absoluteValue
                        // Set the item alpha and scale based on the distance from the center
                        val percentFromCenter = 1.0f - (pageOffset / (5f / 2f))
                        val itemScale = 0.5f + (percentFromCenter * 0.5f).coerceIn(0f, 1f)
                        val opacity = 0.25f + (percentFromCenter * 0.75f).coerceIn(0f, 1f)

                        alpha = opacity
                        scaleY = itemScale
                        scaleX = itemScale
                        shape = CircleShape
                        clip = true
                    }
                    .background(color = colors[page % colors.size])
                    .clickable(
                        interactionSource = MutableInteractionSource(),
                        indication = null,
                        enabled = true,
                    ) {
                        scope.launch {
                            pagerState.animateScrollToPage(page)
                        }
                    })
        }
    }
}


private val colors = listOf(
    Color.Red,
    Color.Green,
    Color.Blue,
    Color.Magenta,
    Color.Yellow,
    Color.Cyan,
)

Screen shot enter image description here

第二种方式的灵感来自Medium https://fvilarino.medium.com/recreating-google-podcasts-speed-selector-in-jetpack-compose-7623203a009d上的一篇博客.在这里,我想,与其在点击时滚动到某个位置,可能是我们应该考虑基于所点击的项目来动画或平滑地滚动到中心.

private val colors = listOf(
    Color.Red,
    Color.Green,
    Color.Blue,
    Color.Magenta,
    Color.Yellow,
    Color.Cyan,
)

@Stable
interface CarouselState {
    val currentValue: Float
    val range: ClosedRange<Int>

    suspend fun snapTo(value: Float)
    suspend fun scrollTo(value: Int)
    suspend fun decayTo(velocity: Float, value: Float)
    suspend fun stop()
}

class CarouselStateImpl(
    currentValue: Float,
    override val range: ClosedRange<Int>,
) : CarouselState {
    private val floatRange = range.start.toFloat()..range.endInclusive.toFloat()
    private val animatable = Animatable(currentValue)
    private val decayAnimationSpec = FloatSpringSpec(
        dampingRatio = Spring.DampingRatioLowBouncy,
        stiffness = Spring.StiffnessLow,
    )
    override val currentValue: Float
        get() = animatable.value

    override suspend fun stop() {
        animatable.stop()
    }

    override suspend fun snapTo(value: Float) {
        animatable.snapTo(value.coerceIn(floatRange))
    }

    override suspend fun scrollTo(value: Int) {
        animatable.snapTo(value.toFloat().coerceIn(floatRange))
    }

    override suspend fun decayTo(velocity: Float, value: Float) {
        val target = value.roundToInt().coerceIn(range).toFloat()
        animatable.animateTo(
            targetValue = target,
            initialVelocity = velocity,
            animationSpec = decayAnimationSpec,
        )
    }

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

        other as CarouselStateImpl

        if (range != other.range) return false
        if (floatRange != other.floatRange) return false
        if (animatable != other.animatable) return false
        if (decayAnimationSpec != other.decayAnimationSpec) return false

        return true
    }

    override fun hashCode(): Int {
        var result = range.hashCode()
        result = 31 * result + floatRange.hashCode()
        result = 31 * result + animatable.hashCode()
        result = 31 * result + decayAnimationSpec.hashCode()
        return result
    }

    companion object {
        val Saver = Saver<CarouselStateImpl, List<Any>>(
            save = { listOf(it.currentValue, it.range.start, it.range.endInclusive) },
            restore = {
                CarouselStateImpl(
                    currentValue = it[0] as Float,
                    range = (it[1] as Int)..(it[2] as Int)
                )
            }
        )
    }
}

@Composable
fun rememberCarouselState(
    currentValue: Float = 0f,
    range: ClosedRange<Int> = 0..40,
): CarouselState {
    val state = rememberSaveable(saver = CarouselStateImpl.Saver) {
        CarouselStateImpl(currentValue, range)
    }
    LaunchedEffect(key1 = Unit) {
        state.snapTo(state.currentValue.roundToInt().toFloat())
    }
    return state
}

@Composable
fun InstagramCarousel(
    modifier: Modifier = Modifier,
    state: CarouselState = rememberCarouselState(),
    numSegments: Int = 5,
    circleColor: Color = MaterialTheme.colors.onSurface,
    currentValueLabel: @Composable (Int) -> Unit = { value -> Text(value.toString()) },
    indicatorLabel: @Composable (Int) -> Unit = { value -> Text(value.toString()) },
) {
    val context = LocalContext.current
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        currentValueLabel(state.currentValue.roundToInt())
        //Icon(Icons.Filled.ArrowDropDown, contentDescription = null)
        val scope = rememberCoroutineScope()

        BoxWithConstraints(
            modifier = Modifier
                .fillMaxWidth()
                .drag(state, numSegments),
            contentAlignment = Alignment.Center,
        ) {

                CenterCircle(
                    modifier = Modifier.align(Alignment.Center),
                    fillColor = Color(android.graphics.Color.parseColor("#4DB6AC")),
                    strokeWidth = 5.dp,
                )

            val segmentWidth = maxWidth / numSegments
            val segmentWidthPx = constraints.maxWidth.toFloat() / numSegments.toFloat()
            val halfSegments = (numSegments + 1) / 2
            val start = (state.currentValue - halfSegments).toInt()
                .coerceAtLeast(state.range.start)
            val end = (state.currentValue + halfSegments).toInt()
                .coerceAtMost(state.range.endInclusive)


            val maxOffset = constraints.maxWidth / 2f
            for (i in start..end) {
                val offsetX = (i - state.currentValue) * segmentWidthPx
                // alpha
                val deltaFromCenter = (offsetX)
                val percentFromCenter = 1.0f - abs(deltaFromCenter) / maxOffset
                val alpha = 0.25f + (percentFromCenter * 0.75f)
                // scale
                val deltaFromCenterScale = (offsetX)
                val percentFromCenterScale = 1.0f - abs(deltaFromCenterScale) / maxOffset
                val scale = 0.5f + (percentFromCenterScale * 0.5f)

                Column(
                    modifier = Modifier
                        .width(segmentWidth)
                        .wrapContentHeight(Alignment.CenterVertically)
                        .graphicsLayer(
                            translationX = offsetX,
                        ),
                    horizontalAlignment = Alignment.CenterHorizontally,
                ) {

                    Box(
                        modifier = Modifier
                            .width(55.dp)
                            .height(55.dp)
                            .graphicsLayer(
                                alpha = alpha,
                                scaleY = scale,
                                scaleX = scale
                            )
                            .clip(CircleShape)
                            .background(colors[i % colors.size])
                            .clickable {
                                scope.launch {
                                    state.scrollTo(i)
                                }
                                Toast
                                    .makeText(context, "$i", Toast.LENGTH_SHORT)
                                    .show()
                            }
                    )
                    // indicatorLabel(i)
                }
            }
        }
    }
}

@SuppressLint("ReturnFromAwaitPointerEventScope", "MultipleAwaitPointerEventScopes")
private fun Modifier.drag(
    state: CarouselState,
    numSegments: Int,
) = pointerInput(Unit) {
    val decay = splineBasedDecay<Float>(this)
    val segmentWidthPx = size.width / numSegments
    coroutineScope {
        while (true) {
            val pointerId =
                awaitPointerEventScope { awaitFirstDown(pass = PointerEventPass.Initial).id }
            state.stop()
            val tracker = VelocityTracker()
            awaitPointerEventScope {
                horizontalDrag(pointerId) { change ->
                    val horizontalDragOffset =
                        state.currentValue - change.positionChange().x / segmentWidthPx
                    launch {
                        state.snapTo(horizontalDragOffset)
                    }
                    tracker.addPosition(change.uptimeMillis, change.position)
                    if (change.positionChange() != Offset.Zero) change.consume()
                }
            }
            val velocity = tracker.calculateVelocity().x / numSegments
            val targetValue = decay.calculateTargetValue(state.currentValue, -velocity)
            launch {
                state.decayTo(velocity, targetValue)
            }
        }
    }
}

@Preview(widthDp = 420)
@Composable
fun InstagramCarouselPreview() {
    ComposeLearningTheme() {
        Surface(modifier = Modifier.fillMaxWidth()) {
            InstagramCarousel(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable {
                    }
                    .padding(vertical = 16.dp),
                currentValueLabel = { value ->
                    Text(
                        text = "${(value / 10)}.${(value % 10)}x",
                        style = MaterialTheme.typography.h6
                    )
                },
                indicatorLabel = { value ->
                    if (value % 5 == 0) {
                        Text(
                            text = "${(value / 10)}.${(value % 10)}",
                            style = MaterialTheme.typography.body2,
                        )
                    }
                }
            )
        }
    }
}

Screen Shot enter image description here

第三种使用自定义布局的方法在这里是https://medium.com/@raghunandan2005/creating-instagram-like-carousel-in-compose-92d65de943a.待定:获取中心项目索引,并在单击项目至中心时平滑滚动

您可以对此进行自定义并实施所需的解决方案.代码片段是不言而喻的.除了圆形框,您还可以使用圆形图像/按钮.

Android相关问答推荐

在Kotlin Jetpack Compose中点击按钮后启动另一个Android应用程序

如何将子零部件的大小调整为可以调整大小的父组件大小?

Jetpack编写使用自定义主题覆盖库中主题部分

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

在Android Studio中,如何在BuildSrc Dependenices Kotlin文件中指定时标记与旧版本的依赖关系

在带有REACT-Native-CLI的开发和生产中使用Firebase的最佳实践

我正在创建一个简单的连接四个游戏,我需要一个弹出式窗口当你赢了

在Jetpack Compose中将导航绘图显示在顶部栏下方、底部栏上方

为什么可以';我不能直接在RecyclerView.ViewHolder中访问视图ID吗?

Android 12+BLE字节不同

在 kotlin 上向适配器添加绑定视图功能

使用 Kotlin 在 Android 中导航时如何防止 ViewModel 被杀死?

如何在 Jetpack Compose 中的特定位置绘制图像

为什么项目捕获对象给我在 Compose 中找不到参考

清洁架构中的服务

如何在 Jetpack Compose 中为中心对齐设置动画?

更改 Android SDK 版本 33 后建议在 xml 布局文件中不起作用

协程是否在 if 条件下保持秩序?

为什么按钮没有拉伸到屏幕边缘?

使 Compose LazyColumn 的最后一项填满屏幕的其余部分