본문 바로가기

안드로이드/[Kotlin]

CoordinatorLayout 적용 (Feat. Scroll 상단 고정)

 

 

AlbumFragment.k

 

FLO 어플 클론코딩을 진행하면서 위와 같은 화면을 구성해야 하는 상황에 닥쳤다. 

 

 

화면 구성을 위와같은 식으로 잡았다.

 

우선 AlbumFragment.kt 의 아래쪽에 삽입되는 3개의 Fragment를 제쳐두고, 위쪽을 보니     [수록곡 , 상세정보 , 영상]  부분은 Scroll이 되면서 화면 상단에 도착했을 때 고정이 되고 아래쪽 Fragment의 ScrollView가 작동하는 식이다.   'ScrollView 상단고정'  뭐 이런 느낌으로 구글링을 해 본 결과 StickyScrolView 라는 키워드를 알게 되었다. 

그래서

 

https://github.com/amarjain07/StickyScrollView

 

GitHub - amarjain07/StickyScrollView: Sticky header and footer for android ScrollView.

Sticky header and footer for android ScrollView. Contribute to amarjain07/StickyScrollView development by creating an account on GitHub.

github.com

위의 GitHub 를 참고해서 코드를 작성 해 보았는데, 어렵쇼....?  AlbumFragment.kt 의 StickyScroll 은 원하는 대로 상단에 고정 되면서 잘 작동하는데, AlbumIncludedFragment.kt 내부의 scroll 이 작동이 안된다... 

 

여기서 많은 삽질을 하고 동아리 질문방에 질문을 해 본 결과, Scroll을 여러개 중첩 하려면 ScrollView가 아닌 NestedScrollView 를 사용해야 한다는 점과, 중복 Scoll과 함께 일부 View의 상단 고정까지 구현하고 싶다면 CoordinatorLayout 을 공부해 보라는 답변을 받았다.

 

 

 

 

그렇다면 CoordinatorLayout 에 대해서 알아보자.

 

https://developer.android.com/reference/androidx/coordinatorlayout/widget/CoordinatorLayout

 

CoordinatorLayout  |  Android Developers

CoordinatorLayout public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2, NestedScrollingParent3 java.lang.Object    ↳ android.view.View      ↳ android.view.ViewGroup        ↳ androidx.coordinatorlayout.widget.

developer.android.com

 


 

CoordinatorLayout 이란??

CoordinatorLayout은 FrameLayout에 기반을 둔 Layout 으로 2개의 주요한 기능이 있다.

  1. 최상위 Decor 뷰로서의 사용
  2. 자식 뷰들간의 인터렉션을 위한 컨테이너로서의 사용

 

대부분 CoordinatorLayout은 스크롤의 움직임에 따라 상단 Appbar에 변화를 주고자 할 때 사용한다. 아래 예시처럼 Appbar의 크기가 변화하는 화면을 만들고 싶을 대 CoordinatorLayout을 사용한다.

 

Cr

 

CoordinatorLayout 은 구조를 잘 파악해야 한다. 기본 구조는 아래와 같다.

<CoordinatorLayout>



    <AppBarLayout>
    
    	<CoollapsingToolbarLayout>
        
            <Toolbar
                ==> 상단 AppBar 에 고정 할 내용들
            />
            
            ==> Scroll을 할 때 위쪽으로 올라가면서 Collapsing 되는 부분
            
    	</CollapsingToolbarLayout>
        
    </AppBarLayout>
    
    
    
	
    <NestedScrollView  OR  RecyclerView>
    
    	<Layout~~~~>
        
    </NestedScrollView  OR  RecyclerView>
    
    
    
</CoordinatorLayout>

 

우선 Toolbar를 사용 할 것이므로 기본으로 설정되어 있는 Actionbar 는 없애주도록 하자.

FLO 클론 어플에 CoordinatorLayout을 적용했을 때 레이아웃과 코드는 아래와 같다.

 

<Toolbar>

<CollapsingToolbarLayout , AppbarLayout>

<TabLayout>

<ViewPager2>

 

 

레이아웃의 색상별로 나눠 보았다. 하단의 Fragment 전환은 TabLayout과 Viewpager2 를 활용하였다. 이에 대해서는 다음에 다른 글에서 설명 하도록 하겠다.

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:fitsSystemWindows="true">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/album_top_appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/transparent"
        app:elevation="0dp">
        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="@android:color/transparent"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <androidx.appcompat.widget.Toolbar
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_collapseMode="pin"
			
                app:contentInsetStart="0dp"
                app:contentInsetEnd="0dp">
                <!--Android Studio Toolbar 내부에 원하는 레이아웃을
                할때 치우침(특히 오른쪽으로) 현상이 생긴다
                이는 Toolbar 레이아웃이 기본적으로 16dp의 여백을 가지고 있기 때문이다.
                이 여백은 툴바에서 사용되는 홈 버튼, 앱 아이콘 등을 위해 설정되어
                있는 것으로 앱의 메인 AppBar로 사용되는 경우 건들지 않는 것이 좋다.
                위 기능 사용 목적이 아닌 내부 레이아웃을 배치하기 위함이라면
                아래 코드를 추가하여 치우침 현상을 해결할 수 있다.-->


                <androidx.constraintlayout.widget.ConstraintLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">
                    <ImageView
                        android:id="@+id/album_btn_arrow_back_iv"
                        android:layout_width="45dp"
                        android:layout_height="45dp"
                        android:layout_marginStart="10dp"
                        android:src="@drawable/btn_arrow_black"
                        app:layout_constraintStart_toStartOf="parent"
                        app:layout_constraintTop_toTopOf="parent" />

                    <ImageView
                        android:id="@+id/album_btn_like_off_iv"
                        android:layout_width="30dp"
                        android:layout_height="36dp"
                        android:src="@drawable/ic_my_like_off"
                        android:layout_marginEnd="10dp"
                        app:layout_constraintBottom_toBottomOf="@+id/album_btn_arrow_back_iv"
                        app:layout_constraintEnd_toStartOf="@id/album_btn_more_iv"
                        app:layout_constraintTop_toTopOf="@+id/album_btn_arrow_back_iv" />

                    <ImageView
                        android:id="@+id/album_btn_more_iv"
                        android:layout_width="36dp"
                        android:layout_height="36dp"
                        android:layout_marginEnd="10dp"
                        android:src="@drawable/btn_player_more"
                        app:layout_constraintBottom_toBottomOf="@+id/album_btn_like_off_iv"
                        app:layout_constraintEnd_toEndOf="parent"
                        app:layout_constraintTop_toTopOf="@+id/album_btn_like_off_iv" />
                </androidx.constraintlayout.widget.ConstraintLayout>
            </androidx.appcompat.widget.Toolbar>

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingTop="38dp"
                android:paddingBottom="35dp">
                <TextView
                    android:id="@+id/album_title_tv"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="IU 5th Album 'LILAC'"
                    android:textStyle="bold"
                    android:textSize="20sp"
                    android:textColor="#000000"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintBottom_toTopOf="@id/album_singer_tv"/>
                <TextView
                    android:id="@+id/album_singer_tv"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="아이유 (IU)"
                    android:textSize="14sp"
                    android:layout_marginTop="3dp"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/album_title_tv"/>
                <TextView
                    android:id="@+id/album_date_tv"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="2021.03.25 | 정규 | 댄스 팝"
                    android:textSize="11sp"
                    android:layout_marginTop="3dp"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/album_singer_tv"/>
                <androidx.cardview.widget.CardView
                    android:id="@+id/album_photo_iv"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    app:layout_constraintTop_toBottomOf="@id/album_date_tv"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:cardCornerRadius="10dp"
                    android:layout_marginTop="10dp">
                    <FrameLayout
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content">
                        <ImageView
                            android:layout_width="150dp"
                            android:layout_height="150dp"
                            android:src="@drawable/img_album_exp2"
                            android:scaleType="centerCrop"
                            android:adjustViewBounds="true"/>

                        <ImageView
                            android:layout_width="35dp"
                            android:layout_height="35dp"
                            android:src="@drawable/widget_black_play"
                            android:layout_gravity="right|bottom"/>
                    </FrameLayout>
                </androidx.cardview.widget.CardView>
                <ImageView
                    android:id="@+id/album_lp_iv"
                    android:layout_width="0dp"
                    android:layout_height="120dp"
                    android:adjustViewBounds="true"
                    android:src="@drawable/img_album_lp"
                    app:layout_constraintTop_toTopOf="@id/album_photo_iv"
                    app:layout_constraintBottom_toBottomOf="@id/album_photo_iv"
                    app:layout_constraintStart_toEndOf="@id/album_photo_iv"/>

            </androidx.constraintlayout.widget.ConstraintLayout>
        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/album_top_appbar"
        app:layout_anchorGravity="bottom"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/album_tab_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:tabIndicatorFullWidth="false"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            />

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/album_view_pager"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@id/album_tab_layout"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>


</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

코드를 그냥 복붙 해 온거여서 말도안되게 길다. 하지만 기본 구조는 위에서 설명한 구조와 동일하다. 살만 붙인거다!!

 

 

위에서 CoordinatorLayout의 구조를 설명할 때 상단 AppbarLayout 부분 말고 아래쪽에는 NestedScrollView 또는 RecyclerView 가 온다고 했었는데 나의 경우에는 ConstraintLayout으로 감싸진 TabLayout과 ViewPager2 가 있다. 어렵쇼....뭐지??? 가 아니다. ViewPager와 연결 된 Fragment에 NestedScrollView가 있다 !!!

 

 


 

 

++ 알아두면 좋은 속성들

 

1.  android:fitsSystemWindows="true"

위 속성을 상단에 고정이 되는 Toolbar를 제외 한 CoordinatorLayout, AppbarLayout, CollapsingToolbarLayout 에 추가 해 줘야 한다.

사실 이 속성이 의미하는 바를 잘 모르겠다. 더 많은 공부와 구글링이 필요할 거 같다. 아무튼 이 속성을 CoordinatorLayout CollapsingToolbarLayout 에 추가했다. 이 속성을 추가하면 화면이 스크롤 됨과 동시에 올라가면서 Status bar 영역까지 채워지면서 스크롤 된다. 우리는 Status bar를 투명하게 했으므로 투명한 Status bar 뒤로 View가 채워진다.

 


 

2.  app:layout_scrollFlags

위 속성을 CollapsintToolbarLayout 에 사용해서 스크롤 시에 다양한 효과를 줄 수 있다. 위 속성을 이용하면 스크롤 시 Toolbar를 보여지게 하거나 감추는 등의 flag를 적용할 수 있다.

 

        scroll | enterAlways

enterAlways는 스크롤이 아래로 이동했을 때 Appbar가 완전히 사라졌다가, 위쪽으로 이동했을 때 Appbar 전체가 나타난다. 웹 브라우저에서 스크롤을 아래로 내릴 땐 주소창이 감춰지다가, 위쪽을 스크롤을 하게 되면 주소창이 보여지는 경우에 적용된다.

 

       scroll | enterAlways | enterAlwaysCollapsed

enterAlwaysCollapsed는 스크롤이 아래로 이동했을 때 Appbar가 완전히 사라졌다가, 위쪽으로 이동했을 때 Toolbar가 minheight 만큼만 내려오고 스크롤이 최 상단에 도착했을 때 나머지 Appbar 전체가 나타난다. 

이 경우에는 Toolbar에 android:minHeight 속성을 적용 시키거나 Toolbar의 height를 직접 설정해 주면 된다. 또한 enterAlwaysCollapsed는 반드시 enterAlways와 조합해서 사용해야 한다.

 

       scroll | exitUntilCollased

exitUntilCollapsed는 스크롤을 아래, 위로 이동할 때 Toolbar 의 minHeight 만큼만 보여지고 스크롤이 최 상단에 도착 시 나머지 Appbar의 전체가 내려오게 된다. 위에서 소개한 enterAlways | enterAlwaysCollapsed 와 다른 점은 아래쪽으로 스크롤링 해도 AppbarLayout 이 완전히 사라지지 않는 다는 것이다.

 

       scroll | snap

snap 은 마치 자석에 달라붙는 것 같은 느낌으로 AppbarLayout size 의 절반 크기를 기준으로 아래 위로 달라 붙는 flag 이다.

 

 


 

3.  app:layout_collapseMode

위 속성을 Toolbar 안에 설정하면 스크롤이 발생했을 때에 Toolbar의 최종 형태가 어떤 형태인지를 결정할 수 있다.

 

       pin

CollapsingToolbarLayout 이 완전히 축소되었을 때에 Toolbar는 화면 상단에 고정(pin) 된다.

 

       parallx

툴바가 축소되는 동안Parallax모드로 동작하도록 한다. ImageView안에 layout_collapseMode=”parallax”를 이용하면 스크롤을 끝까지 올리면 이미지의 중간부분이 보여지고, 해당 옵션을 없애면 위부터 아래로 사라진다.

 

 


4.  app:layout_behavior="@string/appbar_scrolling_view_behavior"

위의 전체코드를 보면 TabLayout과 ViewPager2 를 감싸고 있는 ConstraingLayout에 app:layout_behavior="@string/appbar_scrolling_view_behavior" 라는 속성이 있다. CoordinatorLayout은 FrameLayout을 상속 한 레이아웃이므로 위의 속성을 추가하지 않으면 TabLayout과 ViewPager는 Appbar와 중첩되어 그려진다. 그러므로 매우 필수적인 속성이라고 볼 수 있다.

 

 

 


 

 

 

 

추가적으로 Android Studio 의 기본 템플릿 중 ScrollingActivity의 코드를 살펴보면 anchor 라는 속성이 있다. 아래 코드의 의미를  파악 해 보면 FloatingActionButton을 appbar 를 기준으로 ( app:layout_anchor="@id/app_bar" ) 오른쪽 아래에 배치 하겠다는 뜻이다. ( app:layout_anchorGravity="bottom|end" )

 

<com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/fab_margin"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|end"
        app:srcCompat="@android:drawable/ic_dialog_email" />

 

 

그리고 AppBarLayout 의 속성 중에서 app:elevation="0dp" 속성이 있다. 스크롤이 최하단으로 내려갔을때 toolbar의 주변으로 그림자가 생기는데 이를 없애주는 속성이다. android:elevation="0dp" 가 아니라  app:elevation="0dp" 인것에 유의하자.

 

 

참고 :

https://kangmin1012.tistory.com/33

https://one-delay.tistory.com/68

https://freehoon.tistory.com/38

layout_scrollFlags : http://areemak.blogspot.com/2018/04/blog-post.html