[안드로이드/따라만들기] 카카오뱅크 앱 애니메이션, MotionLayout 으로 따라만들기!

에짱·2021년 10월 1일
1
post-thumbnail
post-custom-banner

우리가 자주 사용하는 앱에는 알게 모르게 많은 애니메이션들이 들어가서 스크롤이나 화면 전환 등을 더욱 부드럽게 해줍니다. 카카오뱅크에 있는 애니메이션이 간단해보여서 한 번 따라 해보았는데 생각보다 간단하진 않았습니다... 역시 개발자들 리스펙..

애니메이션카카오뱅크따라만든 것
앱바 애니메이션
스크롤 애니메이션

안드로이드에서 다음과 같은 방법으로 애니메이션 처리를 할 수 있습니다.

  • Animated Vector Drawable
  • Property Animation Framework
  • LayoutTransition Animation
  • Layout transitions with TransitionManager
  • CoordinatorLayout

위에 카카오뱅크 애니메이션은 2018년에 발표한 애니메이션 툴인 MotionLayout 을 활용해서 따라해본 것입니다!

앱바 애니메이션

<?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"
    android:id="@+id/motionlayout"
    app:layoutDescription="@xml/motion_scene2"
    app:layout_scrollFlags="scroll|enterAlwaysCollapsed|exitUntilCollapsed"
    tools:context=".appbar_anim.MotionActivity">

    <androidx.core.widget.NestedScrollView
        android:id="@+id/nested_scrollview"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/linearlayout"
        app:layout_constraintBottom_toBottomOf="parent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            android:text="@string/kakao_bank_txt" />

    </androidx.core.widget.NestedScrollView>


    <LinearLayout
        android:id="@+id/linearlayout"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:orientation="vertical"
        app:layout_constraintTop_toTopOf="parent"
        android:background="@color/white"
        />

    <TextView
        android:id="@+id/name_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:autoSizeTextType="uniform"
        android:text="최은정"
        android:textStyle="bold"
        android:textSize="17sp"
        android:textColor="@color/black"
        app:layout_constraintBottom_toBottomOf="@id/linearlayout"
        app:layout_constraintTop_toTopOf="@id/linearlayout"
        app:layout_constraintEnd_toEndOf="@id/linearlayout"
        app:layout_constraintHorizontal_bias="0.1"
        app:layout_constraintStart_toStartOf="@id/linearlayout"/>

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn"
        android:layout_width="65dp"
        android:layout_height="32dp"
        android:text="내 계좌"
        android:textSize="13sp"
        android:layout_marginStart="20dp"
        android:background="@drawable/background_btn"
        app:layout_constraintTop_toTopOf="@id/name_tv"
        app:layout_constraintBottom_toBottomOf="@id/name_tv"
        app:layout_constraintStart_toEndOf="@id/name_tv"/>

    <ImageView
        android:id="@+id/profile_iv"
        android:layout_width="40dp"
        android:layout_height="40dp"
        app:layout_constraintBottom_toBottomOf="@id/linearlayout"
        app:layout_constraintEnd_toEndOf="@id/linearlayout"
        app:layout_constraintHorizontal_bias="0.9"
        app:layout_constraintStart_toStartOf="@id/linearlayout"
        app:layout_constraintTop_toTopOf="@id/linearlayout"
        app:srcCompat="@drawable/curby" />

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

우선 앱바 전체가 크기가 변하기 때문에 부모 layout 을 MotionLayout 으로 합니다.
그리고 nestedScrollView 를 제외한 부분이 스크롤에 따라서 움직이기 때문에
이것에 대한 MotionScene xml 파일을 작성해서 MotionLayout 의 layoutDescription 속성으로 연결해주어야 합니다.
MotionLayout 은 ConstraintLayout 의 하위 클래스이기 때문에 constraint 를 지정해서 위치를 잡아주면 됩니다. 또한 MotionLayout은 자신을 포함해서 direct child 는 반드시 id 를 가지고 있어야 합니다.
참고로 안드로이드 스튜디오 layout design 탭에서 보이는 디자인은 end 애니메이션을 보여줍니다.

motion_scene2.xml

<?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
        motion:constraintSetEnd="@+id/collapsed"
        motion:constraintSetStart="@+id/expanded"
        motion:duration="1000"
        motion:motionInterpolator="linear">

        <OnSwipe
            motion:dragDirection="dragUp"
            motion:touchAnchorId="@id/nested_scrollview"
            motion:touchAnchorSide="top" />

    </Transition>

    <ConstraintSet android:id="@+id/expanded">

        <Constraint
            android:id="@+id/linearlayout"
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:orientation="vertical"
            motion:layout_constraintTop_toTopOf="parent"
            android:background="@color/white"
            />

        <Constraint
            android:id="@+id/name_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="최은정"
            android:scaleX="1.3"
            android:scaleY="1.3"
            android:textColor="@color/black"
            motion:layout_constraintBottom_toBottomOf="@id/linearlayout"
            motion:layout_constraintTop_toTopOf="@id/linearlayout"
            motion:layout_constraintEnd_toEndOf="@id/linearlayout"
            motion:layout_constraintHorizontal_bias="0.1"
            motion:layout_constraintStart_toStartOf="@id/linearlayout"/>

        <Constraint
            android:id="@+id/btn"
            android:layout_width="65dp"
            android:layout_height="32dp"
            android:text="내 계좌"
            android:textSize="13sp"
            android:layout_marginStart="20dp"
            android:background="@drawable/background_btn"
            motion:layout_constraintTop_toTopOf="@id/name_tv"
            motion:layout_constraintBottom_toBottomOf="@id/name_tv"
            motion:layout_constraintStart_toEndOf="@id/name_tv"/>

        <Constraint
            android:id="@+id/profile_iv"
            android:layout_width="40dp"
            android:layout_height="40dp"
            motion:layout_constraintBottom_toBottomOf="@id/linearlayout"
            motion:layout_constraintEnd_toEndOf="@id/linearlayout"
            motion:layout_constraintHorizontal_bias="0.9"
            motion:layout_constraintStart_toStartOf="@id/linearlayout"
            motion:layout_constraintTop_toTopOf="@id/linearlayout"
            motion:srcCompat="@drawable/curby" />

    </ConstraintSet>

    <ConstraintSet android:id="@+id/collapsed">

        <Constraint
            android:id="@+id/linearlayout"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:orientation="vertical"
            android:elevation="10dp"
            motion:layout_constraintTop_toTopOf="parent"
            android:background="@color/white"
            />

        <Constraint
            android:id="@id/name_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleY="1.0"
            android:scaleX="1.0"
            android:elevation="10dp"
            motion:layout_constraintTop_toTopOf="@id/linearlayout"
            motion:layout_constraintEnd_toEndOf="@id/linearlayout"
            motion:layout_constraintHorizontal_bias="0.05"
            motion:layout_constraintBottom_toBottomOf="@id/linearlayout"
            motion:layout_constraintStart_toStartOf="@id/linearlayout" />

        <Constraint
            android:id="@+id/btn"
            android:layout_width="65dp"
            android:layout_height="32dp"
            android:text="내 계좌"
            android:textSize="13sp"
            android:elevation="10dp"
            android:layout_marginStart="20dp"
            android:background="@drawable/background_btn"
            motion:layout_constraintTop_toTopOf="@id/name_tv"
            motion:layout_constraintBottom_toBottomOf="@id/name_tv"
            motion:layout_constraintStart_toEndOf="@id/name_tv"/>

        <Constraint
            android:id="@id/profile_iv"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:elevation="10dp"
            motion:layout_constraintStart_toStartOf="@id/linearlayout"
            motion:layout_constraintHorizontal_bias="0.95"
            motion:layout_constraintBottom_toBottomOf="@id/linearlayout"
            motion:layout_constraintEnd_toEndOf="@id/linearlayout"
            motion:layout_constraintTop_toTopOf="@id/linearlayout" />


    </ConstraintSet>
</MotionScene>

애니메이션을 직접적으로 만드는 부분이 바로 이 MotionScene 입니다.
MotionScene에는 반드시 하나 이상의 Transition이 있어야 합니다. 이는 애니메이션의 시작(start)과 끝(end) ConstraintSet 을 정의하고, duration 등의 기타 속성들을 정의합니다. 애니메이션 시작을 OnClick 과 OnSwipe 핸들러로 정의할 수 있으며, KeyFrameSet 을 통해 애니메이션의 시작과 끝 뿐만 아니라 그 사이 지점들의 Constraint 를 정의할 수 있습니다. 여기서는 onSwipe 을 통해 nestedScrollView 가 스크롤 될 때 애니메이션이 시작될 수 있도록 하였습니다.
코드에서 id 가 expanded(start) 로 정의된 ConstraintSet 태그 안에는 앱바가 늘어났을 때의 뷰들의 상태를 지정합니다. 여기서 linearLayout 의 height 가 80dp 인 것에 주목해주세요.
그리고 collapsed(end) ConstraintSet 은 앱바가 줄어들었을 때의 상태를 정의합니다. 여기서는 linearLayout 의 height 를 actionBarSize 로 하였습니다. 이렇게 애니메이션의 시작과 끝 상태를 지정하면 똑똑한 MotionLayout 이 계산해서 부드러운 애니메이션을 만들어준답니다.
추가적으로 이름 부분이 앱바가 늘어났을 때 오른쪽으로 조금 이동하고, 프로필 사진은 왼쪽으로 이동해서 안쪽으로 모아지게 되는데요,
이러한 애니메이션 같은 경우는, layout_constraintHorizontal_bias 를 조절해서 구현하였고, 크기 변화같은 경우는 이름 textView는 scaleX, scaleY 크기를 조절했습니다. 매우 간단하죠?

문제와 해결

자료 조사를 통해서 저는 이 애니메이션을 3가지 방법으로 구현 해보았습니다. 그런데 소개드린 방법이 가장 간단명료한 방법이라 생각이 들어 이 방법을 공유하게 되었습니다.
앱바 애니메이션이기 때문에 AppBarLayout 안에 MotionLayout 을 사용할 수도 있긴 한데요, 제가 이렇게 해보려고 했을 때는 앱바가 크기가 줄어들었을 때는 constraint 로 가운데 정렬이 되지 않는 문제가 있었습니다. 그래서 bottom 에만 constraint 를 주고 margin_bottom 으로 정렬 유사하게 구현해야 되었기 때문에 이 방법을 선택하지 않았습니다.
그런데 이렇게 되면 AppBarLayout 의 liftOnScroll 속성을 사용할 수 없어서 스크롤을 위로 했을 때 앱바 아래 부분에 그림자가 생기는 것을 구현할 수 없게 되는데요,
저는 이를 collapsed 상태에서 elevation 속성을 주어서 해결했습니다.

스크롤 애니메이션

<?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:id="@+id/motionlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/motion_scene_tabscroll"
    app:layout_scrollFlags="scroll"
    tools:context=".tab_scroll_anim.TabScrollActivity">

    <androidx.core.widget.NestedScrollView
        android:id="@+id/nested_scrollview"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginVertical="12dp"
        app:layout_constraintTop_toBottomOf="@id/tablayout"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginHorizontal="12dp"
            >

            <ImageView
                android:id="@+id/viewpager_iv"
                android:layout_width="match_parent"
                android:layout_height="220dp"
                android:src="@drawable/curby_full"
                android:scaleType="centerCrop"
                app:layout_constraintTop_toTopOf="parent"/>

            <TextView
                android:id="@+id/deposit_tv"
                android:layout_width="match_parent"
                android:layout_height="480dp"
                android:text="예금,적금"
                android:textSize="16sp"
                android:textColor="@color/black"
                android:fontFamily="@font/roboto_bold"
                android:paddingTop="40dp"
                android:layout_marginTop="10dp"
                android:background="@color/purple_200"
                app:layout_constraintTop_toBottomOf="@id/viewpager_iv"/>

            ...

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.core.widget.NestedScrollView>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:background="@color/white"
        app:layout_constraintTop_toTopOf="@id/title_tv"
        app:layout_constraintBottom_toBottomOf="@id/tablayout"
        android:id="@+id/linearlayout_shadow"
        android:orientation="horizontal" />

    <TextView
        android:id="@+id/title_tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingStart="13dp"
        android:paddingTop="25dp"
        android:text="상품/서비스"
        android:textColor="@color/black"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tablayout"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:paddingTop="15dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:scrollIndicators="bottom"
        app:layout_constraintTop_toBottomOf="@id/title_tv"
        app:tabIndicatorColor="@color/black"
        app:tabTextAppearance="@style/myCustomTabText"
        app:tabRippleColor="@android:color/transparent"
        app:tabIndicatorFullWidth="false"
        app:tabMode="auto"
        app:tabMinWidth="22dp"
        app:tabSelectedTextColor="@color/black"
        app:tabTextColor="@color/gray">

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/purple_200"
            android:text="전체" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/purple_200"
            android:text="예적금" />

        ...

    </com.google.android.material.tabs.TabLayout>

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

앱바 애니메이션과 유사하게 전체를 MotionLayout 으로 감싸고 nestedScrollView 는 대충.. scrollTo 를 확인할 수 있도록 색으로 각 부분을 표시했습니다... ㅎ
그리고 상단을 linearlayout, textview, tablayout 으로 구성했습니다.

motion_scene_tabscroll

<?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
        motion:constraintSetEnd="@+id/collapsed"
        motion:constraintSetStart="@+id/expanded"
        motion:duration="10"
        motion:motionInterpolator="linear" >

        <OnSwipe
            motion:dragDirection="dragUp"
            motion:touchAnchorId="@id/nested_scrollview"
            motion:touchAnchorSide="top" />

    </Transition>

    <ConstraintSet android:id="@+id/expanded">

        <Constraint
            android:id="@+id/title_tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            motion:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@+id/tablayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            motion:layout_constraintTop_toBottomOf="@id/title_tv" />

        <Constraint
            android:id="@+id/linearlayout_shadow"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            motion:layout_constraintTop_toTopOf="@id/title_tv"
            motion:layout_constraintBottom_toBottomOf="@id/tablayout" />


    </ConstraintSet>

    <ConstraintSet android:id="@+id/collapsed">

        <Constraint
            android:id="@+id/title_tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:elevation="5dp"
            motion:layout_constraintBottom_toTopOf="@id/tablayout" />

        <Constraint
            android:id="@+id/tablayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:elevation="5dp"
            motion:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@+id/linearlayout_shadow"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:elevation="5dp"
            motion:layout_constraintTop_toTopOf="@id/title_tv"
            motion:layout_constraintBottom_toBottomOf="@id/tablayout" />


    </ConstraintSet>
</MotionScene>

motionScene 부분에서 살짝 머리를 좀 썼는데요, title_tv 만 스크롤시 위로 사라지고, tablayout 은 sticky scrollview 처럼 위에 착 딸라붙어야 합니다.
이를 motionLayout 으로 구현하기 위해서는 title_tv 를 collapsed 시에 height 를 0dp 로 만들거나 visibility 를 gone 으로 하면 위로 스크롤과 동시에 tablayout 이 title_tv 위로 올라와서 하나도 똑같아지지가 않습니다.
그래서 저는 대신에 text_tv 의 constraint 를 layout_constraintTop_toTopOf="parent" 에서 layout_constraintBottom_toTopOf="@id/tablayout" 으로 바꾸고,
tablayout 은 layout_constraintTop_toBottomOf="@id/title_tv" 에서 layout_constraintTop_toTopOf="parent" 로 바꿨습니다.
즉, title_tv 가 tablayout 위쪽에 붙도록 해서 tablayout 이 천장에 달라붙을 때 위로 사라지는 것 같은 효과를 낸 것입니다.
그리고 AppBarLayout 의 liftOnScroll 그림자를 표현하기 위해 collapsed 상태의 모든 뷰에게 elevation 을 주었습니다.

class TabScrollActivity : AppCompatActivity(), TabLayout.OnTabSelectedListener,
    View.OnScrollChangeListener {

    private lateinit var binding: ActivityTabScrollBinding

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

        binding = ActivityTabScrollBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.tablayout.addOnTabSelectedListener(this)

        binding.nestedScrollview.setOnScrollChangeListener(this)


    }

    override fun onTabSelected(tab: TabLayout.Tab?) {
            when (tab?.position) {
                0 -> binding.nestedScrollview.scrollToView(binding.viewpagerIv) //전체
                1 -> binding.nestedScrollview.scrollToView(binding.depositTv) //예적금
                2 -> binding.nestedScrollview.scrollToView(binding.loanTv) //대출
                3 -> binding.nestedScrollview.scrollToView(binding.serviceTv) //서비스
                4 -> binding.nestedScrollview.scrollToView(binding.partnershipTv) //제휴
                5 -> binding.nestedScrollview.scrollToView(binding.miniTv) //mini
            }
    }

    override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
    override fun onTabReselected(tab: TabLayout.Tab?) = Unit

    override fun onScrollChange(p0: View?, p1: Int, scrollY: Int, p3: Int, p4: Int) {
        if (scrollY > 0) binding.motionlayout.transitionToEnd()

        when {
            scrollY == 0
            -> {
                binding.tablayout.setScrollPosition(0, 0f, true)
                binding.motionlayout.transitionToStart()
            }
            scrollY >= binding.nestedScrollview.computeDistanceToView(binding.depositTv)  &&
                    scrollY < binding.nestedScrollview.computeDistanceToView(binding.loanTv)
                    -> binding.tablayout.setScrollPosition(1, 0f, true)
            scrollY >= binding.nestedScrollview.computeDistanceToView(binding.loanTv) &&
                    scrollY < binding.nestedScrollview.computeDistanceToView(binding.serviceTv)
                 ->  binding.tablayout.setScrollPosition(2, 0f, true)
            scrollY >= binding.nestedScrollview.computeDistanceToView(binding.serviceTv) &&
                    scrollY < binding.nestedScrollview.computeDistanceToView(binding.partnershipTv)
            ->   binding.tablayout.setScrollPosition(3, 0f, true)
            scrollY >= binding.nestedScrollview.computeDistanceToView(binding.partnershipTv) &&
                    scrollY < binding.nestedScrollview.computeDistanceToView(binding.miniTv)
            ->   binding.tablayout.setScrollPosition(4, 0f, true)
        }

        if(!binding.nestedScrollview.canScrollVertically(1))  binding.tablayout.setScrollPosition(5, 0f, true)

    }
}

이제 남은 것은 각 탭을 클릭했을 때 해당 내용으로 scroll 되는 것과 scroll 시 해당 탭이 선택되는 로직을 짜는 부분입니다.

참고로 탭 클릭시 해당 내용으로 scroll 되는 로직은 https://greedy0110.tistory.com/41 의 코드를 활용했으니 상세한 설명은 여기를 참고해주시면 감사하겠습니다!

onTabSelected 에서 탭의 index 에 따라 scrollToView 확장함수로 해당 내용으로 scroll 합니다. 그리고 onScrollChange 에서는 scrollY (스크롤이 된 길이) 가 0 이상일 때, 즉 스크롤이 되기 시작했을 때, motionScene 의 collapsed (end) 가 작동하도록 합니다.
0일 때(스크롤이 맨 위로 왔을 때, 초기상태) 는 당연히 expanded(start) 상태가 시작되도록 하고, tablayout 의 scrollPosition 을 '전체' 탭에 가있도록 index 를 넣어줍니다.

이때, 직접적으로 binding.tablayout.getTabAt(0)?.select()을 사용하지 않고 setScrollPositon을 사용하는 이유는 위에 onTabSelected 와 충돌하기 때문입니다.
예를 들어서, scrollY가 전체의 top 과 예적금 top 사이에 있을 때, getTabAt(0)?.select() 을 해서 전체 탭을 select 하게 하면 onTabSelected 에 따라 바로 전체의 top 으로 이동하게 됩니다. 그래서 갑자기 순간이동을 하게 되는 불상사가 발생하게 됩니다. 따라서 setScrollPositon 을 사용해서 실제로 탭이 select 되지는 않았지만 무늬만 선택된 것처럼 색상 변경만 해주는 것입니다.

정리

처음으로 모션 레이아웃을 사용해보았는데요, 생각보다 간단하게 애니메이션을 만들 수 있음에 놀랐고, 한편으로 내부 코드는 얼마나 복잡할까.. 싶기도 했습니다ㅋ. 카카오뱅크 애니메이션과 유사하게 만들긴 했지만, 스크롤 애니메이션 같은 경우에는 약간의 차이가 있어서 아마도 카카오에서는 뷰를 커스텀해서 만들었을 가능성이 커보입니다. 더 공부해보고 더 똑같이 만들어 보도록 하겠습니다!ㅎㅎ

참고자료

https://medium.com/gomechanic/android-tab-change-recycler-scroll-in-kotlin-17cd05e88c9b
https://greedy0110.tistory.com/41
https://blog.gangnamunni.com/post/MotionLayout/
https://blog.stylingandroid.com/motionlayout-collapsing-toolbar-part-1/
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=pistolcaffe&logNo=221016672922
https://new-spring-bom.tistory.com/6
https://freehoon.tistory.com/38
https://gist.github.com/mikkipastel/bae0b446f4cffb5b637bc86036d34743
https://blog.gangnamunni.com/post/MotionLayout/
https://gamjatwigim.tistory.com/82
https://www.charlezz.com/?p=717

profile
지금 여기. Here and Now
post-custom-banner

0개의 댓글