24.02.09 MotionLayout 복수 트랜지션으로 인트로 화면 만들기

KSang·2024년 2월 12일
1

TIL

목록 보기
55/101
post-thumbnail

동영상 앱을 만드는데, 처음에 인기동영상과 그 밑에 키워드가 달린 동영상을 보여주는데

데이터를 갱신하는 과정에서 앱 최초 실행시 캐시데이터가 없고 Room생성하는등 시간이 조금 걸려서 그 동안 화면을 가려줄 Splash Screen이 있으면 좋겠다 싶어서 만들었다.

유튜브 화면을 참조해서 만들었다.

현재 앱 대표 아이콘으로 사용중인 이미지다

여기서 저 재생버튼 주변의 pin을 돌려 재생 버튼 처럼 만든뒤

로딩 바를 길게 늘리고 게이지를 채워 로딩하는 식으로 만들 생각이다

. |> ======= 이런 모양으로 만들 생각이다


Layout

우선 벡터이미지에서 play 모양 아이콘과 주변의 pin모양 아이콘을 분리해야하는데

피그마를 이용해서 분리 된 벡터 파일을 각각 받을 수 있다,

<ImageView
        android:id="@+id/iv_splash_play"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:src="@drawable/ic_splash_play"
        app:layout_constraintWidth_percent="0.13"
        app:layout_constraintHeight_percent="0.15"
        app:layout_constraintVertical_bias="0.3"
        app:layout_constraintHorizontal_bias="0.54"
        app:layout_constraintTop_toTopOf="@+id/iv_splash_pin"
        app:layout_constraintStart_toStartOf="@+id/iv_splash_pin"
        app:layout_constraintEnd_toEndOf="@+id/iv_splash_pin"
        app:layout_constraintBottom_toBottomOf="@+id/iv_splash_pin"
        app:tint="@color/main_color"/>
    <ImageView
        android:id="@+id/iv_splash_pin"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:src="@drawable/ic_splash_pin"
        app:layout_constraintWidth_percent="0.4"
        app:layout_constraintHeight_percent="0.4"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:tint="@color/main_color"/>

pin 모양 안에 play모양 아이콘이 들어가야하니 constraint를 전부 pin에 맞춰 설정해주고
bias로 위치를 지정해놨다

안드로이드는 기본적으론 처음에 뷰가 생성되지 않을때 아이콘이 확대되면서 화면을 채우는데

거기서 애니메이션까지 이어갈거니 위치를 전부 parent에 맞춰주고 적당히 크기를 조절해준다.

그리고 난뒤 게이지를 채워줄 로딩 바를 만들어줄건데

<ImageView
        android:id="@+id/iv_splash_status"
        android:layout_width="0dp"
        android:layout_height="14dp"
        android:src="@drawable/ic_status_bar"
        android:scaleType="fitXY"
        android:visibility="gone"
        app:layout_constraintWidth_percent="0.55"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <ImageView
        android:id="@+id/iv_splash_status_loading"
        android:layout_width="0dp"
        android:layout_height="14dp"
        android:src="@drawable/ic_status_bar"
        android:scaleType="fitXY"
        android:visibility="gone"
        app:layout_constraintWidth_percent="0.55"
        app:layout_constraintTop_toTopOf="@+id/iv_splash_status"
        app:layout_constraintStart_toStartOf="@+id/iv_splash_status"
        app:layout_constraintBottom_toBottomOf="@+id/iv_splash_status"
        app:tint="@color/main_color" />

투명도를 gone으로 해주고 중앙에 위치하게, 그리고 로딩 바를 채워줄 바는 로딩바를 end빼고 전부 제약조건을 설정해 위치를 맞춰 준다.

이제 뷰들을 모션 레이아웃으로 감싼다.

이제 애니메이션 노가다 시간인데, 처음엔 키프레임으로 시간별로 나눠서 뷰의 위치들을 바꾸려 했으나
굳이 그럴 필요없이 트랜지션을 여러개로 나눠서 트랜지션이 끝나면 다음 트랜지션으로 바꾸고
애니메이션을 이어가면 시간, 위치 등 계산할 필요없이 구현하기 편할 것 같아 각 애니메이션 별로 나눠서 트랜지션을 구현하기로 했다.


일단 핀을 옆으로 돌리고 크기를 줄이는 애니메이션

로딩바를 만드는 애니메이션

로딩 바를 채워주는 애니메이션으로 나눠서 구현했다.

MotionScence.xml

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@+id/iv_splash_play"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:src="@drawable/ic_splash_play"
            app:layout_constraintWidth_percent="0.13"
            app:layout_constraintHeight_percent="0.15"
            app:layout_constraintVertical_bias="0.3"
            app:layout_constraintHorizontal_bias="0.54"
            app:layout_constraintTop_toTopOf="@+id/iv_splash_pin"
            app:layout_constraintStart_toStartOf="@+id/iv_splash_pin"
            app:layout_constraintEnd_toEndOf="@+id/iv_splash_pin"
            app:layout_constraintBottom_toBottomOf="@+id/iv_splash_pin"
            app:tint="@color/main_color" />
        <Constraint
            android:id="@+id/iv_splash_pin"
            app:layout_constraintWidth_percent="0.4"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHeight_percent="0.4"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@+id/iv_splash_play"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:src="@drawable/ic_splash_play"
            app:layout_constraintWidth_percent="0.03"
            app:layout_constraintHeight_percent="0.05"
            app:layout_constraintVertical_bias="0.5"
            app:layout_constraintHorizontal_bias="0.2"
            app:layout_constraintTop_toTopOf="@+id/iv_splash_pin"
            app:layout_constraintStart_toStartOf="@+id/iv_splash_pin"
            app:layout_constraintEnd_toEndOf="@+id/iv_splash_pin"
            app:layout_constraintBottom_toBottomOf="@+id/iv_splash_pin"
            app:tint="@color/main_color" />
        <Constraint
            android:id="@+id/iv_splash_pin"
            app:layout_constraintWidth_percent="0.1"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:rotation="-90"
            app:layout_constraintHorizontal_bias="0.4"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHeight_percent="0.1"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
        <Constraint
            android:id="@+id/iv_splash_status"
            app:layout_constraintWidth_percent="0.55"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="0.01dp"
            android:alpha="0.01"
            android:layout_height="14dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
        <Constraint
            android:id="@+id/iv_splash_status_loading"
            app:layout_constraintWidth_percent="0.55"
            android:layout_width="0dp"
            android:layout_height="14dp"
            app:layout_constraintBottom_toBottomOf="@+id/iv_splash_status"
            android:visibility="gone"
            app:layout_constraintTop_toTopOf="@+id/iv_splash_status"
            app:layout_constraintStart_toStartOf="@+id/iv_splash_status" />
    </ConstraintSet>

    <Transition
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@+id/start"
        app:duration="600">

    </Transition>
    <ConstraintSet android:id="@+id/statusbar" >
        <Constraint android:id="@+id/iv_splash_play"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:src="@drawable/ic_splash_play"
            app:layout_constraintWidth_percent="0.03"
            app:layout_constraintHeight_percent="0.05"
            app:layout_constraintVertical_bias="0.5"
            app:layout_constraintHorizontal_bias="0.2"
            app:layout_constraintTop_toTopOf="@+id/iv_splash_pin"
            app:layout_constraintStart_toStartOf="@+id/iv_splash_pin"
            app:layout_constraintEnd_toEndOf="@+id/iv_splash_pin"
            app:layout_constraintBottom_toBottomOf="@+id/iv_splash_pin"/>
        <Constraint
            android:id="@+id/iv_splash_pin"
            app:layout_constraintWidth_percent="0.1"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:rotation="-90"
            app:layout_constraintHorizontal_bias="0.2"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHeight_percent="0.1"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
        <Constraint
            android:id="@+id/iv_splash_status"
            app:layout_constraintWidth_percent="0.45"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.6"
            android:layout_width="0dp"
            android:layout_height="14dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
        <Constraint
            android:id="@+id/iv_splash_status_loading"
            app:layout_constraintWidth_percent="0.01"
            android:layout_width="0dp"
            android:layout_height="14dp"
            app:layout_constraintBottom_toBottomOf="@+id/iv_splash_status"
            app:layout_constraintTop_toTopOf="@+id/iv_splash_status"
            app:layout_constraintStart_toStartOf="@+id/iv_splash_status" />
    </ConstraintSet>

    <Transition
        app:constraintSetEnd="@id/statusbar"
        app:constraintSetStart="@+id/end"
        app:duration="600"/>
    <ConstraintSet android:id="@+id/loading" >
        <Constraint android:id="@+id/iv_splash_play"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:src="@drawable/ic_splash_play"
            app:layout_constraintWidth_percent="0.03"
            app:layout_constraintHeight_percent="0.05"
            app:layout_constraintVertical_bias="0.5"
            app:layout_constraintHorizontal_bias="0.2"
            app:layout_constraintTop_toTopOf="@+id/iv_splash_pin"
            app:layout_constraintStart_toStartOf="@+id/iv_splash_pin"
            app:layout_constraintEnd_toEndOf="@+id/iv_splash_pin"
            app:layout_constraintBottom_toBottomOf="@+id/iv_splash_pin"/>
        <Constraint
            android:id="@+id/iv_splash_pin"
            app:layout_constraintWidth_percent="0.1"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:rotation="-90"
            app:layout_constraintHorizontal_bias="0.2"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHeight_percent="0.1"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
        <Constraint
            android:id="@+id/iv_splash_status"
            app:layout_constraintWidth_percent="0.45"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.6"
            android:layout_width="0dp"
            android:layout_height="14dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
        <Constraint
            android:id="@+id/iv_splash_status_loading"
            app:layout_constraintWidth_percent="1"
            android:layout_width="0dp"
            android:layout_height="14dp"
            app:layout_constraintEnd_toEndOf="@+id/iv_splash_status"
            app:layout_constraintBottom_toBottomOf="@+id/iv_splash_status"
            app:layout_constraintTop_toTopOf="@+id/iv_splash_status"
            app:layout_constraintStart_toStartOf="@+id/iv_splash_status" />
    </ConstraintSet>
    <Transition
        app:constraintSetStart="@+id/statusbar"
        app:constraintSetEnd="@+id/loading"
        app:duration="700"/>
</MotionScene>

트랜지션을 4개를 만들었고 순서대로 start -> end -> status -> loading 순이다

이제 이걸 액티비티에서 자연스럽게 연결해보자

Splash.kt

모션 레이아웃에선 상태변화를 감지하는 리스너가 있는데 이를 활용해서 트랜지션 전환을 자연스럽게 하게 했다.

    private fun setMotion() = with(binding.mlSplashLayout) {
        setTransitionListener(object : MotionLayout.TransitionListener {
            override fun onTransitionChange(motionLayout: MotionLayout, startId: Int, endId: Int, progress: Float) {}
            override fun onTransitionStarted(motionLayout: MotionLayout, startId: Int, endId: Int) {}
            override fun onTransitionTrigger(motionLayout: MotionLayout, triggerId: Int, positive: Boolean, progress: Float) {}
            override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
                when (currentId) {
                    R.id.end -> setTransitionAction(R.id.end, R.id.statusbar, 600)
                    R.id.statusbar -> setTransitionAction(R.id.statusbar, R.id.loading, 500)
                    R.id.loading -> navigateToMain()
                }
            }
        })
        setTransitionAction(R.id.start, R.id.end, 600)
    }

트랜지션 리스너를 설정하고 오버라이드해서 각각의 트랜지션이 Completed될때 감지해서

다음 애니메이션을 매끄럽게 실행 시키고

마지막엔 Main액티비티를 실행 시키고 꺼지는 로직을 구현했다.

리스너를 설정할땐 주의할게 있는데 onCreate될때 리스너를 설정하게 되면 애니메이션이 실행되는걸 리스너가 감지하지 못하는데

모션 레이아웃이 완전히 로드되고 초기화 되기전에 리스너가 설정되서 그런다고 한다.

뷰의 레이아웃 배치가 완료되기도전에 실행하게되니 제대로 감지하지못해 애니메이션이 실행되지 못한다

그렇기 때문에 onResume 때 실행하거나 모션 레이아웃뒤에 .post를 붙여 뷰의 레이아웃이 완료되고 나서 작업이 실행하게 하면 해결 할 수 있다.

포스트를 작성하고 나니 로딩바가 너무 굵고 아이콘이 가만히 있는게 너무 어색해보여서 애니메이션을 조금 더 수정 해봤다.

0개의 댓글