[Android] Jitpack으로 라이브러리 배포 - 1
https://github.com/tentenacy/DragHandleBottomSheet
DragHandleBottomSheet의 필요성
사이드 프로젝트 개발 중 Bottom Sheet 안에서 스크롤을 해야 하는 상황에 직면했다.
하지만 Bottom Sheet 컴포넌트 자체가 스크롤 기능을 가지고 있기 때문에,
Bottom Sheet 안에서 스크롤을 하는 거 자체가 중첩 스크롤인 상황이다.
그렇다면 NestedScrollView
를 사용하여 중첩 스크롤을 구현하면 되지만,
나는 추가적으로, Bottom Sheet의 바디가 아닌 드래그 핸들을 통해서만 스크롤을 하고 싶었기에,
DragHandleBottomSheet라는 Custom Bottom Sheet을 개발하기로 했다.
참고로, 내 예전 NestedScrollView
사용 경험으로는 예기치 못한 스크롤 동작이 동반되는 경우가 조금 있었다.
DragHandleBottomSheet은 Bottom Sheet을 Not draggable한 상태로 만들어 드래그 핸들을 통해서 직접 Bottom Sheet의 위치를 애니메이션과 함께 컨트롤하기 때문에 그 안에 있는 뷰의 기본적인 스크롤 동작을 방해하지 않는다.
구현
Not draggable BSD
앞서 언급했듯이, Bottom Sheet Dialog(BSD)의 드래그 핸들을 통해서만 위치를 컨트롤할 것이기 때문에 Not draggable한 상태로 만든다.
SetOnTouchListener
그리고 드래그 핸들에 OnTouchListener
를 설정하여 터치 이벤트를 감지한다. 이 때, 터치 좌표가 event
를 통해 전달될 것이다.
event.action
은 총 세 가지 상태만 사용할 것이다. ACTION_DOWN
, ACTION_MOVE
, ACTION_UP
이다.
MotionEvent.ACTION_DOWN
: 터치 이벤트 시작 시 최초 발생MotionEvent.ACTION_MOVE
: 터치하는 동안 발생MotionEvent.ACTION_UP
: 터치 이벤트 종료 시 발생
BSD의 드래그 동작은 ACTION_MOVE
에서 수행되고, BSD의 EXPANDED 혹은 HIDDEN 동작은 ACTION_UP
에서 수행된다.
드래그 동작
드래그 동작은 BSD의 y
가 최소 지점과 최대 지점 안에서만 수행되게 구현하면 된다.
이는 마지막으로 터치한 y좌표를 저장하는 lastTouchY
라는 변수를 설정하여, 변화율만큼 계속 BSD의 y좌표를 갱신한다.
STATE_EXPANDED or STATE_HIDDEN
기본 BSD의 EXPANDED 혹은 HIDDEN 동작은 하향 드래그 시 기준 지점을 벗어나지 못하면 BSD가 다시 올라오고, 기준 지점을 벗어나면 내려간다.
이 동작을 구현하기 위해서는 세 가지 준비물이 필요하다. BSD의 top
과 y
, BSD 내부 컨테이너의 높이이다.
BSD의 시작 위치 top
에서 컨테이너의 높이의 특정 비율만큼을 기준 지점으로 설정하고, y
가 이 지점을 벗어나는지 못 벗어나는지를 구현하는 것이다.
EXPANDED 애니메이션 직접 구현
BSD를 EXPANDED 혹은 HIDDEN 상태로 전환할 때 BottomSheetBehavior에서 제공하는 STATE_EXPANDED
와 STATE_HIDDEN
을 사용한다.
하지만, STATE_EXPANDED
는 내부 구현이 외부에서 y좌표를 직접 컨트롤하는 경우 정상적으로 동작이 수행되지 않게 되어 있어서, 직접 애니메이션을 줄 수밖에 없었다.
기본 BSD의 EXPANDED 애니메이션을 관찰한 결과, Interpolator는 DecelerateInterpolator
를 사용하는 거 같았다. 그래서 매개변수를 적절하게 조절하여 EXPANDED 애니메이션을 구현했다.
소스코드(Kotlin)
package com.tenutz.kiosksim.ui.base
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import androidx.databinding.ViewDataBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.tenutz.kiosksim.R
abstract class HandleDraggableBottomSheetDialogFragment<VB : ViewDataBinding>(layoutId: Int) :
BaseBottomSheetDialogFragment<VB>(layoutId) {
companion object {
private const val SCROLL_VERTICAL_RATIO = 0.45
}
private var lastTouchY: Float = 0f
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
makeDraggableByHandle(view)
}
@SuppressLint("ClickableViewAccessibility")
private fun makeDraggableByHandle(view: View) {
//common resource id
val dragHandle = view.findViewById<View>(R.id.constraint_drag_handle_container)
bottomSheetBehavior.isDraggable = false
dragHandle.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastTouchY = event.rawY
true
}
MotionEvent.ACTION_MOVE -> {
val deltaY = event.rawY - lastTouchY
val maxY = binding.root.height.toFloat() + bottomSheet.top.toFloat()
val minY = bottomSheet.top.toFloat()
if (bottomSheet.y + deltaY in minY..maxY) {
bottomSheet.y += deltaY
}
lastTouchY = event.rawY
true
}
MotionEvent.ACTION_UP -> {
if (bottomSheet.y < binding.root.height.toFloat() * (1 - SCROLL_VERTICAL_RATIO) + bottomSheet.top) {
ObjectAnimator.ofFloat(bottomSheet, "translationY", 0f).apply {
interpolator = DecelerateInterpolator(2f)
duration = 300
start()
}
} else {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}
true
}
else -> false
}
}
}
}
라이브러리 배포
DragHandleBottomSheet을 라이브러리로 만들어 배포하면 유용하게 쓰일 거 같았다.
한 번도 배포해본 적 없지만, 이번 기회로 한 번 해보려고 한다.
라이브러리 배포에 관한 자세한 내용은 다음 글에서 계속 작성하겠다.