Android 공부/Android UI

Android Motionlayout 삽질기

0. 서론


모션레이아웃은 앱을 상당히 유연하게 하는 매력적인 기능이라고 생각합니다. 드래그나 클릭에 의해서 ConstraintLayout의 ConstraintSet, TransitionManager등과 같은 기능들을 잘 녹였다고 생각합니다. 물론, 구현을 하기까지 어려움을 많이 겪을 수 있겠지만, 어느 순간 자신의 기술이 되어있을 때, UI를 제공해주는 큰 무기를 얻을 수 있다고 생각합니다.




1. 목표


 Lottie와 MotionLayout을 함께 결합해 애니메이션을 구현하면 최고의 효율을 낼 수 있다는 것을 목표로 프로젝트가 진행되겠습니다. 저는 프로젝트내에서 사용자들의 주목을 받기 위해서 MotionLayout을 사용하고 있습니다.


2. MotionLayout 구조


 MotionLayout은 ConstraintLayout 라이브러리 2.0에서 모션과 애니메이션을 관리하기 위해서 사용하는 클래스라고 합니다. 그래서 ConstraintLayout을 기반으로 하고 있어서 ConstraintLayout을 통해 애니메이션을 만드는 개념을 알고 있다면 더 원활하게 해당 개념을 이해할 수 있다고 생각합니다. 프로젝트에서는 이와 같은 구조를 사용하고 있습니다.


 

 이와 같이 ConstraintLayout은 일반적인 Layout 구조와 함께 MotionLayout에서 움직이는 Layout의 시작 구조와 마지막 구조 혹은 중간 구조, 마지막 구조를 만들어서 애니메이션을 만들어낼 수 있습니다. 다시 한번 정리해서 말하자면, Motion이 만들어지기 위한 Layout들을 만들어야하는 것을 의미합니다.

 - Layout의 전체적인 구조: cardview_update_content.xml

 - Layout의 시작 구조: motion_rocket_animation_start.xml

 - Layout의 마지막 구조: motion_rocket_animation_end.xml


3. 코드


3.1. cardview_update_content..xml


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.motion.MotionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/ml_rocket"
    app:layoutDescription="@xml/scene_rocket_in_content">
 
    <TextView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:id="@+id/tv_update_top_title"
        android:gravity="center"
        android:text="@string/congratulation_to_update"
        android:textSize="16sp"
        app:layout_constraintBottom_toTopOf="@+id/guideline_lottie_view"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/lottieAnimationView"
        android:layout_width="128dp"
        android:layout_height="128dp"
        app:layout_constraintBottom_toTopOf="@id/guideline_rocket_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_percent="0.3"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:lottie_autoPlay="true"
        app:lottie_fileName="rocket.json"
        app:lottie_loop="true" />
 
    <android.support.constraint.Guideline
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/guideline_rocket_content"
        app:layout_constraintGuide_percent="0.5"
        android:orientation="horizontal"/>
    <android.support.constraint.Guideline
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/guideline_lottie_view"
        app:layout_constraintGuide_percent="0.15"
        android:orientation="horizontal"/>
    <android.support.constraint.Guideline
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/guideline_text_content"
        app:layout_constraintGuide_percent="0.5"
        android:orientation="horizontal"/>
    <TextView
        android:id="@+id/tv_update_content1"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="8dp"
        android:gravity="center_horizontal"
        android:text="@string/update_content"
        android:textSize="14sp"
        app:layout_constraintBottom_toTopOf="@id/tv_update_content2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/guideline_text_content" />
 
    <TextView
        android:id="@+id/tv_update_content2"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:gravity="center_horizontal"
        android:text="@string/menu_change_app_settings_first_timetable_time"
        android:textSize="12sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@id/button_ok"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_update_content1" />
 
    <Button
        android:id="@+id/button_ok"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="@color/splash_activity_background"
        android:text="@string/all_text_confirm"
        android:textColor="@color/white_color"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.motion.MotionLayout>
cs


 

 모션레이아웃의 구조는 이와 같이 ConstraintLayout을 통해 만들어주는 것과 같은 구조로 레이아웃을 만들어줍니다. 해당 레이아웃은 LottieAnimationView를 중심으로 모션을 넣어줬습니다. 여기서 LottieAnimationView은 View의 메모리를 합리적으로 사용해 애니메이션을 제공해주는 것을 의미하고, 해당 파일은 json으로 해서 적은 용량과 비교적 적은 메모리로 애니메이션을 구현하도록 도와줍니다.

 모션레이아웃에 scene을 등록시켜줘야 하는데, 여기에서 scene은 애니메이션이 움직이는 내용이 담겨져있습니다. 이것은 MotionLayout의 속성에서 app:layoutDescripton에 등록시켜줘야합니다.


자세한 내용은 https://airbnb.design/lottie/ 확인해보시면 쉽게 구현하실 수 있을 것이라고 생각합니다.


3.2. xml/scene_rocket_in_content.xml


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <Transition
        motion:constraintSetStart="@layout/motion_rocket_animation_start"
        motion:constraintSetEnd="@layout/motion_rocket_animation_end"
        motion:duration="3000">
        <OnClick motion:target="@id/button_ok"
            motion:clickAction="toggle"/>
 
        <KeyFrameSet>
            <KeyPosition
                motion:framePosition="50"
                motion:keyPositionType="parentRelative"
                motion:percentY="0.40"
                motion:target="@+id/lottieAnimationView" />
            <KeyAttribute
                android:scaleX="2"
                android:scaleY="2"
                motion:framePosition="50"
                motion:target="@id/lottieAnimationView" />
 
            <KeyAttribute
                android:rotationY="-50"
                motion:framePosition="40"
                motion:target="@id/lottieAnimationView" />
 
            <KeyAttribute
                android:rotationY="50"
                motion:framePosition="60"
                motion:target="@id/lottieAnimationView" />
        </KeyFrameSet>
    </Transition>
</MotionScene>
cs

Scene MotionLayout의 움직임을 관리하는 코드입니다.
MotionScene : Motion에 대한 Scene을 작성하는 코드입니다.
Transition :
 - constraintStart : MotionLayout의 시작 레이아웃을 등록시켜줍니다.
 - constraintEnd : MotionLayout의 마지막 레이아웃을 등록시켜줍니다.
 - duration : MotionLayout의 Motion에 대한 시간을 등록시켜줍니다. 이것은 보통 onClick시에 작용합니다.
 - interpolator : MotionLayout의 Animation에 대해 bounce, easeOut, easeIn 등으로 애니메이션에 대한 효과를 적용할 수 있습니다.
 - 등등이 있습니다.
 해당 Transition은 start와 end 레이아웃만의 속성 값을 통해서 연속적인 애니메이션을 구현해줍니다. 이것은 duration값을 통해서 시간을 조절해주고, interpolator를 통해서 animation 효과를 넣을 수 있습니다.
프로젝트에서 사용되어진 Transition : motion_rocket_animation_start.xml  motion_rocket_animation_end.xml 에서는 Guideline의 위치값만 변하는 레이아웃입니다. 이것은 height의 위치가 전자 레이아웃에서는 500을 갖고, 후자 레이아웃에서 0을 갖고 있으면 500->0으로 이동하는 모션 애니메이션을 제공해줍니다. 

OnClick
- target : MotionLayout 내부에서 반응할 view를 등록
- clickAction : toggle, jumpToStart, jumpToEnd, transitionStrat, transitionEnd이 있습니다. 보통 저는 toggle을 사용하는데, toggle은 클릭시 start->end, end->start로 바꿔줍니다. 그 외에도 jumpTo는 애니메이션을 제외하고 start layout으로 모션을 변경시키고, transtionStart는 애니메이션을  포함해서 start layout으로 모션을 변경시킵니다.
 해당 OnClick은 클릭시에 일어나는 Action을 지정시켜줍니다.

OnSwipe:
- touchAnchorId : swipe할 타겟입니다.
- touchAnchorSide : top, bottom, left, right의 속성이 있으며 드래그 할 시에 해당 레이아웃에 드래그 하는 위치입니다. 이것은 조금더 상세하게 말하자면, LottieView가 있다면 해당 뷰의 상단을 중심으로 드래그를 하겠다는 것입니다.
- dragDirection : dragDown, dragUp, dragLeft, dragRight의 속성이 있으며 어떤 방향으로 드래그를 하겠느냐를 물어보는 속성값입니다.
 해당 OnSwipe는 재미있는 기능으로 레이아웃을 드래그할 수 있는 모션을 만드는 것입니다. 반응할 레이아웃을 정해주고, 해당 레이아웃 내부 뷰에서 드래그가 지정되어지는 위치를 정하고, 드래그의 방향 등을 정하는 속성 값입니다.

KeyFrameSet :
- KeyPosition, KeyAttribute, KeyCycle : 특정 시점에 애니메이션을 만들기 위해서 사용하는 속성들입니다.

3.3. motion_rocket_animation_start.xml


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/lottieAnimationView"
        android:layout_width="128dp"
        android:layout_height="128dp"
        app:layout_constraintBottom_toTopOf="@id/guideline_rocket_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_percent="0.3"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:lottie_autoPlay="true"
        app:lottie_fileName="rocket.json"
        app:lottie_loop="true" />
 
    <android.support.constraint.Guideline
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/guideline_rocket_content"
        app:layout_constraintGuide_percent="0.5"
        android:orientation="horizontal"/>
</android.support.constraint.ConstraintLayout>
cs


MotionLayout의 시작 Layout으로 ConstraintLayout으로 구성시켜줘야 합니다. 해당 코드는 scene에서 constraintStart에 들어가는 layout입니다. 해당 뷰에서는 시작시에 어떻게 레이아웃을 구성하겠냐는 정적인 뷰입니다. constraintStart뷰와 constraintEnd뷰를 활용해 정적인 뷰 2개를 활용해서 애니메이션이 만들어집니다.


3.4. motion_rocket_animation_end.xml


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/lottieAnimationView"
        android:layout_width="128dp"
        android:layout_height="128dp"
        app:layout_constraintBottom_toTopOf="@id/guideline_rocket_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_percent="0.3"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:lottie_autoPlay="true"
        app:lottie_fileName="rocket.json"
        app:lottie_loop="true" />
 
    <android.support.constraint.Guideline
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/guideline_rocket_content"
        app:layout_constraintGuide_percent="0.0"
        android:layout_marginBottom="64dp"
        android:orientation="horizontal"/>
 
</android.support.constraint.ConstraintLayout>
cs



MotionLayout의 마지막 Layout으로 ConstraintLayout으로 구성시켜줘야 합니다. 해당 코드는 scene에서 constraintEnd에 들어가는 layout입니다. 해당 뷰에서는 시작시에 어떻게 레이아웃을 구성하겠냐는 정적인 뷰입니다. constraintStart뷰와 constraintEnd뷰를 활용해 정적인 뷰 2개를 활용해서 애니메이션이 만들어집니다.


4. 사용 후기


 MotionLayout은 이전 '서울살이'프로젝트를 하면서 가장 삽질을 했던 라이브러리입니다. 해당 라이브러리를 사용하게 되면 앱의 각종 모션과 애니메이션을 만들어줄 수 있어서 이전과 다르게 굉장히 유연한 뷰를 만들 수 있게 됩니다.

 많은 삽질 끝에 MotionLayout을 사용해야할 때와 사용하지 말아야할 때를 알 수 있게 되었습니다.

- MotionLayout을 사용할 때: 중복적이지 않은 View에 각종 애니메이션을 넣고 싶을 때, example) FloattingActionButton을 클릭시에 움직이게 하고 싶을 때.

- MotionLayout을 사용하지 말아야할 때 : RecyclerView 내부에 MotionLayout을 사용할 때, Swipe가 작용되는 MotionLayout에 RecyclerView를 사용해야할 때, 이미 구현되어진 뷰가 있다면 해당 뷰를 사용하는 것이 정신건강에 좋습니다.