Search code examples
kotlincanvasandroid-animationandroid-canvasandroid-jetpack-compose

How to handle animating multiple shapes in Android Canvas?


Problem

I am working with Canvas via Android's Jetpack Compose. I'm trying to draw a bar chart with animations, and it works well after the first animation. So whens starting the app, nothing is drawn on screen except the bottom numbers (correct). Then pressing a button changes state and the first bar is drawn to the screen (correct) but it does not animate(incorrect). Afterwards all state changes animate correctly. I'm not sure why.

Attempts

I've tried a few things, with my current close-to-working solution being an animation variable per-bar I want to animate. This works, like above, after the first bar is drawn to the screen. The problem is I can't make it animate on the first bar.

Code Example

I would like to share all of my code for this. It's for learning, so I figure the better code example available the more likely someone can help and the more help this question will be to others.


@Composable
fun RollChart(rolls: Map<Int, Int>, modifier: Modifier) {
    Box(
        modifier = modifier

    ) {

        val animationTargetState = (0..10).map { index ->
            remember { mutableStateOf(0f) }
        }

        val animationValues = (0..10).map { index ->
            animateFloatAsState(
                targetValue = animationTargetState[index].value,
                animationSpec = tween(durationMillis = 1000),
            )
        }

        Canvas(modifier = Modifier.fillMaxSize()) {
            inset(horizontal = 50f, vertical = 48f) {
                val maxRole = rolls.maxOf { it.value }
                val width = (size.width / rolls.size)
                val heightScale = (size.height / maxRole)
                withTransform({
                    translate(left = 50f)
                }) {
                    rolls.forEach { (label, height) ->
                        animationTargetState[label - 2].value =
                            heightScale * height.toFloat()
                        bar(
                            "$label",
                            width,
                            label - 2,
                            animationValues[label - 2].value
                        )
                    }
                }
                val range = IntRange(0, maxRole).step(1 + (maxRole / 5))
                if (range.last == 0) {
                    numberLine("0", 0f)
                }
                for (it in range) {
                    numberLine(
                        "$it",
                        heightScale * it
                    )

                }
            }
        }
    }

}

val textPaint = Paint().asFrameworkPaint().apply {
    isAntiAlias = true
    textSize = 48f
    color = android.graphics.Color.BLACK
    typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD)
}

fun DrawScope.numberLine(
    label: String,
    height: Float
) {
    val base = size.height
    drawLine(
        color = Color.Black,
        start = Offset(0f, base - height),
        end = Offset(size.width, base - height),
        strokeWidth = 5f,
        alpha = 0.5f
    )
    drawIntoCanvas {
        it.nativeCanvas.drawText(
            "$label",
            0f,
            (base - height) - 5f,
            textPaint
        )
    }
}

fun DrawScope.bar(
    label: String,
    width: Float,
    roll: Int,
    height: Float
) {
    val xPos = width * roll.toFloat()
    val base = size.height

    drawRect(
        color = Purple700,
        topLeft = Offset(xPos, base),
        size = Size(width - 10, 0 - height)
    )
    drawIntoCanvas {
        it.nativeCanvas.drawText(
            "$label",
            xPos,
            base + 48f,
            textPaint
        )
    }
}

Also I imagine a video demonstration is helpful so I have it here.

Example Gif


Solution

  • So, I realized the error was in this line:

    val heightScale = (size.height / maxRole)
    

    The maxRole at the start is 0, which causes the animation to never occur. If anyone can then answer why I'd appreciate it, but my thought is that the animation calls are crashing with a number that is impossible. And then after the first render all the maxRole values are > 0.