🔥 Android 에서 애니메이션을 위해 사용하는 MotionLayout 에 대해 알아보자.
Android 에서 애니메이션 효과를 주기 위해서 사용하는 방법에는 여러가지가 있다.
이번에는 그 중에서 MotionLayout 에 대해 알아보고 사용한 예제까지 살펴보려고 한다.
MotionLayout 은 ConstraintLayout 을 상속받은 ViewGroup 이다.
따라서 ConstraintLayout 의 다양한 레이아웃 기능을 기초로 한다.
실시간 상호작용하는 애니메이션 처리를 할 때, 효과적이며 UI 의 요소 이동 및 크기 조절 등을 쉽게 설정할 수 있다.
자세한 사항은 공식 문서를 살펴보자.
공식 문서 : https://developer.android.com/training/constraint-layout/motionlayout?hl=ko
MotionLayout 은 실제로 사용자가 View 를 잡아끄는 등의 모션을 취했을 때, 바로 적용할 수 있다는 장점이 있다.
하지만, 이번 예제에서는 MotionLayout 을 사용하는 기본적인 방법에 중심을 맞추었기 때문에 애니메이션 시작과 끝을 직접 정의해주었다.
아래는 세로 방향 RecyclerView 스크롤을 감지하여 애니메이션 동작을 추가한 예시이다.
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="2dp"
app:layoutDescription="@xml/fragment_ingredient_xml_motionlayout_scene"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/classificationRecyclerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintBottom_toTopOf="@id/noticeTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/noticeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="15dp"
android:background="@drawable/info_round_background"
android:paddingStart="15dp"
android:paddingTop="5dp"
android:paddingEnd="15dp"
android:paddingBottom="5dp"
android:text="@string/text_notice"
android:textSize="15sp"
android:textStyle="bold"
app:drawableLeftCompat="@drawable/info_icon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/classificationRecyclerView" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/selectButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/recommend_button"
android:text="@string/select_button"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
android:id="@+id/motionLayoutTransition"
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="500">
<KeyFrameSet>
<KeyAttribute
motion:framePosition="50"
motion:motionTarget="@id/classificationRecyclerView">
<CustomAttribute
motion:attributeName="alpha"
motion:customFloatValue="1" />
</KeyAttribute>
<KeyAttribute
motion:framePosition="100"
motion:motionTarget="@id/classificationRecyclerView">
<CustomAttribute
motion:attributeName="alpha"
motion:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
motion:framePosition="50"
motion:motionTarget="@id/selectButton">
<CustomAttribute
motion:attributeName="alpha"
motion:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
motion:framePosition="100"
motion:motionTarget="@id/selectButton">
<CustomAttribute
motion:attributeName="alpha"
motion:customFloatValue="1" />
</KeyAttribute>
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/noticeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="15dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@id/classificationRecyclerView" />
<Constraint
android:id="@+id/selectButton"
android:layout_width="0.1dp"
android:layout_height="0.1dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/classificationRecyclerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/noticeTextView"
android:layout_width="0.1dp"
android:layout_height="0.1dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@id/classificationRecyclerView" />
<Constraint
android:id="@+id/selectButton"
android:layout_width="match_parent"
android:layout_height="match_parent"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/classificationRecyclerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
motion:layout_constraintBottom_toTopOf="@id/noticeTextView"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
</MotionScene>
// 위 예제는 recyclerView 스크롤 시 애니메이션이 동작해야 하므로, RecyclerView Scroll event 정의
binding.recyclerMain.apply {
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
// 스크롤을 위로 올렸을 경우, 첫 번째 항목이 완전히 보이는지 확인 (맨 위까지 스크롤),
// 버벅거림 방지를 위해 transition 상태가 확인 후,
// 현재 애니메이션이 진행되고 있지 않다면 motion transition 수행
if (dy < 0
&& layoutManager.findFirstCompletelyVisibleItemPosition() == 0
&& binding.motionLayout.currentState == R.id.end
&& (binding.motionLayout.progress >= 1f
|| binding.motionLayout.progress <= 0f)
) {
binding.motionLayout.transitionToStart()
}
// 스크롤을 아래로 내렸을 경우, 버벅거림 방지를 위해 transition 상태 확인 후,
// 현재 애니메이션이 진행되고 있지 않다면 motion transition 수행
if (dy > 0
&& binding.motionLayout.currentState == R.id.start
&& (binding.motionLayout.progress >= 1f
|| binding.motionLayout.progress <= 0f)
) {
binding.motionLayout.transitionToEnd()
}
}
})
}