0

I have an image that is occupying the whole screen. It is like a background.

I am rendering using a Canvas with the method drawImage(...) which renders it just fine, and to my expectations. I want to change the source of the Image upon clicking on it.

How do I animate this change?

I don't think Canvas offers animation APIs for anything, much less for an image. Any sort of animation would work. Cross-Fade, Slide (Preferred, by the way). Is it even possible in Canvas? If not, what would be the correct way to implement this?

My image drawing code:

Canvas(modifier = Modifier.fillMaxSize()) {
    drawImage(
        background,
        srcSize = IntSize(background.width, background.height),
        dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt())
    )
}
Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42

2 Answers2

1

I hope my example will help you understand how it works, you can use AnimatedContent to make as complex animation as you want. I didn't use canvas.

Row {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(
        targetState = count,
        transitionSpec = {
            // Compare the incoming number with the previous number.
            if (targetState > initialState) {
                // If the target number is larger, it slides up and fades in
                // while the initial (smaller) number slides up and fades out.
                slideInVertically({ height -> height }) + fadeIn() with
                        slideOutVertically({ height -> -height }) + fadeOut()
            } else {
                // If the target number is smaller, it slides down and fades in
                // while the initial number slides down and fades out.
                slideInVertically({ height -> -height }) + fadeIn() with
                        slideOutVertically({ height -> height }) + fadeOut()
            }.using(
                // Disable clipping since the faded slide-in/out should
                // be displayed out of bounds.
                SizeTransform(clip = false)
            )
        }
    ) { targetCount ->
        Image(
            imageVector = if (targetCount % 2 == 0) Icons.Filled.ArrowBack else Icons.Filled.AccountBox,
            contentDescription = "test"
        )
    }
}

Result

enter image description here

Sergei S
  • 2,553
  • 27
  • 36
  • Is it not true that all the `height -> height` calls can be replaced by just `it`? – Richard Onslow Roper Nov 18 '21 at 17:17
  • 1
    It works alright. I'll just analyze Philip's answer and mark the one that suits my use-case. – Richard Onslow Roper Nov 18 '21 at 17:19
  • 1
    Had to mark that one. Actually I had already been using `Canvas` so it required minimal refactoring. But rest assured, your answer is great. The only thing being that the method of recompositions is not quite the convention we use. I have multiple images, so Philip's answer actually seems to be specifically tailored to the use-case. You see, whenever I change the background, it doesn't depend on what the background is. It just renders the provided background. It follows the principle of Object Oriented Programming, so... marked that. Upvote to you for demonstrating the method. It is official. – Richard Onslow Roper Nov 18 '21 at 17:57
1

You can use Animatable to animate position of anything you draw inside Canvas, and draw both old/new images like this:

var currentBackground by remember { mutableStateOf<ImageBitmap?>(null) }
var newBackground by remember { mutableStateOf<ImageBitmap?>(null) }
val offset = remember { Animatable(0f) }

LaunchedEffect(background) {
    if (currentBackground == null) {
        currentBackground = background
        return@LaunchedEffect
    }
    newBackground = background
    offset.animateTo(1f)
    currentBackground = background
    newBackground = null
    offset.snapTo(0f)
}
Canvas(modifier = Modifier.fillMaxSize()) {
    fun drawImage(image: ImageBitmap, offset: Float)  {
        drawImage(
            image,
            srcSize = IntSize(image.width, image.height),
            dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt()),
            dstOffset = IntOffset(0, (size.height * offset).roundToInt()),
        )
    }
    clipRect {
        currentBackground?.let {
            drawImage(it, offset.value)
        }
        newBackground?.let {
            drawImage(it, offset.value - 1)
        }
    }
}

Result:

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • Ok, everything is understood. Only thing being this question: **Inside `LaunchedEffect`, where you are calling the `animate` method on the `Animatable`, does the control wait for the animation to finish, or is it run in the background and execution is continued in parallel?**. The Obvious answer seems to be the latter, but.. I don't know it doesn't sound right to me. Please confirm anyway, thank you Philip. – Richard Onslow Roper Nov 18 '21 at 17:46
  • @MARSK `animateTo` is a coroutine call, it waits for the animation to finish without any freezes. Well you've complicated it by drawing the image in the `Canvas`, I just have answered your question =) – Phil Dukhov Nov 18 '21 at 22:39
  • That's pretty nice of you. I knew only Canvas that could actually stretch the image to the screen. I know `Image` might do it too, but I just didn't feel like going through the documentation (it is quite hard to find stuff there, honestly). Anyway, I guess if it actually would have been an image, it would have been easier to implement this same thing. I get it. – Richard Onslow Roper Nov 19 '21 at 07:11