Bounce Button Animation in Compose

1.1k Views Asked by At

I want to make a button like this in Compose: https://pub.dev/packages/flutter_bounceable

But the clickable method is not work in my code.

I tried with this code, but it has an error. Pushing the button, but there's no action. Animations are working well, but not for the clickable.

fun Modifier.bounceClick(onClick: () -> Unit,animationDuration: Int = 100,
                         scaleDown: Float = 0.9f) = composed {
    val interactionSource = MutableInteractionSource()

    val coroutineScope = rememberCoroutineScope()

    val scale = remember {
        Animatable(1f)
    }

    this
        .scale(scale = scale.value)
        .background(
            color = Color(0xFF35898F),
            shape = RoundedCornerShape(size = 12f)
        )
        .clickable(interactionSource = interactionSource, indication = null, onClick = onClick)
        .pointerInput(Unit) {
            while(true)
                awaitPointerEventScope {
                        awaitFirstDown()
                        coroutineScope.launch {
                            scale.animateTo(
                                scaleDown,
                                animationSpec = tween(animationDuration),
                            )
                        }
                        waitForUpOrCancellation()
                        coroutineScope.launch {
                            scale.animateTo(
                                scaleDown,
                                animationSpec = tween(20),
                            )
                            scale.animateTo(
                                1f,
                                animationSpec = tween(animationDuration),
                            )
                    }
            }
        }
}
2

There are 2 best solutions below

0
On BEST ANSWER

This is quite simple to do with Compose.

You should use foreachGesture or awaitEachGesture if Compose version is 1.4.0-alpha03 with Modifier.pointerInput instead of while. Also when you have clickable you don't need Modifier.pointerInput as well , you can use either of them.

I will only demonstrate how to do it with Modifier.clickable and interactionSource.collectIsPressedAsState() as below.

Result

enter image description here

Implementation

fun Modifier.bounceClick(
    animationDuration: Int = 100,
    scaleDown: Float = 0.9f,
    onClick: () -> Unit
) = composed {

    val interactionSource = remember { MutableInteractionSource() }
    val isPressed by interactionSource.collectIsPressedAsState()

    val animatable = remember {
        Animatable(1f)
    }

    LaunchedEffect(key1 = isPressed) {
        if (isPressed) {
            animatable.animateTo(scaleDown)
        } else animatable.animateTo(1f)
    }

    Modifier
        .graphicsLayer {
            val scale = animatable.value
            scaleX = scale
            scaleY = scale
        }
        .clickable(
            interactionSource = interactionSource,
            indication = null
        ) {
            onClick()
        }
}

Usage

@Composable
private fun BounceExample() {
    Row {
        Box(
            Modifier

                .background(Color.Red, RoundedCornerShape(10.dp))
                .bounceClick {

                }
                .padding(10.dp),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "Hello World", color = Color.White, fontSize = 20.sp)
        }
        Spacer(modifier = Modifier.width(10.dp))

        Box(
            Modifier

                .bounceClick {

                }
                .background(Color.Green, RoundedCornerShape(10.dp))
                .padding(10.dp),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "Hello World", color = Color.White, fontSize = 20.sp)
        }
    }
}
1
On

There's a small problem with @Thracian's answer. If the button is in a scrollable container, and you tap the button quickly it will not animate. This is because the isPressed state doesn't dispatch due to a built in delay (located in the Clickable.kt compose file).

To fix it, change it to the following:

private fun Modifier.bounceClick(
    scaleDown: Float = 0.90f,
    onClick: () -> Unit
) = composed {

    val interactionSource = remember { MutableInteractionSource() }

    val animatable = remember {
        Animatable(1f)
    }

    LaunchedEffect(interactionSource) {
        interactionSource.interactions.collect { interaction ->
            when (interaction) {
                is PressInteraction.Press -> animatable.animateTo(scaleDown)
                is PressInteraction.Release -> animatable.animateTo(1f)
                is PressInteraction.Cancel -> animatable.animateTo(1f)
            }
        }
    }

    Modifier
        .graphicsLayer {
            val scale = animatable.value
            scaleX = scale
            scaleY = scale
        }
        .clickable(
            interactionSource = interactionSource,
            indication = null
        ) {
            onClick()
        }
}