이번 포스팅에서는 Jetpack Compose에서 SharedTransitionLayout과 sharedElement()를 이용해 Shared Element Transition을 구현한 경험을 공유합니다. 공식 사이트에 예제가 잘 올라와 있지만, 막상 적용하는데 시행착오가 있었습니다. 간단한 구현에 필요한 엑기스들만 요약해서 올려봅니다.
먼저 예시에 사용된 프로젝트는 Google 공식 샘플 앱인 nowinandroid 의 아키텍처를 채용하고 있는 점 참고부탁드립니다.
책 리스트 화면 -> 상세 화면으로 전환 시,
책 표지 이미지가 자연스럽게 전환되도록 Shared Element Transition 구현.
1. 애니메이션 적용할 전체 부분을 SharedTransitionLayout 로 씌우기.
본 예시의 경우, 두 개의 화면이 존재합니다.
그리고 화면 이동 시 공유될 element 가 있습니다.
리스트 화면의 item 을 클릭했을 때, 상세 화면으로 이동하며
가 공유됩니다.
먼저 애니메이션 적용할 전체 부분은 Navigation Graph 에 적용했습니다.
// MainActivity
SharedTransitionLayout {
NavigationGraph(navController = navController, sharedTransitionScope = this)
}
2. 애니메이션을 주고자 하는 각 element 에 sharedTransitionScope, animatedVisibilityScope 전달하기.
각 Screen -> element 까지 sharedTransitionScope을 전달합니다.
sharedTransitionScope은 SharedTransitionLayout 에서 받아올 수 있습니다.
// MainActivity
@SuppressLint("UnusedSharedTransitionModifierParameter")
@OptIn(ExperimentalAnimationApi::class, ExperimentalSharedTransitionApi::class)
@Composable
fun NavigationGraph(navController: NavHostController, sharedTransitionScope: SharedTransitionScope) {
NavHost(
navController = navController,
startDestination = MY_LIBRARY_ROUTE
) {
myLibraryScreen(
onNavigateToSearch = { navController.navigateToSearch() }
)
searchBookScreen(
sharedTransitionScope = sharedTransitionScope,
onNavigateBack = { navController.popBackStack()},
onNavigateToDetail = { isbn, cover ->
navController.navigateToSearchBookDetail(isbn, cover)
}
)
searchBookDetailScreen(
sharedTransitionScope = sharedTransitionScope,
onNavigateBack = { navController.popBackStack() }
)
}
}
//
@OptIn(ExperimentalSharedTransitionApi::class)
fun NavGraphBuilder.searchBookScreen(
sharedTransitionScope: SharedTransitionScope,
onNavigateBack: () -> Unit,
onNavigateToDetail: (isbn: String, cover: String) -> Unit,
) {
composable(SEARCH_ROUTE) {
SearchBookScreen(
sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = this,
onNavigateBack = onNavigateBack,
onNavigateToDetail = onNavigateToDetail,
)
}
}
이번에는 animatedVisibilityScope 를 전달합니다. animatedVisibilityScope 은 composable로 부터 받아올 수 있습니다.
SearchScreen 의 매개변수로 sharedTransitionScope, animatedVisibilityScope 을 넘겨주었습니다.
3. 각 element 에 key 제공, 애니메이션 효과 적용하기
이제 넘겨받은 sharedTransitionScope, animatedVisibilityScope 변수를 원하는 element 에 연결해줍니다.
### SEARCH BOOK SCREEN
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class, ExperimentalSharedTransitionApi::class)
@Composable
fun SearchBookScreen(
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope,
onNavigateBack: () -> Unit,
onNavigateToDetail: (isbn: String, cover: String) -> Unit,
viewModel: SearchViewModel = hiltViewModel(),
)
... 화면 구성..
### SEARCH BOOK SCREEN
with(sharedTransitionScope) {
SubcomposeAsyncImage(
model = book.cover,
contentDescription = null,
modifier = Modifier
.sharedElement(
rememberSharedContentState(key = "image/${book.cover}"),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = {initial, taget -> tween(durationMillis = 1000)}
)
.size(width = 90.dp, height = 140.dp)
.padding(start= 10.dp, bottom= 10.dp)
마찬가지로 SearchBookDetailScreen 의 매개변수로 sharedTransitionScope, animatedVisibilityScope 을 넘겨주었습니다.
### SEARCH BOOK DETAIL SCREEN
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
@Composable
fun SearchBookDetailScreen(
isbn: String,
cover: String,
onNavigateBack: () -> Unit,
viewModel: SearchDetailViewModel = hiltViewModel(),
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope
) {
... 화면 구성 소스
### SEARCH BOOK DETAIL SCREEN
with(sharedTransitionScope) {
SubcomposeAsyncImage(
model = cover,
contentDescription = "책 표지",
modifier = Modifier
.sharedElement(
rememberSharedContentState(key = "image/$cover"),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = {initial, taget -> tween(durationMillis = 1000)}
)
...
저의 경우에는 애니메이션을 적용할 sharedElements는 SubcomposeAsyncImage 였습니다.
애니메이션이 공유될 두 가지의 elements 에
with(sharedTransitionScope) {} 블럭으로 SubcomposeAsyncImage 을 감쌉니다.
그러면 modifier.sharedElement() 속성을 사용할 수 있게됩니다.
.sharedElement() 안에는 이제 unique 한 key 값을 넣어야 합니다.
그리고 animatedVisibilityScope 속성에는 매개변수로 받아온 변수를 세팅합니다.
마지막으로 원하는 애니메이션 효과를 적용합니다. 이 부분은 생략해도 기본 boundsTransform 애니메이션이 적용되었습니다.
더 자세한 소스는 아래 링크에서 확인 가능합니다.
https://github.com/stevey-sy/odok-compose
더 자세한 동작 원리와 효과들은 공식 가이드에서 확인 가능합니다.
https://developer.android.com/develop/ui/compose/animation/shared-elements