How To Use Shared Element Transitions in Jetpack Compose

Kürşat
3 min readMay 30, 2024

--

Today, I will explore how we can use shared element transitions in our projects. Let’s start!

1.Add Dependencies

First, we need to add some dependencies to implement shared element transition.

implementation("androidx.navigation:navigation-compose:2.7.7")
implementation("androidx.compose.animation:animation:1.7.0-alpha07")

2. Create City Data Class & Mock Data

Before designing the screens, we need to create a data class and mock data

Data Class

data class City(
val name : String,
@DrawableRes val pic : Int,
)

Mock Data

object Cities {
fun getCities() : List<City> {
return listOf(
City(
name = "Istanbul",
pic = R.drawable.pic_istanbul
),
City(
name = "New York",
pic = R.drawable.pic_newyork
),
City(
name = "Paris",
pic = R.drawable.pic_paris
)
)
}
}

4. Design the Screens

We’ll have two screens in this project. To use shared element transition, screens must be surrounded by SharedTransitionScope. Therefore we need to make our screens an extension function. To animate image and text, we’ll use Modifier.sharedElement. This modifier takes two arguments: one is state and the other is animatedVisibilityScope.

Home Screen

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SharedTransitionScope.HomeScreen(
animatedVisibilityScope: AnimatedVisibilityScope,
onNavigateDetailScreen: (Int, String) -> Unit
) {

val cities = getCities()
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
modifier = Modifier.padding(30.dp),
text = "FEATURED CITIES",
fontWeight = FontWeight.ExtraBold,
fontSize = 22.sp,
)
LazyRow(modifier = Modifier.fillMaxSize()) {
items(cities.size) { index ->
val city = cities[index]
Column(
modifier = Modifier
.padding(10.dp)
.clickable { onNavigateDetailScreen(city.pic, city.name) },
) {
Image(
modifier = Modifier
.size(width = 300.dp, height = 350.dp)
.clip(RoundedCornerShape(12.dp))
.sharedElement(
state = rememberSharedContentState(
key = "image-${city.pic}"
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 1000)
}
),
contentScale = ContentScale.Crop,
painter = painterResource(id = city.pic),
contentDescription = "List Image"
)
Text(
modifier = Modifier
.sharedElement(
state = rememberSharedContentState(
key = "name-${city.name}"
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 1000)
}
),
text = city.name,
fontSize = 26.sp, fontWeight = FontWeight.Bold)
}
}
}
}
}

Detail Screen

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SharedTransitionScope.DetailScreen(
animatedVisibilityScope: AnimatedVisibilityScope,
resId: Int,
name: String
) {
Box {
Image(
modifier = Modifier
.fillMaxSize()
.height(300.dp)
.sharedElement(
state = rememberSharedContentState(
key = "image-${resId}"
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 1000)
}
),
contentScale = ContentScale.Crop,
painter = painterResource(id = resId),
contentDescription = "List Image"
)
Spacer(modifier = Modifier.height(50.dp))
Text(
modifier = Modifier
.align(Alignment.TopStart)
.padding(40.dp)
.sharedElement(
state = rememberSharedContentState(
key = "name-${name}"
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 1000)
}
),
text = name,
fontSize = 26.sp, fontWeight = FontWeight.Bold, color = Color.White)
}
}

3. Set up NavGraph

Before setting up NavHost, we’ll create a Sealed Class for our destinations. In this project, we’ll have two different screens.

Sealed Class for destinations

sealed class Screen (val route : String) {
data object HomeScreen : Screen(route = "home_screen")
data object DetailScreen : Screen(route = "detail_screen")
}

NavGraph

To use Shared Transition Element, the NavHost must be surrounded by SharedTransitionLayout.

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun NavGraph(navController: NavHostController) {
SharedTransitionLayout {
NavHost(navController = navController, startDestination = Screen.HomeScreen.route) {
composable(Screen.HomeScreen.route) {
HomeScreen(
animatedVisibilityScope = this,
onNavigateDetailScreen = { resId, name ->
navController.navigate("${Screen.DetailScreen.route}/$resId/$name")
})
}
composable(
"${Screen.DetailScreen.route}/{resId}/{name}",
arguments = listOf(
navArgument("resId") { type = NavType.IntType },
navArgument("name") { type = NavType.StringType }
)
) { backStackEntry ->
val resId = backStackEntry.arguments?.getInt("resId") ?: R.drawable.pic_istanbul
val name = backStackEntry.arguments?.getString("name") ?: ""

DetailScreen(animatedVisibilityScope = this, resId = resId, name = name)
}
}
}
}

As you can see, implementing Shared Element Transition is quite easy. I hope this writing will be helpful.

https://github.com/kursatkumsuz/shared-element-transition-jetpack-compose

--

--