Android MotionLayout (RecyclerView 스크롤 시 등장 애니메이션)

pass·2023년 9월 11일
0

Android

목록 보기
33/36
post-thumbnail

🔥 Android 에서 애니메이션을 위해 사용하는 MotionLayout 에 대해 알아보자.


Android 에서 애니메이션 효과를 주기 위해서 사용하는 방법에는 여러가지가 있다.
이번에는 그 중에서 MotionLayout 에 대해 알아보고 사용한 예제까지 살펴보려고 한다.

MotionLayout 은 ConstraintLayout 을 상속받은 ViewGroup 이다.
따라서 ConstraintLayout 의 다양한 레이아웃 기능을 기초로 한다.
실시간 상호작용하는 애니메이션 처리를 할 때, 효과적이며 UI 의 요소 이동 및 크기 조절 등을 쉽게 설정할 수 있다.

자세한 사항은 공식 문서를 살펴보자.
공식 문서 : https://developer.android.com/training/constraint-layout/motionlayout?hl=ko


📑 사용

MotionLayout 은 실제로 사용자가 View 를 잡아끄는 등의 모션을 취했을 때, 바로 적용할 수 있다는 장점이 있다.
하지만, 이번 예제에서는 MotionLayout 을 사용하는 기본적인 방법에 중심을 맞추었기 때문에 애니메이션 시작과 끝을 직접 정의해주었다.
아래는 세로 방향 RecyclerView 스크롤을 감지하여 애니메이션 동작을 추가한 예시이다.

✓ 동작 순서

  1. 야채 Vegetables ... 을 포함한 세로 방향 RecyclerView 를 스크롤
  2. 전체 / 야채 / 과일 / ... 을 포함한 가로 방향 RecyclerView 와 '현재 있는...' 이 포함된 문구 사라짐
  3. '추천 받기' 버튼 등장



✓ layout 에 MotionLayout 추가

		<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>
  • 애니메이션 기능에 포함할 요소들을 MotionLayout 내부에 선언해준다.
  • 이후 layout 의 초기 상태를 정의해준다.
  • layoutDescription 에 아래에서 설명하는 MotionScene 을 정의한 파일을 넣어준다.



✓ xml 에 MotionLayout 관련 MotionScene 정의

<?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>
  • Transition/KeyFrameSet 부분에 KeyAttribute 정의
    • 위 예제에서는 50% 와 100% 진행되었을 때, 투명도를 조절
    • classificationRecyclerView 는 나타나도록 정의, selectButton 는 사라지도록 정의
  • ConstraintSet 부분에 Constraint 정의
    • 각 id 에 따라 start 상태와 end 상태를 의미
    • ConstraintSet android:id="@+id/end" 일 경우 애니메이션이 끝났을 경우를 의미
    • start 일 경우 noticeTextView 와 RecyclerView 는 정상적으로 보이고, selectButton 은 가로와 세로 길이가 0.1dp 로 설정
    • end 상태일 경우 반대로 selectButton 만 보이도록 설정



✓ 애니메이션 동작 수행 (Activity 혹은 Fragment)

		// 위 예제는 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()
                    }
                }
            })
        }
  • 스크롤 이벤트 특성 상 여러번 호출될 수 있기 때문에 조건을 설계하여 trainsition event 수행



🔥 결과

profile
안드로이드 개발자 지망생

0개의 댓글