androidanimationandroid-jetpack-composeandroid-canvasandroid-jetpack-compose-canvas

How to switch movement of object from one path to another smoothly in Jetpack Compose?


I am totally new in jetpack. I am working on a project where a shape is revolving around a centre point in a specific radius. On a button click, Shape needs to start revolving in a radius smaller than before. I am able to achieve that bit, but when shape switches, it kind of first stop for a moment, switches to smaller radius path in a vertical line manner and then start rotating again, which is kind of giving a bad look to it. How can I make this switching/transitioning of shape smooth? Below is the code I am using:

@Composable
fun AnimateObject(){
    val transition = rememberInfiniteTransition()
    val animatedProgress = transition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 3000
                0.0f at 0 with LinearEasing
                1.0f at 3000 with LinearEasing
            },
            repeatMode = RepeatMode.Restart
        )
    )

    val outerCircleRadius = 450f
    val innerCircleRadius = 250f
    var outerCircleCenter = Offset(x = 0f, y = 0f)
    var innerCircleCenter = Offset(x = 0f, y = 0f)
    var playerXOffset = remember { mutableStateOf(0f) }
    var playerYOffset = remember { mutableStateOf(0f) }
    var isInOuterCircle = remember { mutableStateOf(true) }



    val angle = animatedProgress.value * 360f
    val radians = Math.toRadians(angle.toDouble())

    Canvas(modifier = Modifier.size(200.dp), onDraw = {

        outerCircleCenter = Offset(size.width / 2, size.height / 2)
        innerCircleCenter = Offset(size.width / 2, size.height / 2)

        CreateCirlcePath(radius = outerCircleRadius, center = outerCircleCenter, drawScope = this)
        CreateCirlcePath(radius = innerCircleRadius, center = innerCircleCenter, drawScope = this)

        if (isInOuterCircle.value == true) {
            playerXOffset.value = outerCircleCenter.x + outerCircleRadius * cos(radians).toFloat()
            playerYOffset.value = outerCircleCenter.y + outerCircleRadius * sin(radians).toFloat()
        } else {
            playerXOffset.value = innerCircleCenter.x + innerCircleRadius * Math.cos(radians).toFloat()
            playerYOffset.value = innerCircleCenter.y + innerCircleRadius * Math.sin(radians).toFloat()
        }


        print("Offset values, ${playerXOffset.value} ")

        // Draw the moving shape (circle in this case)
        drawCircle(
            color = Color.Blue,
            center = Offset(playerXOffset.value, playerYOffset.value),
            radius = 16.dp.toPx()
        )
    })

    Row {
        val buttonTitle = remember { mutableStateOf("Click Me") }
        Button(
            onClick = {
                buttonTitle.value = "Clicked"
                isInOuterCircle.value = !isInOuterCircle.value
            },
            modifier = Modifier.size(width = 150.dp, height = 50.dp)
        ) {
            Text(text = buttonTitle.value)
        }
    }
}

I am experiencing a glitchy transition from one circle path to another. Here is the link to demonstrate how does it look:

https://imgur.com/a/M6GDfcX

I want this transition to be smooth in a way that when transition is happening from one circle to another, shape continues its motion and during switching, it lands on the other circle path in a slanted manner (or like a tangent). Any help would be appreciated! Thanks


Solution

  • You can linearly interpolate between inner and outer circles using lerp function.

    https://en.wikipedia.org/wiki/Linear_interpolation

    I used rotate function of Canvas while only interpolating trajectory of circle and results is

    enter image description here

    @Preview
    @Composable
    private fun RadiusChangeLerpAnimationTes() {
    
        val outerCircleRadius = 450f
        val innerCircleRadius = 250f
    
        var isOut by remember {
            mutableStateOf(false)
        }
    
        val transition = rememberInfiniteTransition(label = "")
    
        val angle by transition.animateFloat(
            initialValue = 0f,
            targetValue = 360f,
            animationSpec = infiniteRepeatable(
                animation = keyframes {
                    durationMillis = 3000
                    0.0f at 0 with LinearEasing
                    360f at 3000 with LinearEasing
                },
                repeatMode = RepeatMode.Restart
            ), label = ""
        )
    
        val progress by animateFloatAsState(
            if (isOut) 1f else 0f,
            animationSpec = tween(durationMillis = 700, easing = LinearEasing),
            label = "distance"
        )
    
        val distance = remember(progress) {
            lerp(innerCircleRadius, outerCircleRadius, progress)
        }
    
        var position by remember {
            mutableStateOf(Offset.Unspecified)
        }
    
        Column {
            Canvas(
                modifier = Modifier.fillMaxWidth().aspectRatio(1f)
            ) {
    
                drawCircle(
                    color = Color.Blue,
                    radius = outerCircleRadius,
                    style = Stroke(2.dp.toPx())
                )
    
                drawCircle(
                    color = Color.Blue,
                    radius = innerCircleRadius,
                    style = Stroke(2.dp.toPx())
                )
    
                rotate(angle) {
                    drawCircle(
                        color = Color.Red,
                        radius = 50f,
                        center = Offset(center.x + distance, center.y)
    
                    )
                }
    
                val angleInRadians = angle * DEGREE_TO_RAD
                position = Offset(
                    center.x + distance * cos(angleInRadians), center.y + distance * sin(angleInRadians)
                )
            }
    
            Button(
                modifier = Modifier.fillMaxWidth(),
                onClick = {
                    isOut = isOut.not()
                }
            ) {
                Text("Out $isOut")
            }
    
            if (position != Offset.Unspecified) {
                Text("Position: $position")
            }
        }
    }
    
    private const val DEGREE_TO_RAD = (Math.PI / 180f).toFloat()