어? 이거 좋다!
차이카드에는 올려서 부스트하기 기능이있다. 아래에서부터 핸들을 쭉 땅겨서 부스트가 작동하도록하는 것인데...
플레이오에서 레벨업 페이지와 내역페이지가 리스트로 죽 늘어진 레이아웃을 보고...차이처럼 구성해보는 것은 어떨까 건의드렸다. 성능상의 문제가 있기도 했지만, 작은 폰을 쓰는 유저들은 아래에 내역이 있는지 조차 모를 확률도 있었다. (ㅋㅋㅋ)
호기롭게 건의했지만 혼자서 공부하다보니 저런 레이아웃이 어떻게 구현되어있고, 뭐라고 검색해야 할지도 몰랐는데 해답은~ 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되었으며, 대신에 addBottomSheetCallback과 removeBottomSheetCallback을 사용하면 된다고 합니다.
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
'프로그래밍 > Android' 카테고리의 다른 글
[Android Studio] 범블비 Network Inspector 인코딩 깨짐 해결방법 (0) | 2022.03.05 |
---|---|
[Android] 안드로이드로 게임을 만들어 보았다 (4) | 2022.02.04 |
adb에서 쉽게 딥링크(Deeplink) 열기 / 인텐트(Intent) 전송 (0) | 2021.11.24 |
[안드로이드] VideoView 소리없이 비디오 재생하기 (0) | 2021.11.11 |
[Okhttp3] Expected URL scheme 'http' or 'https' but no colon was found 해결방법 (0) | 2021.10.26 |