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