Is it possible to change the size of the Image composable without triggering recomposition

1.7k Views Asked by At

I have an animateDpAsState(..), whenever this animation is triggered it changes the Modifier.size(value) of an Image(...) thus causing recomposition.

Is there a way to skip composition phase for this specific scenario? Allowing an image to change its size?

2

There are 2 best solutions below

0
On

I found the solution! To skip recomposition but still affect things around the layout, you can do it do it in layout phase, otherwise move it to Draw phase!

Apply this Modifier to the Image(...) modifier parameter.

Modifier.layout { measurable, constraints ->
    val size = animationSize.toPx() // getting the size of the current animation
    val placeable = measurable.measure(Constraints.fixed(size.toInt(), size.toInt())) // setting the actual constraints of the image

    // Set the layout with the same width and height of the image.
    // Inside the layout we will place the image. This layout function is like a "box"
    layout(placeable.width,placeable.height) {
        // And then we will place the image inside the "box"
        placeable.placeRelative(0, 0) 
    }
}
8
On

You can do it using Modifier.drawWithContent, Modifier.drawBeheind or using Canvas which is a Spacer with Modifier.drawBehind. Modifiers with lambda trigger Layout, Layout->Draw or Draw phases skipping Composition as in this answer.

The snippet below changes size with animation and if you want size changes to be applied from center you can add translate either

enter image description here

@Composable
private fun ImageSizeAnimationSample() {
    val painter = painterResource(id = R.drawable.landscape1)
    var enabled by remember { mutableStateOf(true) }
    val sizeDp by animateDpAsState(if (enabled) 200.dp else 0.dp)
    val density = LocalDensity.current
    val context = LocalContext.current

    SideEffect {
        println(" Composing...")
        Toast.makeText(context, "Composing...", Toast.LENGTH_SHORT).show()
    }

    Canvas(modifier = Modifier.size(200.dp)) {
        val dimension = density.run { sizeDp.toPx() }
        with(painter) {
            draw(size = Size(dimension, dimension))
        }
    }

    Button(onClick = { enabled = !enabled }) {
        Text("Enabled: $enabled")
    }
}

With translation

Canvas(modifier = Modifier.size(200.dp)) {
    val dimension = density.run { sizeDp.toPx() }
    with(painter) {
        translate(left = (size.width - dimension) / 2, top = (size.height - dimension) / 2) {
            draw(size = Size(dimension, dimension))
        }
    }
}

enter image description here

In these examples only one recomposition is triggered for animation because

    val sizeDp by animateDpAsState(if (enabled) 200.dp else 0.dp)

reads enabled value but you can handle animations with Animatable which won't trigger any recomposition either.

@Composable
private fun ImageSizeAnimationWithAnimatableSample() {
    val painter = painterResource(id = R.drawable.landscape1)
    val animatable = remember { Animatable(0f) }
    val coroutineScope = rememberCoroutineScope()

    val context = LocalContext.current

    SideEffect {
        println(" Composing...")
        Toast.makeText(context, "Composing...", Toast.LENGTH_SHORT).show()
    }

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

        with(painter) {
            val dimension = size.width * animatable.value
            translate(left = (size.width - dimension) / 2, top = (size.height - dimension) / 2) {
                draw(size = Size(dimension, dimension))
            }
        }
    }

    Button(onClick = {
        coroutineScope.launch {
            val value = animatable.value
            if(value == 1f){
                animatable.animateTo(0f)
            }else {
                animatable.animateTo(1f)
            }
        }
    }) {
        Text("Animate")
    }
}