[Android] CoordinatorLayout

핑구·2023년 11월 6일
1

Android

목록 보기
8/8
post-thumbnail

여행 상세보기에서 CoordinatorLayout을 사용하여 스크롤을 내리게 되었을 때, 앱바가 축소되며 상단에 고정되게 했다.
이 CoordinatorLayout에 대해 정리할 필요를 느껴 글을 작성하게 되었다.

공식문서 해석

CoordinatorLayout

CoordinatorLayout은 매우 강력한 기능을 가진 FrameLayout 이다. 이 레이아웃은 다음의 2가지 기본사용 사례를 위해 만들어졌다.

  1. 애플리케이션에서 최상위의 decor View로써 사용
  2. 자식 뷰들간의 특정한 인터렉션을 지원하는 컨테이너로써 사용

🐥 decorView는 뭐야?

The DecorView is the view that actually holds the window’s background drawable. Calling getWindow().setBackgroundDrawable() from your Activity changes the background of the window by changing the DecorView‘s background drawable.
스택오버플로우

CoordinatorLayout의 자식 뷰에 대한 Behaviors를 지정하면 하나의 부모 내에서 다양한 상호 작용을 제공할 수 있으며 자식뷰들도 서로 상호 작용할 수도 있습니다.

동작을 사용하여 슬라이딩 서랍과 패널부터 스와이프하여 해제할 수 있는 요소, 움직이거나 애니메이션이 적용될 때 다른 요소에 달라붙는 버튼에 이르기까지 다양한 상호 작용과 추가 레이아웃 수정을 구현할 수 있습니다.

코디네이터 레이아웃의 자식에는 anchor가 있을 수 있습니다. 이 뷰 ID는 CoordinatorLayout의 임의의 하위 항목에 해당해야 하지만 앵커된 자식 자체나 앵커된 자식의 하위 항목이 아닐 수도 있습니다. 다른 임의의 콘텐츠 창을 기준으로 플로팅 뷰를 배치하는 데 사용할 수 있습니다.

insetEdge속성을 이용해 CoordinatorLayout 안에 자식뷰들이 어떻게 배치될지 지정할 수 있습니다. 만약 자식뷰가 겹칠 것을 대비해, dodgeInsetEdges속성을 주어 적절하게 뷰가 겹치지 않도록 배치할 수 있습니다.


진짜 뭐래는지 감이 하나도 안잡혀서 이번에는 책을 읽어보았다.

깡샘의 안드로이드앱

AppBar란 화면 위쪽의 꾸밀 수 있는 영역이다.(ToolBar는 메뉴)
AppBar를 사용할 때는 대부분 AppBar 레이아웃 안에 ToolBar를 포함한다.

CoordinatorLayout - 뷰끼리 상호 작용하기

CoordinatorLayout은 뷰끼리 상호 작용해야 할 때 사용한다.
예를 들어 아래 View를 스크롤했는데 위의 View도 함께 스크롤되어야한다면 이를 사용한다.

위의 그림처럼 CoordinatorLayout 안에 ViewA, ViewB가 존재한다.
ViewA에서 스크롤이 발생한다면 이 정보를 CoordinatorLayout이 받아서 다른 뷰(ViewB)에 전해준다. 이렇게 두 View 모두 스크롤 된다.
자식 뷰끼리 상호작용 하려면 누군가는 코디네이터 레이아웃에 정보를 전달해야하고 또 다른 누군가는 그 정보를 받아야 한다. 이 역할을 Behavior가 하게 된다.

textView, ImageView 등은 스크롤 기능이 없기 때문에 이런 뷰에서 발생하는 스크롤을 연동하려면 NestedScrollView를 이용한다.

<androidx.coordinatorlayout.widget.CoordinatorLayout>
	<com.google.android.material.appbar.AppBarLayout>
		<androidx.appcompat.widget.Toolbar
			app:layout_scrollFlags="scroll|enterAlways" />
		<ImageView
			app:layout_scrollFlags="scroll|enterAlways" />
	</com.google.android.material.appbar.AppBarLayout>
	<androidx.core.widget.NestedScrollView
		app:layout_behavior="@string/appbar_scrolling_view_behavior">
		<TextView ... />
	</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>	

app:layout_behavior은 자신의 스크롤 정보를 어느 Behavior 클래스가 받아서 처리해야 하는지를 의미한다.
(예시에서는 app:layout_behavior 설정값을 문자열 리소스로 지정했는데 이 문자열은 com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior라는 클래스명이다.)
이 설정으로 CoordinatorLayout이 중첩 스크롤 뷰 정보를 AppBar 레이아웃의 ScrollingViewBehavior 클래스에 전달한다.
그리고 app:layout_scrollFlags 속성이 설정된 뷰가 스크롤 정보를 수신해서 함께 스크롤된다. 대부분 이 속성은 다음에 소개하는 CollapsingToolbarLayout에 추가한다.

CollapsingToolbarLayout

AppBar 레이아웃 하위에 선언하여 AppBar가 접힐 때 다양한 설정을 할 수 있다.
위의 예시처럼 이것을 사용하지 않고도 AppBar 레이아웃에 추가한 ToolBar나 ImageView 등을 접을 수 있지만 AppBar에 여러 개의 View를 추가했다면 매번 모든 View에 app:layout_scrollFlags속성을 지정하는 것은 효율적이지 않다.
그래서 앱바 레이아웃 하위에 CollapsingToolbarLayout을 추가하여 AppBar가 스크롤되어 접히거나 나타날 때 어떻게 동작해야 하는지를 설정한다.

<androidx.coordinatorlayout.widget.CoordinatorLayout>
	<com.google.android.material.appbar.AppBarLayout>
		<com.google.android.material.appbar.CollapsingToolbarLayout
			app:contentScrim="?attr/colorPrimary"
			app:expandedTitleMarginBottom="50dp"
			app:layout_scrollFlags="scroll|exitUntilCollapsed"
			app:title="AppBar Title">
			<ImageView
				app:layout_collapseMode="parallax" />
			<androidx.appcompat.widget.Toolbar
				app:layout_collapseMode="pin" />
		</com.google.android.material.appbar.CollapsingToolbarLayout>
	</com.google.android.material.appbar.AppBarLayout>
	<androidx.recyclerview.widget.RecyclerView
		app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

AppBar 레이아웃과 RecyclerView의 스크롤을 연동하겠다는 의도다.

속성 설명
app:expandedTitleMarginBottom : 앱바가 접히지 않았을 때 제목의 위치 설정
app:contentScrim : 앱바가 접히면 여기서 지정한 색상으로 앱바를 출력

스크롤 설정 속성

  • scroll | enterAlways : 스크롤 시 완전히 사라졌다가 거꾸로 스크롤 시 처음부터 다시 나타난다.(AppBar의 스크롤이 끝나면 그다음에 RecyclerView가 스크롤된다)
  • scroll | enterAlwaysCollapsed : 스크롤 시 완전히 사라졌다가 거꾸로 스크롤 시 처음부터 나타나지 않고 메인 콘텐츠 부분이 끝까지 스크롤된 다음에 나타난다.(RecyclerView가 모두 스크롤되고 AppBar가 스크롤된다)
  • scroll | exitUntilCollapsed : 스크롤 시 모두 사라지지 않고 ToolBar를 출력할 정도의 한 줄만 남을 때까지 스크롤된다.

app:layout_collapseMode
ToolBar와 ImageView에 이 속성을 지정했습니다. 이 속성은 AppBar를 스크롤할 때 AppBar에 포함한 각각의 View가 어떻게 움직여야 하는지를 설정한다.
AppBar 전체의 스크롤 설정은 CollapsingToolbarLayout의 layout_scrollFlags 속성으로 하고, 그 하위 뷰마다 스크롤 설정은 layout_collapseMode 속성으로 한다.

  • pin : 고정되어 스크롤되지 않는다.
  • parallax : 함께 스크롤된다.

pin으로 하면 AppBar는 스크롤되지만 뷰는 스크롤되지 않는다. 반면에 parallax로 지정하면 AppBar를 스크롤할 때 처음부터 함께 스크롤된다.


Behavior 만들어보기

어느정도 이해되었지만 나는 원리가 알고 싶었다.
그래서 직접 Behavior를 만들어보기로 했다.
참고 블로그를 바탕으로 만들어보았다.

진짜 멋없긴 한데, 설명해보자면
FloatingActionButton을 클릭하면 밑에 SnackBar가 뜬다. SnackBar가 뜨면 FloatingActionButton이 작아지고 가운데 ImageView가 커진다. 반대로 SnackBar가 사라지면 FloatingActionButton와 ImageView가 돌아온다.

class ExpandBehavior(context: Context, attrs: AttributeSet) :  
    CoordinatorLayout.Behavior<ImageView>(context, attrs) { 
  
    override fun layoutDependsOn(  
        parent: CoordinatorLayout,  
        child: ImageView,  
        dependency: View,  
    ): Boolean {  
        return dependency is SnackbarLayout  
    }  
  
    override fun onDependentViewChanged(  
        parent: CoordinatorLayout,  
        child: ImageView,  
        dependency: View,  
    ): Boolean {  
        child.scaleX = 5f  
        child.scaleY = 5f  
  
        return false  
    }  
  
    override fun onDependentViewRemoved(  
        parent: CoordinatorLayout,  
        child: ImageView,  
        dependency: View,  
    ) {  
        child.scaleX = 1f  
        child.scaleY = 1f  
    }  
}

위의 코드는 ImageView에 적용된 behavior이다.
단순하게 보면 layoutDependsOn()에서 true를 반환한다면 onDependentViewChanged()이 실행된다.

자세히 들어가서 CoordinatorLayout 내의 코드를 보면 아래와 같다.
동작원리는 은근 단순하다.

@SuppressWarnings("unchecked")  
final void onChildViewsChanged(@DispatchChangeEvent final int type) {  
    final int layoutDirection = ViewCompat.getLayoutDirection(this);  
    final int childCount = mDependencySortedChildren.size();  
    final Rect inset = acquireTempRect();  
    final Rect drawRect = acquireTempRect();  
    final Rect lastDrawRect = acquireTempRect();  
  
    for (int i = 0; i < childCount; i++) {  
        //...
        // Update any behavior-dependent views for the change  
        for (int j = i + 1; j < childCount; j++) {  
            final Behavior b = checkLp.getBehavior();  
  
            if (b != null && b.layoutDependsOn(this, checkChild, child)) {  
                if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {  
                    continue;
  				}  
  
                final boolean handled;  
                switch (type) {  
                    case EVENT_VIEW_REMOVED:  
                        b.onDependentViewRemoved(this, checkChild, child);
                        handled = true;  
                        break;                    
                    default:  
                        handled = b.onDependentViewChanged(this, checkChild, child);  
                        break;
  				}  
  
                if (type == EVENT_NESTED_SCROLL) {                   
  					checkLp.setChangedAfterNestedScroll(handled);  
                }  
            }  
        }  
    }  
  //...
}

final void onChildViewsChanged(@DispatchChangeEvent final int type) {...}
여기서 for문을 돌며 자신의 child들을 돌아보면서 event에 따라 각각의 함수를 실행한다.
layoutDependsOn()에서 true를 반환하면 switch문이 실행된다.
이 안에서 만약 Remove되었다면 onDependentViewRemoved()가 실행된다.
그게 아니면 onDependentViewChanged()를 실행한다.

이게 끝이다!

+ onDependentViewChanged가 항상 false를 return 하는 이유

(나만 궁금했지 또)

return true if the Behavior changed the child view's size or position, false otherwise
CoordinatorLayout에 의해 자식 뷰들의 위치나 상태에 변화가 필요한 경우 true로 설정된다.


참고

깡샘의 안드로이드 앱 프로그래밍 with 코틀린
http://dktfrmaster.blogspot.com/2018/03/coordinatorlayout.html
https://hatti.tistory.com/entry/android-Behavior
https://black-jin0427.tistory.com/201

profile
발전중

0개의 댓글