MotionLayout 사용해서 애니메이션 효과 주기

LeeEunJae·2022년 9월 8일
1

Study Kotlin

목록 보기
13/20

📌 결과화면

MotionLayout에 대한 내용은 이전에 게시물을 올린적이 있지만, 다시해봐도 새로운 내용이기 때문에... ㅋㅋ 복습하는 차원에서 정리해봤습니다.

📌 activity_main.xml

motionLayout에 motion 효과를 주기 위해서 app:layoutDescription 속성에 어떤 모션효과를 줄 것인지 정의 되어있는 xml 파일을 지정해줘야합니다.
우선 밑의 코드를 activity_main 에 복붙하고 효과를 주는 부분은 밑에서 알아보도록 하겠습니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    app:layoutDescription="@xml/button_shown_scene"
    android:id="@+id/buttonShownMotionLayout">
    <ScrollView
        android:id="@+id/scrollView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent">
        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <View
                android:layout_width="match_parent"
                android:layout_height="600dp"
                android:background="@color/black"/>

            <androidx.constraintlayout.motion.widget.MotionLayout
                android:id="@+id/gatheringDigitalThingsLayout"
                android:layout_width="480dp"
                android:layout_height="300dp"
                android:layout_gravity="center_horizontal"
                app:layoutDescription="@xml/gathering_digital_things_scene">

                <ImageView
                    android:id="@+id/tvImageView"
                    android:layout_width="400dp"
                    android:layout_height="250dp"
                    android:scaleType="centerCrop"
                    android:src="@drawable/tv"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />

                <ImageView
                    android:id="@+id/tabletImageView"
                    android:layout_width="200dp"
                    android:layout_height="100dp"
                    android:src="@drawable/tablet"
                    android:scaleType="centerCrop"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    app:layout_constraintBottom_toBottomOf="parent"/>
                <ImageView
                    android:id="@+id/laptopImageView"
                    android:layout_width="200dp"
                    android:layout_height="150dp"
                    android:src="@drawable/laptop"
                    android:scaleType="centerCrop"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    app:layout_constraintBottom_toBottomOf="parent"/>
                <ImageView
                    android:id="@+id/phoneImageView"
                    android:layout_width="100dp"
                    android:layout_height="130dp"
                    android:src="@drawable/phone"
                    android:scaleType="centerCrop"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    app:layout_constraintBottom_toBottomOf="parent"/>

            </androidx.constraintlayout.motion.widget.MotionLayout>

        </androidx.appcompat.widget.LinearLayoutCompat>

    </ScrollView>

    <Button
        android:id="@+id/button"
        android:layout_width="0dp"
        android:layout_height="64dp"
        android:text="2주 무료 이용"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginStart="24dp"
        android:layout_marginEnd="24dp"/>

</androidx.constraintlayout.motion.widget.MotionLayout>

📌 gathering_digital_things_scene.xml

우선 버튼을 제외한 4개의 imageView 에 대한 모션을 정의한 xml 파일입니다.
res/xml 경로에 xml 파일을 하나 생성하고 밑의 코드를 작성합니다.
자세한 코드 설명은 밑에서 하겠습니다.

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="500">
        <KeyFrameSet>
            <KeyAttribute
                android:scaleX="0.9"
                android:scaleY="0.9"
                app:framePosition="0"
                app:motionTarget="@+id/tvImageView"
                app:transitionEasing="decelerate"
                android:alpha="0"/>
            <KeyAttribute
                android:scaleX="1"
                android:scaleY="1"
                app:framePosition="100"
                app:motionTarget="@+id/tvImageView"
                app:transitionEasing="decelerate"
                android:alpha="1"/>
        </KeyFrameSet>
        <KeyFrameSet>
            <KeyAttribute
                android:scaleX="0.8"
                android:scaleY="0.8"
                app:framePosition="0"
                app:motionTarget="@+id/tabletImageView"
                app:transitionEasing="decelerate"
                android:alpha="0"/>
            <KeyAttribute
                android:scaleX="1"
                android:scaleY="1"
                app:framePosition="100"
                app:motionTarget="@+id/tabletImageView"
                app:transitionEasing="decelerate"
                android:alpha="1"/>
        </KeyFrameSet>
        <KeyFrameSet>
            <KeyAttribute
                android:scaleX="0.8"
                android:scaleY="0.8"
                app:framePosition="0"
                app:motionTarget="@+id/laptopImageView"
                app:transitionEasing="decelerate"
                android:alpha="0"/>
            <KeyAttribute
                android:scaleX="1"
                android:scaleY="1"
                app:framePosition="100"
                app:motionTarget="@+id/laptopImageView"
                app:transitionEasing="decelerate"
                android:alpha="1"/>
        </KeyFrameSet>
        <KeyFrameSet>
            <KeyAttribute
                android:scaleX="0.8"
                android:scaleY="0.8"
                app:framePosition="0"
                app:motionTarget="@+id/phoneImageView"
                app:transitionEasing="decelerate"
                android:alpha="0"/>
            <KeyAttribute
                android:scaleX="1"
                android:scaleY="1"
                app:framePosition="100"
                app:motionTarget="@+id/phoneImageView"
                app:transitionEasing="decelerate"
                android:alpha="1"/>
        </KeyFrameSet>

    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/tvImageView"
            android:layout_width="400dp"
            android:layout_height="250dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.9"/>
        <Constraint
            android:id="@+id/tabletImageView"
            android:layout_width="200dp"
            android:layout_height="100dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="0"
            app:layout_constraintVertical_bias="0.8"/>
        <Constraint
            android:id="@+id/laptopImageView"
            android:layout_width="250dp"
            android:layout_height="150dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="1"
            app:layout_constraintVertical_bias="0.8"/>
        <Constraint
            android:id="@+id/phoneImageView"
            android:layout_width="100dp"
            android:layout_height="130dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="1"
            app:layout_constraintVertical_bias="0.8"/>

    </ConstraintSet>
    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/tvImageView"
            android:layout_width="400dp"
            android:layout_height="250dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>
        <Constraint
            android:id="@+id/tabletImageView"
            android:layout_width="200dp"
            android:layout_height="100dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="0.1"
            app:layout_constraintVertical_bias="0.75"/>
        <Constraint
            android:id="@+id/laptopImageView"
            android:layout_width="250dp"
            android:layout_height="150dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="0.8"
            app:layout_constraintVertical_bias="0.75"/>
        <Constraint
            android:id="@+id/phoneImageView"
            android:layout_width="100dp"
            android:layout_height="130dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="0.9"
            app:layout_constraintVertical_bias="0.7"/>
    </ConstraintSet>
</MotionScene>

Transition, KeyFrameSet, KeyAttribute

편의를 위해 tvImageView 의 모션만 가지고 설명하겠습니다.

 <Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="500">
        <KeyFrameSet>
            <KeyAttribute
                android:scaleX="0.9"
                android:scaleY="0.9"
                app:framePosition="0"
                app:motionTarget="@+id/tvImageView"
                app:transitionEasing="decelerate"
                android:alpha="0"/>
            <KeyAttribute
                android:scaleX="1"
                android:scaleY="1"
                app:framePosition="100"
                app:motionTarget="@+id/tvImageView"
                app:transitionEasing="decelerate"
                android:alpha="1"/>
        </KeyFrameSet>
 </Transition>

Transition 으로 모션의 시작 및 종료상태, 원하는 중간상태를 지정하고, KeyFrameSet 안에 있는 KeyAttribute 로 모션이 동작하는 과정에서 뷰 속성의 변화를 지정합니다.
위의 코드에서는 모션 타겟을 tvImageView로 두고, 속성값을 지정해줬습니다.

ConstraintSet, Constraint

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/tabletImageView"
            android:layout_width="200dp"
            android:layout_height="100dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="0"
            app:layout_constraintVertical_bias="0.8"/>
        

    </ConstraintSet>
    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/tabletImageView"
            android:layout_width="200dp"
            android:layout_height="100dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="0.1"
            app:layout_constraintVertical_bias="0.75"/>
        
    </ConstraintSet>

Constraint 는 모션 요소의 위치를 지정해줍니다.
start 상태일 때, app:layout_constraintHorizontal_bias="0" 였던 것이
end 상태일 때, app:layout_constraintHorizontal_bias="0.1" 변화되어 start -> end 로 모션이 변화하면서 왼쪽에서 살짝 튀어나오는 듯한 효과가 발생합니다.
app:layout_constraintVertical_bias 속성도 같은 맥락입니다.

📌 button_shown_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@+id/start"
        app:duration="500">
        <KeyFrameSet>
            <KeyAttribute
                app:framePosition="0"
                app:motionTarget="@+id/button"
                app:transitionEasing="decelerate"
                android:alpha="0"/>
            <KeyAttribute
                app:framePosition="100"
                app:motionTarget="@+id/button"
                app:transitionEasing="decelerate"
                android:alpha="1"/>
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@+id/button"
            android:layout_width="0dp"
            android:layout_height="64dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="1.4"
            android:layout_marginStart="24dp"
            android:layout_marginEnd="24dp"/>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@+id/button"
            android:layout_width="0dp"
            android:layout_height="64dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.97"
            android:layout_marginStart="24dp"
            android:layout_marginEnd="24dp"/>
    </ConstraintSet>

</MotionScene>

start 상태일 때, app:layout_constraintVertical_bias="1.4" (화면 밑 바깥쪽)
end 상태일 때, app:layout_constraintVertical_bias="0.97"(화면 밑에서 살짝 위)
화면 밑에서 튀어나오는 듯한 효과가 발생합니다.

📌 MainActivity.kt

위에서 모션 레이아웃에 어떤 모션효과를 줄 것인지 설정을 했고, 코드를 통해 모션효과를 실제로 적용하는 과정이 필요합니다.
그리고, 어느 시점에 start -> end 모션을 주고, end -> start 모션을 주는지 알려줘야합니다.


import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.TypedValue
import androidx.constraintlayout.motion.widget.MotionLayout
import com.dldmswo1209.ott.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    val binding by lazy{ ActivityMainBinding.inflate(layoutInflater) }
    private var isGatheringMotionAnimating = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        // 현재 scrollView 의 y축 값을 가져오기 위해서 viewTreeObserver 를 사용해야 한다.
        // viewTreeObserver 에 리스너를 붙여줘서 스크롤 변화를 감지한다.
        binding.scrollView.viewTreeObserver.addOnScrollChangedListener {
            if(binding.scrollView.scrollY > 150f.dpToPx(this).toInt()){
                // 150px 만큼 스크롤 되면
                if(!isGatheringMotionAnimating){
                    binding.gatheringDigitalThingsLayout.transitionToEnd() // 애니메이션 동작 start->end
                    binding.buttonShownMotionLayout.transitionToEnd()
                }
            }else{ // 150px 만큼 스크롤 되지 않은 경우
                if(!isGatheringMotionAnimating){
                    binding.gatheringDigitalThingsLayout.transitionToStart() // 애니메이션 동작 end->start
                    binding.buttonShownMotionLayout.transitionToStart()
                }
            }
        }

        // 모션 레이아웃에 setTransitionListener 를 등록해서 transition 상태를 감지한다.
        // start 상태이면 flag 변수를 true , end 상태이면 false
        binding.gatheringDigitalThingsLayout.setTransitionListener(object: MotionLayout.TransitionListener{
            override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
                isGatheringMotionAnimating = true
            }

            override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {

            }

            override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
                isGatheringMotionAnimating = false
            }

            override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {}

        })

    }
    // dp -> px 함수
    fun Float.dpToPx(context: Context): Float =
        TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, context.resources.displayMetrics)
}

코드에 대한 설명은 주석을 열심히 달아놓았으니,, 생략하겠습니다.

출처 및 참고자료

해당 프로젝트는 FastCampus의 30개 프로젝트로 배우는 Android 앱 개발 with Kotlin 초격차 패키지 Online. 을 수강하면서 만든 프로젝트 입니다.

https://developer.android.com/training/constraint-layout/motionlayout?hl=ko

profile
매일 조금씩이라도 성장하자

0개의 댓글