프로그래밍/Android

[안드로이드] BottomSheetBehavior로 차이 카드 앱 처럼 UI 구성하기

Lou Park 2021. 12. 1. 00:53

어? 이거 좋다!

차이카드 UI, 올려서 부스트 적용하기

차이카드에는 올려서 부스트하기 기능이있다. 아래에서부터 핸들을 쭉 땅겨서 부스트가 작동하도록하는 것인데...

플레이오에서 레벨업 페이지와 내역페이지가 리스트로 죽 늘어진 레이아웃을 보고...차이처럼 구성해보는 것은 어떨까 건의드렸다. 성능상의 문제가 있기도 했지만, 작은 폰을 쓰는 유저들은 아래에 내역이 있는지 조차 모를 확률도 있었다. (ㅋㅋㅋ)

 

호기롭게 건의했지만 혼자서 공부하다보니 저런 레이아웃이 어떻게 구현되어있고, 뭐라고 검색해야 할지도 몰랐는데 해답은~ BottomSheet이었다. 그동안 BottomSheet은 BottomSheetDialog로만 이용해봐서 일반 뷰에서 쓰는건 처음이었는데, behavior만 설정해주면 간단히 멋진 레이아웃을 구현할 수 있었다.

 

만들어진 레이아웃 (플레이오)

 

다음 화면과 같은 레이아웃을 구성하기위해서 필요한 대략적인 XML 구조는 다음과 같다.

<CoordinatorLayout>
	<!-- 콘텐츠 -->
	<LinearLayout/>
	<!-- BottomSheet -->
	<CardView 
    app:behavior_hideable="false"
    app:behavior_peekHeight="86dp"
	app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"/>
</CoordinatorLayout>

 


BottomSheetBehavior의 속성들(Attributes)

CoordinatorLayout의 자식이 이런 layout_behavior=BottomSheetBehavior을 가지게 되면 BottomSheet처럼 작동하게 된다. 내가 설정한 속성은 hideable과 peekHight이 있는데, 이 외에도 BottomSheetBehavior과 함께 사용할 수 있는 다양한 속성들이 있다.

 

 

BottomSheetBehavior_Layout_android_maxWidth

BottomSheet의 최대 가로길이 설정

 

BottomSheetBehavior_Layout_behavior_draggable

드래그를 통해서 BottomSheet을 접고 펼칠지 여부, 기본값은 true

 

BottomSheetBehavior_Layout_behavior_expandedOffset

BottomSheet을 완전히 펼쳤을때 상단에 여백을 주고싶을경우, 그만큼의 Offset.

 

 BottomSheetBehavior_Layout_behavior_halfExpandedRatio

BottomSheet의 상태중에서 STATE_HALF_EXPANDED라는 상태가있는데, 뷰가 절반정도 펼쳐졌을때 이 상태값을 가지게 된다.

BottomSheet가 어느정도로 펼쳐졌을때 이 상태가 될지 기준을 정한다. 기본 값은 딱 절반인 0.5다.

 

 BottomSheetBehavior_Layout_behavior_hideable

true일경우, BottomSheet을 아래로 내려 사라지게 할 수 있다. 

 

 BottomSheetBehavior_Layout_behavior_peekHeight

BottomSheet이 접힌 상태일때 높이를 설정한다. 다시 꺼낼수 있을정도의 높이가 되어야하므로 최소 16dp 이상 잡는 것이 UX에 좋을 것이다.

 

 BottomSheetBehavior_Layout_behavior_skipCollapsed

완전히 펼친 상태에서 숨김상태로 변할때, 접힘 상태를 스킵할지 하지않을지 여부다. 기본값은 false.

일반적이라면 펼친후에 BottomSheet를 사라지게 한다면 EXPANDED -> HALF_EXPANDED -> COLLAPSED -> HIDDEN이 되겠지만, 이 속성을 true로 설정한다면 중간에 COLLAPSED 단계가 빠지게 된다.

 

XML 레이아웃

activity_main.xml 의 전체 레이아웃은 다음과 같다.

하지만 이렇게 하더라도, BottomSheet을 펼침에 따라 카드뷰의 곡률이 달라지거나 화살표가 돌아가거나 하지 않는데, 그 부분은 동적으로 구현해볼것이다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:animateLayoutChanges="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

    	<!-- Main content -->    

    </LinearLayout>

    <!-- Bottom sheet -->

    <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/bottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:descendantFocusability="beforeDescendants"
        android:focusableInTouchMode="true"
        android:orientation="vertical"
        android:padding="10dp"
        app:behavior_hideable="false"
        app:behavior_peekHeight="86dp"
        app:cardCornerRadius="36dp"
        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:gravity="center_horizontal">

            <ImageView
                android:id="@+id/guideline1"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:rotation="180"
                android:tint="#aaaaaa"
                android:layout_marginTop="10dp"
                android:src="@drawable/ic_caret"/>

            <com.gna.playio.ui_component.text.MediumTextView
                android:id="@+id/guideline2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:textColor="#aaaaaa"
                android:text="올려서 이용내역 보기"/>

            <FrameLayout
                android:id="@+id/fragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>

        </LinearLayout>

    </androidx.cardview.widget.CardView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

상태에 따라 애니메이션 적용하기

MainActivity.kt의 내용은 다음과 같다.

BottomSheet.from(View)를 통해서 BottomSheetBehavior를 얻어올 수 있고, setBottomSheetCallback으로 BottomSheet의 상태변화를 감지할 수 있게된다. 

 

onStateChanged에서 주는 상태값은 다음과 같다.

 

 STATE_SETTLING: (움직이다가) 안정화되는 중

 STATE_DRAGGING: 드래그하는 중

 STATE_HIDDEN: 숨겨짐

 STATE_COLLAPED: 접힘

 STATE_HALF_EXPANDED: 절반이 펼쳐짐

 STATE_EXPANDED:  완전히 펼쳐짐

 

 

나는 펼쳐졌을때 fragment를 붙여주는 방법으로 위 레이아웃을 구현하였다.

 

*2021년 12월 현재 기준 setBottomSheetCallback은 Deprecated되었으며, 대신에 addBottomSheetCallbackremoveBottomSheetCallback을 사용하면 된다고 합니다.

class MainActivity : AppCompatActivity() {
	
	private lateinit var binding: ActivityMainBinding

	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)


        // 현재 접힌 상태에서의 BottomSheet 귀퉁이의 둥글기 저장
        val cornerRadius = bottomSheet.radius 

        val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)

        bottomSheetBehavior.setBottomSheetCallback(object: BottomSheetBehavior.BottomSheetCallback() {
            override fun onStateChanged(bottomSheet: View, newState: Int) {
            	// 상태가 변함에 따라서 할일들을 적어줍니다.
            	if (newState == STATE_EXPANDED) {
            		// TODO; 내용을 보여주기 위해 fragment 붙이기...
            	}
            }

            override fun onSlide(bottomSheetView: View, slideOffset: Float) {
                // slideOffset 접힘 -> 펼쳐짐: 0.0 ~ 1.0
                if (slideOffset >= 0) {
                	// 둥글기는 펼칠수록 줄어들도록
                    binding.bottomSheet.radius = cornerRadius - (cornerRadius * slideOffset)
                    // 화살표는 완전히 펼치면 180도 돌아가게
                    binding.guideline1.rotation =  (1 - slideOffset) * 180F
                    // 글자는 조금더 빨리 사라지도록 
                    binding.guideline2.alpha = 1 - slideOffset * 2.3F
                    // 내용의 투명도도 같이 조절...
                    binding.fragment.alpha = Math.min(slideOffset * 2F, 1F)
                }
            }
        })
    }

}

 


참고자료

https://developer.android.com/reference/com/google/android/material/bottomsheet/BottomSheetBehavior