위와 같은 느낌으로 생각하면 된다
<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">
<data>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 하단 삭제 view 영역 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/erase_item_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="0.3"
android:background="#CCD4FF">
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 상단 아이템 view 영역 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/swipe_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/gl_v_4.44"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.0444" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/gl_v_95.56"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.9556" />
<View
android:id="@+id/empty_view_1"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="360:15"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_image"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/empty_view_2"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="@id/gl_v_4.44"
app:layout_constraintTop_toBottomOf="@id/empty_view_1"
app:layout_constraintWidth_percent="0.25"
tools:src="@drawable/ic_launcher_background" />
<TextView
android:id="@+id/tv_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@id/empty_view_2"
app:layout_constraintStart_toEndOf="@id/iv_image"
app:layout_constraintTop_toBottomOf="@id/empty_view_1"
tools:text="RecyclerView Item 1" />
<View
android:id="@+id/empty_view_2"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="360:15"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<View
android:id="@+id/view_line"
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:background="@color/black"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</layout>
ItemTouchHelper는 RecyclerView에 삭제를 위한 스와이프 및 드래그 앤 드롭 지원을 지원하는 유틸리티 클래스입니다. ItemTouchHelper는 사용자가 액션을 수행할 때 이벤트를 수신하는 RecyclerView 및 이벤트에 반응하는 콜백 메소드가 선언되어 있는 Callback 클래스와 함께 사용합니다.
즉, 구조적으로 RecyclerView와 ItemTouchHelper.Callback을 ItemTouchHelper가 연결시켜주는 것입니다.
(참고링크 : https://velog.io/@changhee09/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-ItemTouchHelper)
class SwipeHelper: ItemTouchHelper.Callback() {
override fun getMovementFlags( // 이동 방향을 결정!!
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
// Drag와 Swipe 방향을 결정 Drag는 사용하지 않아 0, Swipe의 경우는 오른쪽에서 왼쪽으로만 가능하게 설정, 양방향 모두 하고 싶다면 'ItemTouchHelper.LEFT or ItemTouchHelper.Right'
return makeMovementFlags(0, ItemTouchHelper.LEFT)
}
override fun onMove( // Drag 시 호출
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { // Swipe 시 호출
}
...
}
// ItemTouchHelper.Callback 을 리사이클러뷰와 연결
val swipeHelper = SwipeHelper() // ItemTouchHelper.Callback 구현 클래스
val itemTouchHelper = ItemTouchHelper(swipeHelper)
itemTouchHelper.attachToRecyclerView(binding.rvData) // rvData = 리사이클러뷰 id
Utility class for ItemTouchHelper which handles item transformations for different API versions.
This class has methods that map to ItemTouchHelper.Callback's drawing methods. Default implementations in ItemTouchHelper.Callback call these methods with RecyclerView.ViewHolder.itemView and ItemTouchUIUtil makes necessary changes on the View depending on the API level. You can access the instance of ItemTouchUIUtil via ItemTouchHelper.Callback.getDefaultUIUtil() and call its methods with the children of ViewHolder that you want to apply default effects.
https://developer.android.com/reference/androidx/recyclerview/widget/ItemTouchUIUtil
공식문서를 참고해보니, 뷰홀더의 아이템뷰 전체가 아닌 일부에 ItemTouchHelper에서 제공하는 효과?를 적용하는 경우 ItemTouchUIUtil을 사용하는 것 같다
뷰홀더의 아이템뷰에서 상단에 배치된 뷰만 스와이프 시킬 것이므로 ItemTouchUIUtil을 사용해보자!
class SwipeHelper: ItemTouchHelper.Callback() {
...
// Called by the ItemTouchHelper when the user interaction with an element is over and it also completed its animation.
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
//super.clearView(recyclerView, viewHolder)
getDefaultUIUtil().clearView(getView(viewHolder))
}
// Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed =
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
//super.onSelectedChanged(viewHolder, actionState)
viewHolder?.let {
getDefaultUIUtil().onSelected(getView(it))
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
//super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE){
val view = getView(viewHolder)
getDefaultUIUtil().onDraw(
c,
recyclerView,
view,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
private fun getView(viewHolder: RecyclerView.ViewHolder): View {
return (viewHolder as RvAdapter.MyDataViewHolder).itemView.findViewById(R.id.swipe_view) // 아이템뷰에서 스와이프 영역에 해당하는 뷰 가져오기
}
}
class SwipeHelper: ItemTouchHelper.Callback(){
...
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
//return super.getSwipeEscapeVelocity(defaultValue)
return defaultValue * 20
}
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
//return super.getSwipeThreshold(viewHolder)
return 2f
}
}
class SwipeHelper: ItemTouchHelper.Callback() { // ItemTouchHelper.Callback 을 구현해야 한다
private var currentPosition: Int? = null
private var previousPosition: Int? = null
private var currentDx = 0f
private var clamp = 0f
override fun getMovementFlags( // 이동 방향을 결정!!, 스와이프 시 항상 onChildDraw 보다 먼저 호출!
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
Log.d("AppTest", "getMovementFlags")
val view = getView(viewHolder)
clamp = view.width.toFloat() / 10 * 3 // 아이템뷰 가로 길이의 비율로 clamp 설정도 가능!!!!
// Drag와 Swipe 방향을 결정 Drag는 사용하지 않아 0, Swipe의 경우는 오른쪽에서 왼쪽으로만 가능하게 설정, 양방향 모두 하고 싶다면 'ItemTouchHelper.LEFT or ItemTouchHelper.Right'
return makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
}
override fun onMove( // Drag 시 호출
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { // Swipe 시 호출
}
// Called by the ItemTouchHelper when the user interaction with an element is over and it also completed its animation.
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
//super.clearView(recyclerView, viewHolder)
currentDx = 0f
getDefaultUIUtil().clearView(getView(viewHolder))
previousPosition = viewHolder.adapterPosition
}
// Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed =
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
//super.onSelectedChanged(viewHolder, actionState)
viewHolder?.let {
currentPosition = viewHolder.adapterPosition // 현재 스와이프 한 아이템 위치
getDefaultUIUtil().onSelected(getView(it))
}
}
override fun onChildDraw( // 스와이프 동작 시 호출
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float, // 스와이프를 시작한 터치 지점에서 얼만큼 좌우로 움직였는지! (왼쪽 = 음수, 오른쪽 = 양수)
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
//super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE){
Log.d("AppTest", "onChildDraw")
val view = getView(viewHolder)
val isClamped = getClamped(viewHolder as RvAdapter.MyDataViewHolder)
//val isClamped =getTag(viewHolder)
val x = clampViewPositionHorizontal(view, dX, isClamped, isCurrentlyActive)
//Log.d("AppTest", "dX : ${dX}, dY : ${dY}")
currentDx = x
Log.d("AppTest", "x : ${x}")
getDefaultUIUtil().onDraw(
c,
recyclerView,
view,
x, // dX 가 아닌 x 로 설정해야함!!!, x만큼 왼쪽으로 스와이프 뷰를 밀어서 그린다!
dY,
actionState,
isCurrentlyActive
)
}
}
private fun clampViewPositionHorizontal(
view: View,
dX: Float, //
isClamped: Boolean,
isCurrentlyActive: Boolean // 스와이프 중인지 , 손 떼면 false 된다
) : Float {
// View의 가로 길이의 30% 만 스와이프 되도록
val maxSwipe: Float = -view.width.toFloat() / 10 * 3 // 음수 값!!, xml 상에서 삭제 영역이 아이템뷰 width의 0.3 만큼 차지하도록 설정한 것과 맞추기 위함
// RIGHT 방향으로 swipe 막기
val right: Float = 0f
val x = if (isClamped) {
// View가 고정되었을 때 swipe되는 영역 제한
if (isCurrentlyActive) dX - clamp else -clamp
// 스와이프 된 고정 상태에서 dX - clamp 가 maxSwipe 이하인 상태에서 터치 유지 해제(왼쪽 스와이프 시도 중) -> maxSwipe 리턴
// 스와이프 된 고정 상태에서 dx - clamp 가 maxSwipe 이상인 상태에서 터치 유지 해제(오른쪽 스와이프 시도 중) -> isClamped 에 false 값이 들어올 것임 -> currentDx가 -clamp 보다 커진다!
} else {
dX // maxSwipe보다 더 많이 왼쪽으로 스와이프 해도 maxSwipe 까지만 스와이프 된다(이때 isClamped는 true 된다) / 밑에서 maxSwipe & x 중에 큰 값을 사용하기 때문
}
return min(max(maxSwipe, x), right) // right = 항상 0 이므로 min 함수에서 최대는 항상 0 값이 나온다 -> 스와이프를 통해 오른쪽으로 밀리는 일은 없다!!
}
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
//return super.getSwipeEscapeVelocity(defaultValue)
return defaultValue * 10
}
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { // 터치 유지하다가 떼면 호출됨
//return super.getSwipeThreshold(viewHolder)
Log.d("AppTest", "getSwipeThreshold")
//val isClamped = getClamped(viewHolder as RvAdapter.MyDataViewHolder)
//val isClamped =getTag(viewHolder)
// 현재 View가 고정되어있지 않고 사용자가 -clamp 이상 swipe시 isClamped true로 변경 아닐시 false로 변경 처리 할 것!!!
Log.d("AppTest", "isClamped = ${currentDx <= -clamp}")
//setTag(viewHolder, currentDx <= -clamp) // 스와이프 되고 오른쪽 스와이프 시에만 닫히도록 하게하기 위해 '!isClamped && ' 조건 제거
setClamped(viewHolder as RvAdapter.MyDataViewHolder, currentDx <= -clamp)
return 2f
}
private fun getView(viewHolder: RecyclerView.ViewHolder): View {
return (viewHolder as RvAdapter.MyDataViewHolder).itemView.findViewById(R.id.swipe_view) // 아이템뷰에서 스와이프 영역에 해당하는 뷰 가져오기
}
private fun setClamped(viewHolder: RvAdapter.MyDataViewHolder, isClamped: Boolean){
viewHolder.setClamped(isClamped)
}
private fun getClamped(viewHolder: RvAdapter.MyDataViewHolder): Boolean{
return viewHolder.getClamped()
}
fun setClamp(clamp: Float) { // activity or fragment에서 clamp 값을 설정할 수도 있다
this.clamp = clamp
}
// 스와이프가 되었는지를 tag 값으로 판단했으나, 뷰홀더 재활용 과정에서 혼란이 발생할 수 있음 -> 리사이클러뷰 데이터 클래스에 swipe가 되었는지를 판단하는 data 추가
private fun setTag(viewHolder: RecyclerView.ViewHolder, isClamped: Boolean) {
// isClamped를 view의 tag로 관리
viewHolder.itemView.tag = isClamped
}
private fun getTag(viewHolder: RecyclerView.ViewHolder) : Boolean {
// isClamped를 view의 tag로 관리
return viewHolder.itemView.tag as? Boolean ?: false
}
/////////////////////////////////////////////////////////////////////////////////
fun removePreviousClamp(recyclerView: RecyclerView) {
if (currentPosition == previousPosition)
return
previousPosition?.let {
val viewHolder = recyclerView.findViewHolderForAdapterPosition(it) ?: return
getView(viewHolder).translationX = 0f
setTag(viewHolder, false)
previousPosition = null
}
}
}
// 스와이프를 하던 손 or 마우스를 떼는 순간 getSwipeThreshold 호출되며, 여기서 isClamped 가 true 가 될지, false가 될 지를 정한다!!!
setTag 함수와 getTag함수의 tag 관리 대신 recyclerView에 들어가는 dataClass 값에 isSwiped라는 boolean값을 넣어준 뒤에 adpater를 helperCallBack클래스에 생성자로 받아서 tag를 adapter.currentList[viewHodler.layoutPosition].isSwiped로 바꾸어 사용해도 괜찮을까요? 혹시 좋은 방법이 있다면 추가 부탁드립니다!