패스트터틀

Android Shared Elements Transition 사용 및 삽질기 (+with navigation component, recycler view) 본문

Development language/android

Android Shared Elements Transition 사용 및 삽질기 (+with navigation component, recycler view)

SudekY 2023. 2. 6. 17:28

배달의민족을 사용하다가 아래와 같이 음식이미지가 애니메이션과 Screen 영역까지 고정되어서 연결되는 애니메이션이 뭔지 궁금해서 찾아보았다.

 

 

 

근데 뭘 어떻게 검색해야되지 하다가 moved fixed image.. fragment to fragment fixed image ... 등 어찌어찌 검색하다가  

Shared Elements Transition 를 발견했다.

 

Shared Elements Transition

위와 같은 애니메이션을 Shared Elements Transition 이라고한다. 사용법은 Shared Element(공유요소) 를 지정하고 transition 애니메이션을 작동시키면 된다. 자세한 내용은 아래 문서에 잘 정리되어있다. 참고로 API 21 이상에서 사용가능하여 그 아래 버젼은 사용시 분기처리가 필요하다

 

https://developer.android.com/training/transitions/start-activity?hl=ko#start-with-element] 

 

애니메이션으로 활동 시작  |  Android 개발자  |  Android Developers

애니메이션으로 활동 시작 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 머티어리얼 디자인 앱의 활동 전환은 공통 요소 간의 모션 및 변환을 통해 서로

developer.android.com

 

 

 

나는 Fragment A -> Fragment B 의 구조로 Detail 한 정보를 Fragment B 에서 처리하고 싶었다.

나는 아래 세 가지를 삽질했다.

1. Navigation Component 와 어떻게 사용해야하지?

2. 왜 안되는거야? RecycleView 는 다르게 설정해야되는건가?

3. 이제되는것 같은데 Pop 할 때는 동작을 안하는데?

 

 

우선 첫 째로 Navigation Component 와의 사용은 검색해보니 문서에 있었다.

https://developer.android.com/guide/navigation/navigation-animate-transitions?hl=ko 

 

대상 간 전환 애니메이션 처리  |  Android 개발자  |  Android Developers

Jetpack의 탐색 구성요소로 대상 간 전환을 애니메이션 처리합니다.

developer.android.com

본래 FragmentManager 에서 commit 할 때 추가요소로 FragmentTransaction.addSharedElement() 값을 추가해야하는데 이를 아래와 같은 코드로 작성하고 extras 값을 추가로 navigate 할 때 추가로 넣어주면 된다.

val extras = FragmentNavigatorExtras(view1 to "hero_image")

view.findNavController().navigate(
    R.id.confirmationAction,
    null, // Bundle of args
    null, // NavOptions
    extras)

 

 

 

그리고 두 번째로 RecylerView 와의 사용이다. 

여기서부터 시간을 잡아먹었는데 그 이유가 아래 문서를 읽지 못해서이다..

샘플코드를 참고해도 계속 작동을 하지 않았는데 아래 문서에 나와있듯이 Recylerview 사용시에는 transitionName 을 xml 에 정의해서는 안된다.

 

 

문서가 여기저기 나뉘어져 있어서 놓쳤던것 같고 사실 TransitionName 을 xml 에서 고정했는데 list 내 View 마다 전부 다 다른 View 인데 이게 가능하다고? 의심은 하긴했는데 그 의심이 옳았다.ㅠ

https://developer.android.com/guide/fragments/animate#recyclerview

 

Navigate between fragments using animations  |  Android Developers

Navigate between fragments using animations Stay organized with collections Save and categorize content based on your preferences. The Fragment API provides two ways to use motion effects and transformations to visually connect fragments during navigation.

developer.android.com

그렇다면 TransitionName 을 무엇으로 지정한다는 말인가? 문서에서는 아래와 같이 설명되어있다

Another point to consider when using shared element transitions with a RecyclerView is that you cannot set the transition name in the RecyclerView item's XML layout because an arbitrary number of items share that layout. A unique transition name must be assigned so that the transition animation uses the correct view.

해당 View 와 연결된 Unique 한 값으로 지정하라고 하는데 나는 사용하는 Hero 데이터 객체의 ID 를 사용했다. 

 

 

간단하게 코드로 보면 아래와 같다.

avengersAdapter = AvengersAdapter( // <-- 어뎁터
    onHeroClick = { view: View, hero: Hero ->
    	// 고차함수를 사용하여 선택된 이미지의 view 객체를 같이 받습니다
        navigateToAvengersDetail(view, hero)
    }
)
...
..
.

fun navigateToAvengersDetail(view: View, hero: Hero){
    val actions = AvengersFragmentDirections.actionAvengersFragmentToAvengersDetailFragment(hero)
    ViewCompat.setTransitionName(view, hero.id.toString())
    val extras = FragmentNavigatorExtras(view to view.transitionName)
    findNavController().navigate(actions, extras)
}

 

위 코드에서 FragemntNavigatorExtras 로 값을 넣을때 view 와 해당 view 의 transitionName 을 넣어야한다.

근데 왜 인지 모르지만 ViewCompat 을 통해 값을 넣고 가져오는(view.transitionName) 형태로 넣어야 하고 직접 넣는다면 에러가 발생했다. 참고하면 될것같다.

 

 

Fragment B(Transition 이 끝나는 Fragment) 에서는 아래와 같이 설정한다.

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?,
): View {
    sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(R.transition.shared_image)
    return super.onCreateView(inflater, container, savedInstanceState)
}

// TODO: 공유 요소를 사용하여 이미지뷰를 출력
override fun init() {
    binding.imageviewAvengersDetailFragmentCenter.transitionName = args.hero.id.toString()
    Glide.with(binding.imageviewAvengersDetailFragmentCenter)
        .load("${hero.thumbnail.path}.${hero.thumbnail.extension}")
        .into(binding.imageviewAvengersDetailFragmentCenter)
}

onCreateView 에서 sharedElementEnterTransition 를 설정한다. 그리고 해당 ImageView 의 transitionName 을 Fragment A 에서 설정한 값과 동일하게 설정하고 이미지를 화면에 올리면된다.

 

중간 결과물은 아래와 같다.

 

 

 

Pop 할 때는 왜 동작을 안하지?

위 사진을 보면 알겠지만 클릭시에는 애니메이션이 동작을 하지만 pop 시에는 동작을 하지 않는다.

이는 ViewHolder 내 아이템내 View 의 모든 TransitionName 에 대하여 정의를 하지 않아서 발생하는 문제였다.

정의는 ViewHolder 에서 진행한다.

 

그리고 이미지가 전부 로드되기전에는 아래 함수를 호출하여 Transition 애니메이션을 지연시키고

postponeEnterTransition()

이미지가 전부 로드되면 Transition 을 실행시켜야한다.

startPostponedEnterTransition()

 

이 순서를 지켜야만 정상동작을 한다. 사실 수 많은 예제에서는 지연(postpone) 관련 처리를 하지 않아도 동작을 하는데 Recyclerview 는 이것을 지켜야하는것같고 아마도 Shared Elements 가 처리되는 동안의 다른 애니메이션이 처리되야 하기 때문으로 추측해본다..

정확히 이게 올바른 코드인지는 모르나 그래도 5시간의 삽질끝에 알아낸 결과이다...

 

아래는 ViewHolder 클래스내 bind 부분이고 

listItemHeroImgView.transitionName = item.id.toString()

로 모든 View 의 TransitionName 을 처리하도록 진행하였다.

그리고 Glide 는 Listener 로 받아 이미지가 로드가 완료되었을때(실패 or 성공) 아래 Fragment init 코드부분에서 startPostponedEnterTransition 을 호출하도록 하였다.

 

ViewHolder

fun bind(onHeroClick: (View, Hero) -> Unit, item: Hero, imgLoadComplete : () -> Unit) {
    binding.apply {
        hero = item
        listItemHeroImgView.transitionName = item.id.toString()
        Glide.with(listItemHeroImgView)
            .load("${item.thumbnail.path}.${item.thumbnail.extension}")
            .addListener(object: RequestListener<Drawable>{
                override fun onLoadFailed(
                    e: GlideException?,
                    model: Any?,
                    target: Target<Drawable>?,
                    isFirstResource: Boolean,
                ): Boolean {
                    imgLoadComplete()
                    return false
                }

                override fun onResourceReady(
                    resource: Drawable?,
                    model: Any?,
                    target: Target<Drawable>?,
                    dataSource: DataSource?,
                    isFirstResource: Boolean,
                ): Boolean {
                    imgLoadComplete()
                    return false
                }

            })
            .error(R.drawable.whoishe)
            .placeholder(R.drawable.whoishe)
            .into(listItemHeroImgView)
        listItemHeroImgView.setOnClickListener {
            onHeroClick(listItemHeroImgView, item)
        }
    }

 

AvengersFragment -  init() 부분을 참고

@AndroidEntryPoint
class AvengersFragment : BaseFragment<FragmentAvengersBinding>(R.layout.fragment_avengers) {

    lateinit var avengersAdapter: AvengersAdapter

    private val viewModel: AvengersViewModel by viewModels()

    override fun init() {
        postponeEnterTransition()
        avengersAdapter = AvengersAdapter(
            onHeroClick = { view: View, hero: Hero ->
                navigateToAvengersDetail(view, hero)
            },
            imgLoadComplete = {
                startPostponedEnterTransition()
            }
        )

 

 

 

[ 최종 결과물 ]

 

 

 

 

해당 프로젝트는 아래와 같다.

https://github.com/sdk0213/Multi-Module-Hilt-Sample-Project

 

GitHub - sdk0213/Multi-Module-Hilt-Sample-Project: MultiModule/Hilt/UnitTest

MultiModule/Hilt/UnitTest. Contribute to sdk0213/Multi-Module-Hilt-Sample-Project development by creating an account on GitHub.

github.com

근데 이렇게 보면 코드 찾아보기 귀찮을것같으니 위 코드를 적용한 commit revision 링크는 아래와 같다.

패치를 진행한 revision 은 두개이다. 두 개를 전부 참고하면 될것같다.

 

(첫번째 commit revision - 2번사항까지 해결한 부분)

https://github.com/sdk0213/Multi-Module-Hilt-Sample-Project/commit/5c16888762d8459df5f9afc8cd2f2a273a625a21

 

Added image transition effects in module:feature:avengers · sdk0213/Multi-Module-Hilt-Sample-Project@5c16888

Show file tree Showing 6 changed files with 70 additions and 14 deletions.

github.com

(두번째 commit revision - 3번 사항까지 해결한 부분)

https://github.com/sdk0213/Multi-Module-Hilt-Sample-Project/commit/8558d00c105e46760846527777601ef1f7835d68

 

Fixed an issue where animation did not work in a navigation pop situa… · sdk0213/Multi-Module-Hilt-Sample-Project@8558d00

…tion

github.com

 

 

 

 

참고한 프로젝트와 블로그는 아래와 같다.(근데 그냥 문서 읽는게 더 빠르다)

https://mikescamell.com/shared-element-transitions-part-4-recyclerview/

https://github.com/android/animation-samples/tree/master/GridToPager

Comments