Create A Checkmark Animation in Android Compose Using Sequential Animatables

You are currently viewing Create A Checkmark Animation in Android Compose Using Sequential Animatables

In this post I will show you a sample of using Animatables in Android Compose to create a checkmark animation that could be used for onSuccess callbacks.

I chose to use Animatables because these can be run sequentially, one after another in sequence, to achieve a full UI animation.

Code

Below is the code used to create the composable element. I have added a clickable function to the box just for demonstration purposes so the animation can be reset. You can remove this as well as the *.snapTo functions in the LaunchEffect to only allow this to run once.

@Composable
fun SuccessGreenCheckmark(
    padding: Dp,
    dimension: Dp,
    tickSize: Float
) {

    val density = LocalDensity.current

    var startAnim by remember { mutableStateOf(false) }

    val downTickStart = Offset(
        with(density){ dimension.toPx() * 0.25f },
        with(density){ dimension.toPx() * 0.5f }
    )

    val downTickEnd = Offset(
        with(density){ dimension.toPx() * 0.5f },
        with(density){ dimension.toPx() * 0.75f }
    )

    val upTickStart = Offset(
        with(density){ dimension.toPx() * 0.5f },
        with(density){ dimension.toPx() * 0.75f }
    )

    val upTickEnd = Offset(
        with(density){ dimension.toPx() * 0.75f },
        with(density){ dimension.toPx() * 0.25f }
    )

    val circleAnimation = remember {  Animatable(0f) }
    val downTick = remember { Animatable(downTickStart, Offset.VectorConverter) }
    val upTick = remember { Animatable(upTickStart, Offset.VectorConverter) }


    LaunchedEffect(startAnim) {
        circleAnimation.snapTo(0f)
        downTick.snapTo(downTickStart)
        upTick.snapTo(upTickStart)
        circleAnimation.animateTo(360f, tween(600))
        downTick.animateTo(downTickEnd, tween(1000))
        upTick.animateTo(upTickEnd, tween(300, easing = FastOutSlowInEasing))
    }

    Box(modifier = Modifier
        .padding(padding)
        .size(dimension)
        .onGloballyPositioned {
            startAnim = !startAnim
        }
        .clickable {
            startAnim = !startAnim }
        .drawWithContent {
            drawArc(
                color = Color.Green,
                startAngle = 0f,
                sweepAngle = circleAnimation.value,
                style = Fill,
                useCenter = true
            )
            drawArc(
                color = Color.Black,
                startAngle = 0f,
                sweepAngle = circleAnimation.value,
                style = Stroke(width = tickSize*0.35f, cap = StrokeCap.Round),
                useCenter = true
            )
            if(downTick.value != downTickStart) {
                drawLine(
                    color = Color.Black,
                    cap = StrokeCap.Round,
                    strokeWidth = tickSize,
                    start = downTickStart,
                    end = downTick.value
                )
            }
            if(upTick.value != upTickStart) {
                drawLine(
                    color = Color.Black,
                    cap = StrokeCap.Round,
                    strokeWidth = tickSize,
                    start = upTickStart,
                    end = upTick.value
                )
            }
        }, contentAlignment = Alignment.Center
    ) { }
}

Below is how you can call the element in your MainActivity.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            SampleSuccessGreenCheckmarkTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Box(modifier = Modifier.padding(innerPadding).fillMaxSize(), contentAlignment = Alignment.Center) {
                        SuccessGreenCheckmark(
                            15.dp,
                            256.dp,
                            25f
                        )
                    }

                }
            }
        }
    }
}

The Box element draws the UI components in the drawWithContent function. When one Animatable is complete, it will start the next and so on until the animation is finished.

Hope this is useful.

BONUS

Below I expanded on this idea to create a better looking checkmark using Paths along with a radial gradient to the background.

@Composable
fun SuccessGreenCheckmark(
    padding: Dp,
    dimension: Dp,
    tickSize: Float
) {
    var path by remember { mutableStateOf(Path()) }
    val pathMeasure by remember{ mutableStateOf(PathMeasure()) }
    pathMeasure.setPath(path, false)

    var startAnim by remember { mutableStateOf(false) }

    val tickFloatAnim = remember { Animatable(0f) }
    val animatedPath = remember { derivedStateOf {
        val destination = Path()
        pathMeasure.setPath(path, false)
        pathMeasure.getSegment(0f, tickFloatAnim.value, destination)
        destination
    } }

    val circleAnimation = remember {  Animatable(0f) }

    LaunchedEffect(startAnim) {
        circleAnimation.snapTo(0f)
        tickFloatAnim.snapTo(0f)
        circleAnimation.animateTo(360f, tween(600))
        tickFloatAnim.animateTo(pathMeasure.length, tween(300))
    }

    Box(modifier = Modifier
        .padding(padding)
        .size(dimension)
        .onGloballyPositioned {
            startAnim = !startAnim
            path = Path().apply {
                val width = it.size.width
                val height = it.size.height
                moveTo(width*0.28f, height*0.55f)
                quadraticBezierTo(width*0.45f, height*0.65f, width*0.48f, height*0.75f)
                quadraticBezierTo(width*0.56f, height*0.5f, width*0.8f, height*0.25f)
            }
        }
        .clickable {
            startAnim = !startAnim }
        .drawWithContent {
            drawArc(
                brush = Brush.radialGradient(
                    0.0f to Color.Green.copy(alpha = 0.25f),
                    1.0f to Color.Green.copy(alpha = 0.85f)
                ),
                startAngle = 0f,
                sweepAngle = circleAnimation.value,
                style = Fill,
                useCenter = true
            )
            drawArc(
                color = Color.Black,
                startAngle = 0f,
                sweepAngle = circleAnimation.value,
                style = Stroke(width = tickSize*0.35f, cap = StrokeCap.Round),
                useCenter = true
            )
            drawPath(
                path = animatedPath.value,
                color = Color.Black,
                style = Stroke(width = tickSize, cap = StrokeCap.Round))
        }, contentAlignment = Alignment.Center
    ) { }
}

Below is the result. Slightly more aesthetically appealing.

Leave a Reply