2

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?

Thracian
  • 43,021
  • 16
  • 133
  • 222
Barrufet
  • 495
  • 1
  • 11
  • 33
  • I am not sure that you can avoid recomposition since image size changing can change the ui structure – Steyrix Dec 16 '22 at 15:31
  • Yeah I was thinking that, since `drawing` allows you to draw over a specific UI size and it's like you have a dependency on the actual size. But I had some hopes that you could do this on `Layout` phase. Since you can measure and place the size you want. – Barrufet Dec 16 '22 at 15:36
  • Found the solution, just in case you're interested @Steyrix ! – Barrufet Dec 17 '22 at 10:34

2 Answers2

3

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) 
    }
}
Barrufet
  • 495
  • 1
  • 11
  • 33
0

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")
    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Ok, this seems to answer my question and it's amazing! But I want to make an extra question: What if I want the Canvas to resize itself together with the image? Like If I set a row with Text - Image - Text. I want the Image and canvas to resize thus making an impact on the other Layouts In this example once it `enabled` is false, the 2 `Text` should be together and once is enabled then the image is between the 2 texts. Does it make sense? – Barrufet Dec 16 '22 at 17:03
  • I tried to go with this example by removing the `.size(200.dp)` from the Canvas that doesn't seem to occupy a "space" in the UI. – Barrufet Dec 16 '22 at 17:04
  • If anything other than second `Text` is effected you can add `Modifier.offset{}` to second Text or use `Layout` and change measurement constraints of Image if you know how to use Layouts and Constraints – Thracian Dec 16 '22 at 17:20
  • If you don't assign a size Modifier Composables don't occupy space by default. `Button`, `Slider`, `Switch`, etc. are assigned default sizes but in Jetpack Compose you can draw modifiers, `Modifier.drawX`, even if Composable size is 0 or outside of Composable bounds too. size field is `DrawScope.size` and calculated based on `Modifier` which is required when you need to do calculation. drawCircle(radius = 100f, center=Offset(1000f,1000f)) draws this circle even if Modifier.size(0.dp) is set – Thracian Dec 16 '22 at 19:39
  • Found the solution I was looking for, thanks for your effort! – Barrufet Dec 17 '22 at 10:33
  • @Thracian what does `translate` function do ? – Kotlin Learner Dec 17 '22 at 16:16
  • @vivekmodi let's say you draw an image with top left (0,0) if you set translate(left=100, top=100) top left position of the image is moved by 100px to bottom left – Thracian Dec 17 '22 at 17:24
  • Okk got it now. Thanks – Kotlin Learner Dec 17 '22 at 17:44