일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Recylcer
- Android
- 치유
- javap
- throws
- HelloWorld
- 취약점
- 버킷리스트
- opcode
- IMPLEMENT
- ㅇ
- Shared Elements
- Transition
- bytecode 분석
- static
- abstract
- jvm
- 여행계획
- Interface
- 보안취약점
- 일상회피
- 심리여행
- 보안
- 회피
- 일상탈출
- bytecode
- Navigation Component
- extends
- 여행
- 심리학
- Today
- Total
패스트터틀
Android Shared Elements Transition 사용 및 삽질기 (+with navigation component, recycler view) 본문
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]
나는 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
본래 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
그렇다면 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
근데 이렇게 보면 코드 찾아보기 귀찮을것같으니 위 코드를 적용한 commit revision 링크는 아래와 같다.
패치를 진행한 revision 은 두개이다. 두 개를 전부 참고하면 될것같다.
(첫번째 commit revision - 2번사항까지 해결한 부분)
(두번째 commit revision - 3번 사항까지 해결한 부분)
참고한 프로젝트와 블로그는 아래와 같다.(근데 그냥 문서 읽는게 더 빠르다)
https://mikescamell.com/shared-element-transitions-part-4-recyclerview/
https://github.com/android/animation-samples/tree/master/GridToPager
'Development language > android' 카테고리의 다른 글
Android Paging v3 라이브러리 + 맵 마커 최적화 적용기 (2) | 2021.12.25 |
---|---|
Error about sun/misc/BASE64Encoder 해결법 (Eclipse) (0) | 2021.11.25 |
앱 빌드후 홈런쳐에서 실행시 onResume() 이 작동하지 않고 항상 앱이 재시작될때 (0) | 2021.02.25 |