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:
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.