androidkotlinandroid-jetpack-composeshared-element-transitioncompose-multiplatform

Exit transition of Shared Element of Jetpack Compose not working


I'm developing an application in Compose Multiplatform where I implement shared element transitions between two screens: a main screen displaying multiple images (MainScreenImages) and a full-screen image view (FullScreenImage).

My goal is to have the image displayed in full screen (FullScreenImage) when tapped, and then return to the main screen when tapped anywhere on the screen. The animation from main screen to full screen works fine, but from full screen to main screen doesn't, as no animation appears.

As I said, I'm using Compose Multiplatform (that's why I have DrawableResource). This is the code I did:

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun ImagesTest(){

    val images = remember { mutableListOf(Res.drawable.im_test, Res.drawable.im_test_2) }

    var enlargeImage: DrawableResource? by remember { mutableStateOf(value = null) }

    var showImageFullScreen by remember { mutableStateOf(value = false) }

    SharedTransitionLayout {
        AnimatedContent(
            targetState = showImageFullScreen, label = "ImageTransition"
        ) { isImageFullScreen ->
            if (!isImageFullScreen) {
                MainScreenImages(
                    imagesList = images,
                    onImageClick = {
                        enlargeImage = it
                        showImageFullScreen = true
                    },
                    transitionScope = this@SharedTransitionLayout,
                    animatedVisibilityScope = this@AnimatedContent
                )
            } else {
                enlargeImage?.let {
                    FullScreenImage(
                        enlargeImage = it,
                        closeFullImage = {
                            enlargeImage = null
                            showImageFullScreen = false
                        },
                        transitionScope = this@SharedTransitionLayout,
                        animatedVisibilityScope = this@AnimatedContent
                    )
                }
            }
        }

    }
}

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun MainScreenImages(
    imagesList: List<DrawableResource>,
    onImageClick: (DrawableResource) -> Unit,
    transitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        LazyRow(
            contentPadding = PaddingValues(horizontal = 8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp)
        ) {

            items(imagesList, key = { it.hashCode() }) { image ->
                with(transitionScope) {
                    Image(
                        painter = painterResource(image),
                        contentDescription = null,
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .size(100.dp)
                            .clip(MaterialTheme.shapes.small)
                            .sharedElement(
                                state = rememberSharedContentState(key = image.hashCode()),
                                animatedVisibilityScope = animatedVisibilityScope
                            )
                            .clickable {
                                onImageClick(image)
                            }
                    )
                }

            }
        }
    }
}

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun FullScreenImage(
    enlargeImage: DrawableResource,
    closeFullImage: () -> Unit,
    transitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
) {

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black.copy(alpha = 0.8f))
            .clickable { closeFullImage() },
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        Spacer(modifier = Modifier.weight(1f))

        with(transitionScope) {

            Image(
                painter = painterResource(enlargeImage),
                contentDescription = null,
                contentScale = ContentScale.Fit,
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f)
                    .padding(horizontal = 16.dp)
                    .sharedElement(
                        state = rememberSharedContentState(key = enlargeImage.hashCode()),
                        animatedVisibilityScope = animatedVisibilityScope
                    )
            )

        }

        Spacer(modifier = Modifier.weight(1f))
    }
}

This is a gif where you can see what is happening:


Solution

  • The issue is that you're setting enlargeImage = null. The enlargeImage?.let {} bit around FullScreenImage() causes Compose to remove it before the animation can finish the transition. If you really need the null logic, you change this:

    closeFullImage = {
        enlargeImage = null
        showImageFullScreen = false
    }
    

    To this:

    closeFullImage = {
        showImageFullScreen = false
        scope.launch {
            delay(300L)
            enlargeImage = null
        }
    }
    

    In my case I got rid of the null logic entirely because AnimatedContent took care of everything just based on the showImageFullScreen variable alone.