RecyclerView는 안드로이드 앱에서 많은 아이템들을 리스트로 보여줄 때 유용하게 사용되는 위젯 중 하나이다.
RecyclerView에 animation을 적용할 수 있는 방법은 크게 2가지다.
- LayoutAnimation
- RecyclerView.ItemAnimatior
LayoutAnimation
public class RecyclerView extends ViewGroup implements ...
LayoutAnimation은 RecyclerView의 고유한 개념이 아닌, ViewGroup에 통용되는 속성이다.
RecyclerView 또한 ViewGroup을 상속하고 있으므로, LayoutAnimation 적용이 가능하다.
LayoutAnimation을 적용하는 방법은 아래와 같다.
1. LayoutAnimation 속성으로 정의할 animation 집합을 xml로 작성한다.
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500">
<translate
android:fromYDelta="20%"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toYDelta="0" />
<alpha
android:fromAlpha="0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toAlpha="1" />
</set>
<translate/>은 item을 이동시키는 애니메이션 효과이다.
fromYDelta를 20%로 설정했기 때문에 살짝 아래에서 위로 올라오는 애니메이션이 진행된다.
<alpha/>는 item의 투명도를 서서히 변화시킨다.
0부터 1로 변화하므로 위 translate 속성과 함께 사용하면, 서서히 위로 올라오면서 눈에 보이는 애니메이션이 진행될 것이다.
추가로 interpolator는 animation의 보간이다.
animation을 어떻게 동작시킬지 정해줄 수 있다.
@android:anim/accelerate_decelerate_interpolator는 애니메이션이 서서히 빨라지다가 다시 느려지는 형태이다.
이 외에도 다양한 interpolator가 존재하며, 적용하면 시각적으로 부드럽고 고급진 효과를 제공할 수 있다.
2. 위에서 만든 animation 리소스를 속성으로 사용할 LayoutAnimation xml을 작성한다.
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/anim_fall_down"
android:animationOrder="random"
android:delay="100%" />
LayoutAnimation은 일반적인 animation과 달리, <layoutAnimation/> 태그로 이루어진 xml에 속성으로 지정해주어야 한다.
android:animation에 1번 과정에서 만든 animation 리소스를 등록한다.
android:animationOrder는 ViewGroup의 자식 View들에게 어떠한 순서로 애니메이션을 진행시킬지 정하는 속성이다.
여기에는 normal, reverse, random 3가지가 존재한다.
- normal : item에 순서대로 애니메이션을 적용한다.
- reverse : item에 역순으로 애니메이션을 적용한다.
- random : item에 랜덤 순서대로 애니메이션을 적용한다.
android:delay는 각 item이 애니메이션을 진행할 때의 시간차를 정해줄 수 있다.
당연한 이야기지만, 값이 작을 수록 시간차가 적고, 값이 클수록 시간차가 크다.
3. LayoutAnimation을 적용하고자 하는 ViewGroup에 android:layoutAnimation 속성으로 설정한다.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/movies_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/layout_anim_fall_down"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_movie" />
마지막으로, ViewGroup에 android:layoutAnimatoin을 통해 애니메이션을 적용해준다.
위 세 과정만 거치면, RecyclerView.ItemAnimator에 비해 비교적 간단하게 커스텀 애니메이션을 적용할 수 있다.
추가로, notifyDatasetXxx() 등 adapter에게 데이터셋 변경을 알리는 함수 호출시 애니메이션이 제대로 동작하지 않을 수 있다.
그 이유는 자식 View가 그려졌을 때, 애니메이션 1회 동작하도록 구현되어 있기 때문이다.
이러한 현상을 해결하기 위해 recyclerView.scheduleLayoutAnimation() 또는 recyclerView.startLayoutAnimation()을 함께 호출해줄 수 있다.
startLayoutAnimation()은 즉시 자식 View에 대한 animation을 진행한다.
scheduleLayoutAnimation()은 바로 애니메이션을 자식 View에 변경이 있을 때, 비로소 animation을 진행한다.
RecyclerView.ItemAnimatior
RecyclerView.ItemAnimator는 RecyclerView의 3가지 주요 구성요소(Adapter, ItemAnimator, LayoutManager)중 하나이다.
아이템이 리스트에서 추가, 제거, 이동, 변경되는 애니메이션 효과를 정의할 수 있다.
앞서 설명한 LayoutAnimation에 비해 구현이 어렵다는 단점이 있다.
하지만, 아이템 변동에 대한 다양한 callback을 제공하기 때문에 상황에 따라 애니메이션을 다르게 제공할 수 있다.
public abstract static class ItemAnimator {
...
public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
...
public class DefaultItemAnimator extends SimpleItemAnimator {
// RecyclerView.java
...
ItemAnimator mItemAnimator = new DefaultItemAnimator();
...
RecyclerView는 기본적으로 DefaultItemAnimtor 를 적용한다.
DefaultItemAnimtor의 부모 자식 관계를 간단하게 정리하면 다음과 같다.
RecyclerView.ItemAnimator
|
SimpleItemAnimator
|
DefaultItemAnimator
DefaultItemAnimator는 코드가 모두 구현되어 있기 때문에, 약간의 수정을 통해 커스터마이징할 수 있다.
반면, SimpleItemAnimator는 추상 클래스이므로 각 아이템 변동 상황에 따른 코드를 모두 구현해주어야 한다.
우선 간단하게 ItemAnimator를 RecyclerView에 어떻게 적용하는지 알아본다.
val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
val itemAnimator = DefaultItemAnimator()
recyclerView.itemAnimator = itemAnimator
RecyclerView에는 setItemAnimator라는 setter 메서드를 제공한다.
위 코드를 통해 간단히 Animator를 적용할 수 있다.
조금 더 깊게 파고 들어보자.
public void setItemAnimator(@Nullable ItemAnimator animator) {
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
mItemAnimator.setListener(null);
}
mItemAnimator = animator;
if (mItemAnimator != null) {
mItemAnimator.setListener(mItemAnimatorListener);
}
}
만약 RecyclerView에 새로운 Animator를 적용한다면, RecyclerView.ItemAnimator의 endAnimations()를 호출한다.
따라서 애니메이션이 모두 취소된다는 것을 고려할 필요가 있다.
DefaultItemAnimator
val itemAnimator = DefaultItemAnimator()
itemAnimator.addDuration = 500
recyclerView.setItemAnimator(itemAnimator)
.
.
.
// RecyclerView.kt
public void setAddDuration(long addDuration) {
mAddDuration = addDuration;
}
만약 각 Item의 애니메이션에 Duration을 정하고 싶다면 위와 같이 작성한다.
kotlin에서는 프로퍼티로 변환되어 addDuration이라고 되어 있지만, 이는 Java에서는 명백히 setter임을 주의해야 한다.
즉, Duration을 더해주는 것이 아니라 새롭게 초기화한다.
SimpleItemAnimator
지금까지 알아본 DefaultItemAnimator는 위에서 언급했듯이 기본적으로 RecyclerView에 적용되어 있으며, 모든 구현이 되어있는 구현체이다.
DefaultItemAnimator가 상속하고 있는 부모 클래스인 SimpleItemAnimator는 abstract class이기 때문에 거의 모든 콜백을 개발자가 직접 구현해야 한다.
class CustomAnimator : SimpleItemAnimator() {
// Animation 실행 대기 중인 ViewHolder 리스트
private val animViewHolders: MutableList<RecyclerView.ViewHolder> = mutableListOf()
override fun runPendingAnimations() {
// Animation queue에 대기 중인 Animation을 진행하기 위해 call
}
override fun endAnimation(item: RecyclerView.ViewHolder) {
// 애니메이션을 종료해야 할 때 call
item.itemView.animate().cancel()
}
override fun endAnimations() {
// 모든 애니메이션을 종료해야 할 때 call
// ex) setItemAnimator()를 할 때 모든 애니메이션을 종료하기 위해 호출된다.
animViewHolders.forEach { it.itemView.animate().cancel() }
}
override fun isRunning(): Boolean {
// 현재 실행중인 Animation이 있는가
return addAnimViewHolders.isNotEmpty() && moveAnimViewHolders.isNotEmpty()
}
override fun animateAdd(holder: RecyclerView.ViewHolder?): Boolean {
// notifyItemInseted() 호출시 Animation
addAnimViewHolders.add(holder)
}
override fun animateRemove(holder: RecyclerView.ViewHolder?): Boolean {
// notifyItemRemoved() 호출시 Animation
}
override fun animateMove(
holder: RecyclerView.ViewHolder?,
fromX: Int,
fromY: Int,
toX: Int,
toY: Int,
): Boolean {
// notifyItemMoved() 호출시 Animation
animViewHolders.add(holder)
}
override fun animateChange(
oldHolder: RecyclerView.ViewHolder?,
newHolder: RecyclerView.ViewHolder?,
fromLeft: Int,
fromTop: Int,
toLeft: Int,
toTop: Int,
): Boolean {
// notifyItemChanged() 호출시 call
}
}
위 코드의 주석을 보면 언제 호출되는지 알 수 있다.
현재 animation 실행을 대기하고 있는 ViewHolder를 관리하기 위해 MutableList를 만들어주었다.
runPendingAnimations()
animateAppearance(), animateChange(), animatePersistence(), animateDisappearance() 의 반환 값에 따라 호출이 결정된다. 반환값이 true인 경우 호출된다.
runPendingAnimations() will be scheduled to be run on the next frame.
데이터 세트가 변경되고 RecyclerView가 다시 레이아웃을 그린 후, runPendingAnimations() 메서드가 호출되어 ItemAnimator에 대한 애니메이션을 실행한다.
endAnimation(item: RecyclerView.ViewHolder)
특정 ViewHolder의 animaiton을 종료해야 할 때 호출되는 endAnimation()에서는 viewHolder가 가지고 있는 view의 애니메이션을 cancel() 해주어야 한다.
endAnimations()
모든 ViewHolder의 애니메이션을 종료해야 하는 경우 호출된다.
ex) RecyclerView.setItemAniamtor()
이 때에는, 인자로 ViewHolder가 따로 주어지지 않기 때문에 별도로 관리하고 있는 ViewHolder 리스트에서 각각 종료해주어야 한다.
isRunning()
현재 실행 중인 Animation이 있는지를 반환한다.
ViewHolder를 담고 있는 리스트가 비어있지 않다는 것은 곧 실행해야 할 Animation이 있다는 것이기 때문에 리스트가 비었는지 여부를 반환해준다.
animateChange(), animateRemove(), animateAdd(), animateMove()
Adapter의 notifyItemXxx() 메서드 호출시 실행된다.
애니메이션 호출시, 관리를 위해 ViewHolder List에 Item을 추가해준다.
각각 notifyItemXxx() 메서드가 호출될 때 상황에 적절한 Animation을 커스터마이징하여 호출한다.
holder.itemView.animate().translationY(20F)...
animateXxx() 메서드의 반환 타입은 Boolean이다.
공식 문서에 따르면 아래와 같다.
반환값은 애니메이션이 설정되었는지 여부와 다음 기회에 ItemAnimator의 runPendingAnimations() 메서드를 호출해야 하는지 여부를 나타냅니다. 이 메커니즘을 통해 ItemAnimator는 개별 애니메이션을 설정할 수 있으며, animateAdd(), animateMove(), animateRemove(), animateChange()에 대한 호출이 하나씩 들어올 때 개별 애니메이션을 설정한 다음 나중에 runPendingAnimations()를 호출할 때 애니메이션을 함께 시작할 수 있습니다.
public final void dispatchAddFinished(RecyclerView.ViewHolder item) {
onAddFinished(item);
dispatchAnimationFinished(item);
}
구현시 전체적으로 중요한 점은 Animation 시작 종료시에 dispatchXxxStarting(), dispatchXxxFinished() 을 적절히 호출해주어야 한다.
if (!isRunning) {
dispatchAnimationsFinished()
}
Animation 리스너를 등록하여 모든 Animation들이 끝났는지 확인하고, dispatchAnimationsFinished()를 호출한다.
dispatch prefix 메서드를 제대로 호출해주지 않으면 Animation이 꼬이거나, 잔상이 남는 경우가 발생할 수 있다.
LayoutAnimation에 비해 구현시 고려해야할 지식이 훨씬 많다.
하지만 그만큼 각 상황에 대해 더 유연하게 커스터마이징 할 수 있다는 장점이 있다.
'Android' 카테고리의 다른 글
[Android] API 33 onBackPressed() deprecated (0) | 2023.05.08 |
---|---|
[Android] PendingIntent 공식문서 파헤치기 (0) | 2023.05.02 |
[Android] Listview vs RecyclerView (1) | 2023.04.20 |
[Android] This version of the Android Support plugin for IntelliJ IDEA (or Android Studio) 오류 해결 (2) | 2022.12.23 |
[Android] ViewModel + Event Wrapper Pattern 단일 이벤트 처리 (0) | 2022.12.12 |