프로젝트를 진행하다 아래 사진처럼 하단에 존재하면서 스크롤을 하거나 터치할 때 올라오는 뷰가 필요해 BottomSheet를 찾아보았다.
Bottom Sheet
BottomSheet의 종류로는 Persistent BottomSheet와 Modal BottomSheet 이 있다.
- Persistent는 특정 화면의 레이아웃에 속해있고, Behavior를 설정해 주면 동작시킬 수 있다.
즉, 특정한 컨텐츠에 속해있는 또 다른 화면. - Modal은 안드로이드의 Toast처럼 어디에서나 띄울 수 있다. 프래그먼트(BottomSheetDialogFragment) 이용
Persistent BottomSheet
프로젝트에 사용한 구현 방법으로는 Persistent BottomSheet이다.
CoordinatorLayout의 자식 뷰에 BottomSheetBehavior를 사용해 BottomSheet처럼 작동하게 한다.
BottomSheet으로 사용할 뷰 생성
<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
app:behavior_hideable="false"
app:behavior_peekHeight="32dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/design_default_color_primary"
app:layout_constraintBottom_toBottomOf="@id/bottom_btn" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottom_btn"
android:layout_width="160dp"
android:layout_height="32dp"
android:background="@color/design_default_color_primary"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/icon_title"
android:text="↑"
android:textColor="#ffffff"
android:textStyle="bold"
android:layout_marginEnd="5dp"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/icon_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:letterSpacing="-0.03"
android:text="AndroidTechNote"
android:textColor="#ffffff"
android:textSize="13dp"
android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="168dp"
android:background="#F2F2F2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/bottom_btn">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:text="To Do List"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bottom_sheet_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingHorizontal="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/text" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
BottomSheetHavior 지정
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
BottomSheetBehavior의 속성
- android_maxWidth : Sheet의 최대 가로길이 설정
- behavior_draggable : 드래그를 통해서 Sheet를 접고 펼칠지 여부, default는 true
- behavior_expandedOffset : 완전히 펼쳤을 때 상단의 여백 지정 값
- behavior_halfExpandedRatio : Sheet 상태 중 STATE_HALF_EXPANDED (뷰가 절반 정도 펼쳐졌을 때) 일 때 이 상태 값을 가지게 된다.
Sheet가 어느 정도로 펼쳐졌을 때 이 상태가 될지 기준을 정한다. default는 딱 절반인 0.5 - behavior_hideable : 숨기기 유무 (true | false)
- behavior_peekHeight : 접힌 상태일 때 높이를 설정
- behavior_skipCollapsed : 완전히 펼친 상태에서 숨김 상태로 변할 때, 접힘 상태를 스킵할지 하지 않을지 여부, default는 false, 일반적이라면 펼친 후에 BottomSheet를 사라지게 한다면 EXPANDED -> HALF_EXPANDED -> COLLAPSED -> HIDDEN이 되겠지만, 이 속성 true 설정 시 중간에 COLLAPSED 단계가 빠지게 된다.
사용되는 곳의 Layout에 바로 지정해도 되지만 include로 여러 곳에서 사용하려고 따로 생성해 주었다.
사용할 곳에서 BottomSheet을 감싸는 CoordinatorLayout을 사용하고 include로 지정해 준다.
<?xml version="1.0" encoding="utf-8"?>
<layout 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"
tools:context=".coordinator.BottomSheetActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hellow World!!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/bottom_sheet"
layout="@layout/layout_bottom_sheet" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
지정해 주고 해당 영역을 스크롤해보면 BottomSheet이 생긴 걸 알 수 있다.
(임시로 채운 Todo RecyclerView)
Behavior 상태
BottomSheet.from(View)를 통해서 BottomSheetBehavior를 얻어올 수 있고, assBottomSheetCallback으로 상태변화를 감지할 수 있게 된다.
- onStateChanged에서 주는 상태 값은 아래와 같다.
- STATE_COLLAPSED : 접힘
- STATE_EXPANDED : 펼쳐짐
- STATE_HIDDEN : 숨겨짐
- STATE_HALF_EXPANDED : 절반 펼쳐짐
- STATE_DRAGGING : 드래그하는 중
- STATE_SETTLING : (움직이다가) 안정화되는 중
- onSlide에서는 슬라이드 될 때의 offset 값이 오게 된다
- Hide -1.0 ~ Collapsed 0.0 ~ Expended 1.0
각 State 값과 onSlide에서 offset 값에 따른 처리를 하면 된다.
나 같은 경우 접혔을 때와 펼쳤을 때 Text 변경과 offset 값을 이용해 뷰의rotation, alpha, translate 를 조작하여 슬라이드 될 때 애니메이션 효과를 주고 View에 ClickListener를 주어 Behavior의 상태값을 변경해 클릭으로 펼치고 접을 수 있게 해 보았다.
val bottomBehavior = BottomSheetBehavior.from(binding.bottomInclude.root)
//Behavior 속성 programmatically
bottomBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
bottomBehavior.peekHeight = 32.dp
bottomBehavior.isHideable = false
bottomBehavior.apply {
addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
when(newState){
BottomSheetBehavior.STATE_COLLAPSED -> { //접힘
binding.bottomInclude.iconTitle.text = "OPEN"
}
BottomSheetBehavior.STATE_EXPANDED -> { //펼쳐짐
binding.bottomInclude.iconTitle.text = "CLOSE"
}
BottomSheetBehavior.STATE_HIDDEN -> {} //숨겨짐
BottomSheetBehavior.STATE_HALF_EXPANDED -> {} //절반 펼쳐짐
BottomSheetBehavior.STATE_DRAGGING -> {} //드래그하는 중
BottomSheetBehavior.STATE_SETTLING -> {} //(움직이다가) 안정화되는 중
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
//슬라이드 될때 offset / hide -1.0 ~ collapsed 0.0 ~ expended 1.0
binding.bottomInclude.apply {
icon.rotation = slideOffset * -180F
bottomSheetRecycler.animate().x(bottomSheetRecycler.width * (1 - slideOffset)).setDuration(0).start()
bottomSheetRecycler.alpha = slideOffset
}
}
})
}
binding.bottomInclude.bottomBtn.setOnClickListener {
bottomBehavior.state = if (bottomBehavior.state == BottomSheetBehavior.STATE_COLLAPSED){
BottomSheetBehavior.STATE_EXPANDED
} else
BottomSheetBehavior.STATE_COLLAPSED
}
BottomSheetBehavior 내부동작 설명
BottomSheetBehavior 파헤치기
CoordinatorLayout에서 layout_behavior를 BottomSheetBehavior로 설정해 준 경우 어떻게 동작하는지 알아봅니다.
hongbeomi.medium.com
기본적으로 BottomSheetBehavior는 CoordinatorLayout 클래스 내부의 Behavior라는 추상클래스를 상속받고 있다.
그래서 CoordinatorLayout에서 자식 뷰에 app:layout_behavior라는 속성을 지정해 줌으로써 상호작용할 수 있는 것.
또한 뷰가 접힌 상태에서 레이아웃 인스펙터로 살펴보면 다음과 같은 모습을 하고 있다.
BottomSheetBehavior로 설정된 뷰는 현재 보이지 않는 상태지만 이미 그려져 있는 상태인 것을 알 수 있다.
프로젝트에 이용한 결과물
Modal BottomSheet
일반 DIalog와 Fragment Dialog 두 가지 형태로 구현가능
1. Dialog 구현
val dialogView = layoutInflater.inflate(R.layout.layout_bottom_sheet_fragment, null)
BottomSheetDialog(this@BottomSheetActivity).apply {
setContentView(dialogView)
show()
}
2.FragmentDialog 구현
Fragment로 사용하려면 BottomSheetDialogFragment Class를 생성
class BottomSheetFragment : BottomSheetDialogFragment(){
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
//return super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.layout_bottom_sheet_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
val modalBottomSheet = BottomSheetFragment()
modalBottomSheet.show(supportFragmentManager, modalBottomSheet.tag)
BottomSheet
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=mym0404&logNo=221366306772
https://jizard.tistory.com/312
https://thdev.tech/androiddev/2016/12/11/Android-BottomSheet-Intro/
https://medium.com/android-bits/android-bottom-sheet-30284293f066
'Android > Reference' 카테고리의 다른 글
Notification (0) | 2023.12.09 |
---|---|
DataStore / Preference, Proto (0) | 2023.12.04 |
RecyclerView Item에 Animation 주기 (0) | 2022.04.18 |
ItemDecoration / RecyclerView Item 간격 조정 (0) | 2022.04.10 |
LayoutParam Programmatically (0) | 2022.04.06 |