Jetpack Compose中如何缩放按钮动画

11
我想使用Jetpack Compose构建AirBnB应用中的这个神奇按钮动画。 1 不幸的是,最近动画/转换API被改变了,并且几乎没有文档。有人能帮助我找到实现此按钮按下动画的正确方法吗? 编辑:根据 @Amirhosein 的答案,我已经开发了一个几乎与Airbnb示例完全相同的按钮。
代码:
@Composable
fun AnimatedButton() {
    val boxHeight = animatedFloat(initVal = 50f)
    val relBoxWidth = animatedFloat(initVal = 1.0f)
    val fontSize = animatedFloat(initVal = 16f)

    fun animateDimensions() {
        boxHeight.animateTo(45f)
        relBoxWidth.animateTo(0.95f)
       // fontSize.animateTo(14f)
    }

    fun reverseAnimation() {
        boxHeight.animateTo(50f)
        relBoxWidth.animateTo(1.0f)
        //fontSize.animateTo(16f)
    }

        Box(
        modifier = Modifier
            .height(boxHeight.value.dp)
            .fillMaxWidth(fraction = relBoxWidth.value)

            .clip(RoundedCornerShape(8.dp))
            .background(Color.Black)
            .clickable { }
            .pressIndicatorGestureFilter(
                onStart = {
                    animateDimensions()
                },
                onStop = {
                    reverseAnimation()
                },
                onCancel = {
                    reverseAnimation()
                }
            ),
        contentAlignment = Alignment.Center
    ) {
        Text(text = "Explore Airbnb", fontSize = fontSize.value.sp, color = Color.White)
    }
}

视频:

2

很遗憾,我无法正确地使文本动画化,因为目前它看起来非常糟糕。

6个回答

18

你是在寻找类似这样的东西吗?

@Composable
fun AnimatedButton() {
    val selected = remember { mutableStateOf(false) }
    val scale = animateFloatAsState(if (selected.value) 2f else 1f)

    Column(
        Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(
            onClick = {  },
            modifier = Modifier
                .scale(scale.value)
                .height(40.dp)
                .width(200.dp)
                .pointerInteropFilter {
                    when (it.action) {
                        MotionEvent.ACTION_DOWN -> {
                            selected.value = true }

                        MotionEvent.ACTION_UP  -> {
                           selected.value = false }
                    }
                    true
                }
        ) {
            Text(text = "Explore Airbnb", fontSize = 15.sp, color = Color.White)
        }
    }
}

2
这对我有效。请注意,我还必须包括设置 selected.value = false 以处理 MotionEvent.ACTION_CANCEL,这样如果我在没有抬起手指的情况下将手指从按钮上拖出去,动画就会重置。如果没有它,按钮仍然会认为它是“选定的”,即使它实际上并不是。 - trod
我收回之前的观点。虽然这个解决方案非常适合提供动画效果,但是由于某种原因,添加一个返回true.pointerInteropFilter将会阻止onClick事件的触发。如果可以撤回我的点赞,我会这么做的。 - trod
再次跟进,我已经调整了.pointerInteropFilter如下,似乎可以同时提供动画和点击处理:.pointerInteropFilter { if (isEnabled) { when (it.action) { MotionEvent.ACTION_DOWN -> { isPressed = true } MotionEvent.ACTION_UP -> { isPressed = false onClick() } MotionEvent.ACTION_CANCEL -> { isPressed = false } } } else { isPressed = false } true }, - trod
在添加了“scale”效果后,“onClick = { do something }”不起作用。 - Binh Ho

10

这是我在项目中使用的实现方式。对我来说,这似乎是最简洁的。

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val sizeScale by animateFloatAsState(if (isPressed) 0.5f else 1f)

Button(
    onClick = { },
    modifier = Modifier
        .wrapContentSize()
        .graphicsLayer(
            scaleX = sizeScale,
            scaleY = sizeScale
        ),
    interactionSource = interactionSource
) { Text(text = "Open the reward") }

5

使用 pressIndicatorGestureFilter 来实现此行为。

以下是我的解决方法:

@Preview
@Composable
fun MyFancyButton() {
val boxHeight = animatedFloat(initVal = 60f)
val boxWidth = animatedFloat(initVal = 200f)
    Box(modifier = Modifier
        .height(boxHeight.value.dp)
        .width(boxWidth.value.dp)
        .clip(RoundedCornerShape(4.dp))
        .background(Color.Black)
        .clickable { }
        .pressIndicatorGestureFilter(
            onStart = {
                boxHeight.animateTo(55f)
                boxWidth.animateTo(180f)
            },
            onStop = {
                boxHeight.animateTo(60f)
                boxWidth.animateTo(200f)
            },
            onCancel = {
                boxHeight.animateTo(60f)
                boxWidth.animateTo(200f)
            }
        ), contentAlignment = Alignment.Center) {
           Text(text = "Utforska Airbnb", color = Color.White)
     }
}

默认的Jetpack Compose Button 在其 onClick 事件中会消耗点击手势,并且 pressIndicatorGestureFilter 不会接收到这些点击事件。这就是我创建这个自定义按钮的原因。


很好的方法!你知道我们如何动画化整个表面(包括字体大小),而不仅仅是盒子的宽度和高度吗?(请参见我的更新问题) - Yannick
文字字体大小的动画似乎很简单。你说的“目前看起来非常糟糕”是什么意思? - Amirhosein
pressIndicatorGestureFilter 导入是什么? - Sergio

3

您可以使用Modifier.pointerInput侦测tapGesture

定义一个枚举:

enum class ComponentState { Pressed, Released }

然后:

var toState by remember { mutableStateOf(ComponentState.Released) }
val modifier = Modifier.pointerInput(Unit) {
    detectTapGestures(
        onPress = {
            toState = ComponentState.Pressed
            tryAwaitRelease()
            toState = ComponentState.Released
        }

    )
}
 // Defines a transition of `ComponentState`, and updates the transition when the provided [targetState] changes
val transition: Transition<ComponentState> = updateTransition(targetState = toState, label = "")

// Defines a float animation to scale x,y
val scalex: Float by transition.animateFloat(
    transitionSpec = { spring(stiffness = 50f) }, label = ""
) { state ->
    if (state == ComponentState.Pressed) 1.25f else 1f
}
val scaley: Float by transition.animateFloat(
    transitionSpec = { spring(stiffness = 50f) }, label = ""
) { state ->
    if (state == ComponentState.Pressed) 1.05f else 1f
}

应用modifier并使用Modifier.graphicsLayer,同时改变文本的大小。

Box(
    modifier
        .padding(16.dp)
        .width((100 * scalex).dp)
        .height((50 * scaley).dp)
        .background(Color.Black, shape = RoundedCornerShape(8.dp)),
    contentAlignment = Alignment.Center) {

        Text("BUTTON", color = Color.White,
            modifier = Modifier.graphicsLayer{
                scaleX = scalex;
                scaleY = scaley
            })

}

enter image description here


0
这里是 ScalingButton,当用户点击按钮时会触发 onClick 回调函数,当用户按下按钮并没有释放它后,移开手指离开按钮区域时状态会被重置。我使用 Modifier.pointerInput 函数来检测用户输入:
@Composable
fun ScalingButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) {
    var selected by remember { mutableStateOf(false) }
    val scale by animateFloatAsState(if (selected) 0.7f else 1f)

    Button(
        onClick = onClick,
        modifier = Modifier
            .scale(scale)
            .pointerInput(Unit) {
                while (true) {
                    awaitPointerEventScope {
                        awaitFirstDown(false)
                        selected = true
                        waitForUpOrCancellation()
                        selected = false
                    }
                }
            }
    ) {
        content()
    }
}

或者

另一种方法是不使用无限循环:

@Composable
fun ScalingButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) {
    var selected by remember { mutableStateOf(false) }
    val scale by animateFloatAsState(if (selected) 0.75f else 1f)

    Button(
        onClick = onClick,
        modifier = Modifier
            .scale(scale)
            .pointerInput(selected) {
                awaitPointerEventScope {
                    selected = if (selected) {
                        waitForUpOrCancellation()
                        false
                    } else {
                        awaitFirstDown(false)
                        true
                    }
                }
            }
    ) {
        content()
    }
}

我把按钮替换成了一个框,现在不起作用了。 - Mehdi.ncb

-2

如果您想使用不同类型的动画(如缩放、旋转和许多其他类型的动画)来创建动画按钮,则可以在Jetpack Compose中使用此库。在这里查看


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接