kotlinandroid-jetpack-composehorizontal-scrollingjetpack-compose-animationandroid-jetpack-compose-row

How to implement bounce effect for row with horizontal scroll?


I have Row with .horizontalScroll() and I need bounce animation effect. That is, when the list appears, it scrolls forward a little, and then returns back. At the same time, I need a spring effect when returning the scroll back. Here is my implementation:

val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
val bounceDistance = 150
    
LaunchedEffect(Unit) {
    val animatable = Animatable(scrollState.value.toFloat())

    coroutineScope.launch {
        animatable.animateTo(
            targetValue = bounceDistance.toFloat(),
            animationSpec = tween(
                durationMillis = 200,
                easing = LinearOutSlowInEasing
            )
        )
        scrollState.scrollTo(animatable.value.toInt())
        animatable.animateTo(
            targetValue = 0f,
            animationSpec = spring(
                dampingRatio = 0.4f,
                stiffness = Spring.StiffnessLow
            )
        )
        scrollState.scrollTo(animatable.value.toInt())
        animatable.animateTo(
            targetValue = bounceDistance * 0.3f,
            animationSpec = tween(
                durationMillis = 400,
                easing = LinearOutSlowInEasing
            )
        )
        scrollState.scrollTo(animatable.value.toInt())
        animatable.animateTo(
            targetValue = 0f,
            animationSpec = spring(
                dampingRatio = 0.6f,
                stiffness = Spring.StiffnessLow
            )
        )

        scrollState.scrollTo(animatable.value.toInt())
    }
}

However, it all looks very unsmooth and slow. What am I doing wrong?

P.S. This is needed to hint to the user that the component has horizontal scrolling.


Solution

  • Your code doesn't work properly because you manually call scrollState.scrollTo 4 times after each animation. You should call scrollState.scrollTo when animatable.value updates, for example:

    val scrollState = rememberScrollState()
    val bounceDistance = 150
    val animatable = remember { Animatable(0f) }
    
    LaunchedEffect(Unit) {
        animatable.animateTo(
            targetValue = bounceDistance.toFloat(),
            animationSpec = tween(
                durationMillis = 200,
                easing = LinearOutSlowInEasing
            )
        )
        animatable.animateTo(
            targetValue = 0f,
            animationSpec = spring(
                dampingRatio = 0.4f,
                stiffness = Spring.StiffnessLow
            )
        )
        animatable.animateTo(
            targetValue = bounceDistance * 0.3f,
            animationSpec = tween(
                durationMillis = 400,
                easing = LinearOutSlowInEasing
            )
        )
        animatable.animateTo(
            targetValue = 0f,
            animationSpec = spring(
                dampingRatio = 0.6f,
                stiffness = Spring.StiffnessLow
            )
        )
    }
    
    LaunchedEffect(animatable.value) {
        scrollState.scrollTo(animatable.value.toInt())
    }
    

    Instead of making bounce animation manually you can use tween animation spec with EaseOutBounce easing:

    screen capture

    val animatable = remember { Animatable(0, Int.VectorConverter) }
    LaunchedEffect(Unit) {
        animatable.animateTo(
            targetValue = bounceDistance,
            animationSpec = tween(
                durationMillis = 200,
                easing = LinearOutSlowInEasing
            )
        )
        animatable.animateTo(
            targetValue = 0,
            animationSpec = tween(
                durationMillis = 1000,
                easing = EaseOutBounce
            )
        )
    }
    
    LaunchedEffect(animatable.value) {
        scrollState.scrollTo(animatable.value)
    }