Android CoordinatorLayout 사용기

김성환·2024년 4월 3일

앱을 만들다보니까 해당앱의 레이아웃이나 디자인 현재 서비스되고있는 다른 앱들과 다르다고 생각이 들었습니다.
웹 화면만봐도 스크롤을 위로 올리면 toolbar가 보이고 스크롤을 내리면 보이지 않는 이런 UI를 만들고 싶다고 생각하였습니다. 찾아보니 모션을 감지하여 코드로 해당 툴바를 보이게하거나 사리지게 만들수도 있지만 안드로이드에 이런기능을 지원하는 CoordinatorLayout 레이아웃을 지원한다고 합니다. 이번 포스팅에서는 CoordinatorLayout을 사용해보며 이레이아웃의 기능과 응용방법들을 알아가볼까 합니다.

CoordinatorLayout is a super-powered FrameLayout.
CoordinatorLayout is intended for two primary use cases:
1. As a top-level application decor or chrome layout
2. As a container for a specific interaction with one or more child views
By specifying Behaviors for child views of a CoordinatorLayout you can provide many different interactions within a single parent and those views can also interact with one another. View classes can specify a default behavior when used as a child of a CoordinatorLayout by implementing the AttachedBehavior interface.
Behaviors may be used to implement a variety of interactions and additional layout modifications ranging from sliding drawers and panels to swipe-dismissable elements and buttons that stick to other elements as they move and animate.
Children of a CoordinatorLayout may have an anchor. This view id must correspond to an arbitrary descendant of the CoordinatorLayout, but it may not be the anchored child itself or a descendant of the anchored child. This can be used to place floating views relative to other arbitrary content panes.
Children can specify insetEdge to describe how the view insets the CoordinatorLayout. Any child views which are set to dodge the same inset edges by dodgeInsetEdges will be moved appropriately so that the views do not overlap.

공식문서에서의 CoordinatorLayout의 정의 입니다.
해석하면 CoordinatorLayout 강력한 기능을 가진 FrameLayout이고 애플리케이션에서 최상위의 decor View로써 사용되거나 하나 이상의 자식 뷰와 상호 작용하는 특정 상호 작용의 컨테이너로 사용된다고 합니다.

CoordinatorLayout의 자식 뷰에 대해 동작을 지정함으로써 하나의 부모 내에서 다양한 상호 작용을 제공하고 해당 뷰들은 서로 상호 작용할 수 있습니다. View 클래스는 AttachedBehavior 인터페이스를 구현함으로써 CoordinatorLayout의 자식으로 사용될 때 기본 동작을 지정할 수 있습니다.

동작은 슬라이딩 서랍 및 패널에서 스와이프로 제거할 수 있는 요소 및 이동 및 애니메이션하는 동안 다른 요소에 붙어있는 버튼과 같은 다양한 상호 작용 및 추가적인 레이아웃 수정을 구현하는 데 사용될 수 있습니다.

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

자식은 CoordinatorLayout에 어떻게 뷰를 삽입하는지를 나타내는 insetEdge를 지정할 수 있습니다. 자식뷰가 겹칠 것을 대비해, dodgeInsetEdges속성을 주어 적절하게 뷰가 겹치지 않도록 배치할 수 있습니다.


결과적으로 위에서 모션을 감지하고 해당 동작이 진행됬다는 정보를 뷰끼리 CoordinatorLayout을통하여 상호 작용을 할수있다는것입니다. 코디네이터 레이아웃에 정보를 전송하고 수신하는 역할의 경우 Behavior이라는 클래스를 이용하게 되는데 자식 뷰의 동작을 정의하여 특정 자식 뷰에 대한 상호 작용 및 레이아웃 수정을 구현할 수 있습니다.

class CustomBehavior(context: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior<View>(context, attrs) {

    // Behavior에서 사용할 필요에 따라 생성자 및 다른 메서드를 오버라이드할 수 있습니다.

    override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
        // dependency에 따라 child의 레이아웃이 어떻게 의존해야 하는지 여부를 지정합니다.
        // 예를 들어, dependency가 Snackbar인 경우에만 child의 레이아웃을 변경할 수 있도록 지정할 수 있습니다.
        return dependency is Snackbar.SnackbarLayout
    }

    override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
        // dependency의 변경에 따라 child의 레이아웃을 조정합니다.
        // 이 메서드에서 child의 위치 및 크기를 조정하거나 다른 작업을 수행할 수 있습니다.
        return true
    }

    // 다른 Behavior 메서드를 필요에 따라 오버라이드할 수 있습니다.
}

이런식으로 커스텀해서 사용할수도 있습니다.
해당 behavior들은 xml에 지정하여 손쉽게 사용할수있습니다.

behavior와 관련된 속성으로는

app:layout_behavior

이 속성은 CoordinatorLayout의 자식 뷰에 대한 Behavior 클래스를 지정합니다. 즉, 해당 뷰가 어떤 동작을 수행할지 결정합니다. 이 속성은 CoordinatorLayout 내에서 특정 뷰에 대한 상호작용을 지정할 때 사용됩니다.

app:layout_scrollFlags

이 속성은 AppBarLayout 내의 자식 뷰의 스크롤 동작을 지정합니다. 다양한 플래그를 사용하여 스크롤 동작을 제어할 수 있습니다. 주요 플래그에는 다음이 있습니다.

scroll: View가 사라진다고 지정하며 사용하지 않으면 View는 사라지지 않고 남아있는다.
enterAlways: scroll과 같이 사용되어야하며 아래로 스크롤하면 사라지고 위로 스크롤하면 나타난다.
enterAlwaysCollapsed: enterAlways와 비슷하지만 아래로 스크롤 할 때 리스트의 끝에 도달해야 나타난다. scroll, enterAlways와 같이 사용해야 한다.
exitUntilCollapsed: 위로 스크롤 할 때 minHeight에 도달하기 전까진 나타나지 않는다.
그리고 반대로 스크롤 할 때 minHeight까지는 View가 사라지지 않는다.

app:behavior_expandedOffset

이 속성은 BottomSheetBehavior의 확장된 상태에서의 오프셋을 지정합니다. BottomSheet가 확장된 상태일 때 오프셋은 항상 이 속성에 지정된 값만큼 적용됩니다.

app:behavior_fitToContents

BottomSheetBehavior가 내용물의 크기에 맞게 조정되어야 함을 나타내는 속성입니다. 내용물의 크기에 맞게 조정되면 BottomSheet는 화면의 일부만 차지하게 됩니다.

app:behavior_hideable

BottomSheetBehavior를 숨길 수 있는지 여부를 지정하는 속성입니다. 숨김 가능한 경우 BottomSheetBehavior의 상태를 COLLAPSED로 변경하여 숨길 수 있습니다.

app:behavior_peekHeight

BottomSheetBehavior의 피크 높이를 지정하는 속성입니다. 피크 높이는 사용자가 BottomSheet를 드래그하여 보여주거나 숨길 때 높이로 사용됩니다.


적용

예를 들어 FloatingActionButton과 RecyclerView가 있는데 RecyclerView가 스크롤방향에따라 FloactingActionButton을 사라지게 만들거나 보이게 만들어봅시다

activity_main

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <!-- @string/appbar_scrolling_view_behavior =  com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior-->
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:layout_margin="30dp"
        android:src="@drawable/baseline_add_24"
        app:layout_behavior="com.example.myapplication.CustomFabBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<RecyclerView>(R.id.recyclerView)?.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = object : RecyclerView.Adapter<ViewHolder>() {
                private val dataSet = Array<String>(30) { "no.$it" }

                override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
                    val view = LayoutInflater.from(parent.context)
                        .inflate(R.layout.item_layout, parent, false)
                    return ViewHolder(view)
                }

                override fun onBindViewHolder(holder: ViewHolder, position: Int) {
                    holder.textView.text = dataSet[position]
                }

                override fun getItemCount(): Int {
                    return dataSet.size
                }

            }
        }
    }

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.textView)
    }
}

CustomFabBehavior.kt

class CustomFabBehavior(context: Context, attrs: AttributeSet) :
    FloatingActionButton.Behavior(context, attrs) {// CoordinatorLayout.Behavior<FloatingActionButton>(context, attrs)이렇게 사용할수도 있음

    override fun onStartNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: FloatingActionButton,
        directTargetChild: View,
        target: View,
        axes: Int,
        type: Int,
    ): Boolean {
        return axes == View.SCROLL_AXIS_VERTICAL
                || super.onStartNestedScroll(coordinatorLayout,child,directTargetChild,target,axes,type)
    }

    override fun onNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: FloatingActionButton,
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int,
        consumed: IntArray,
    ) {
        super.onNestedScroll(coordinatorLayout,child,target,dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed,type,consumed)
        Log.d("*****", "onNestedScroll: $dxConsumed , ${child.visibility}")
        if (dyConsumed > 0 && child.visibility == View.VISIBLE ) {
            child.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
                override fun onHidden(fab: FloatingActionButton?) {
                    super.onHidden(fab)
                    fab?.visibility = View.INVISIBLE
                }
            })
        } else if (dyConsumed < 0 && child.visibility != View.VISIBLE ) {
            child.show()
        }
    }
}

이렇게 커스텀한 behavior 말고도 안드로이드 자체적으로 지원하는 behavior도 있습니다.
그 대표적인 예가 AppBarLayout.Behavior입니다.

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">
 
    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true">
 
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/app_toolbar"
            android:layout_width="match_parent"
            android:layout_height="?android:attr/actionBarSize"
            app:title="@string/app_name"
            app:titleTextColor="@android:color/white"
            app:layout_scrollFlags="scroll|enterAlways"/>
 
    </com.google.android.material.appbar.AppBarLayout>
 
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/bible_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
 
</androidx.coordinatorlayout.widget.CoordinatorLayout>

이런식으로 스크롤 플래그나 behavior를 지정하여 설정할수 있습니다.


reference
https://developer.android.com/reference/androidx/coordinatorlayout/widget/CoordinatorLayout
https://developer.android.com/reference/androidx/coordinatorlayout/widget/CoordinatorLayout.Behavior
https://velog.io/@pingu244/Android-CoordinatorLayout
https://readystory.tistory.com/127
https://velog.io/@jeep_chief_14/Coordinator-Layout%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%ED%88%B4%EB%B0%94-%EC%88%A8%EA%B8%B0%EA%B8%B0
https://black-jin0427.tistory.com/201

0개의 댓글