Android

RecyclerView Drag & Drop

그란. 2023. 5. 18. 12:29
ItemTouchHelper.Callback

이용하여 아이템 드래그&드랍을 구현하려고 한다.

이때 실무에 적용하기 위해 ListAdapter, 뷰모델과 연동하여 리스트 싱크까지 맞춘 방법으로 정리


일단 기본 방법으로 구현하면 LongClick 일때 드래그가 시작된다. (ItemTouchHelper.Callback 내부 구현)

class ItemTouchHelperCallback(
    private val moveListener: ItemMoveListener
) : ItemTouchHelper.Callback(
) {

    override fun getMovementFlags(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder
    ): Int {
        return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
    }

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        moveListener.onItemMove(viewHolder.absoluteAdapterPosition, target.absoluteAdapterPosition)
        return true
    }

    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        super.onSelectedChanged(viewHolder, actionState)
        when (actionState) {
            ItemTouchHelper.ACTION_STATE_IDLE -> moveListener.onStopDrag()
        }
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}

}

interface ItemMoveListener {
    fun onItemMove(from: Int, to: Int)
    fun onStopDrag()
}

interface ItemDragListener {
    fun onStopDrag(list: List<UiModel>)
}

data class UiModel(
  val name:String
)

 

 

인터페이스를 fun onStopDrag(list:List<UiModel>) 로 정의 했는데  ( 리사이클러뷰 모델 )

공통 유틸로 사용하고 싶으면 UiModel data 클래스에 다른 Interface ( ex : Differable )을 상속받아 추상화할 수 있다. 

data class UiModel(
  val name:String
):Differable

inteface Differable

interface ItemDragListener {
    fun onStopDrag(list: List<Differable>)
}

 

class MyAdapter(
    val listener: ItemDragListener
) ListAdapter , ItemMoveListener {

    override fun onStopDrag() {
        listener.onStopDrag(currentList)
    }

    override fun onItemMove(from: Int, to: Int) {
        val current = currentList[from]
        submitList(currentList.toMutableList().apply {
            removeAt(from)
            add(to, current)
        })
    }
 }

 

코드만 봐선 뭐가 뭔지 모르니 도식으로 정리

 

ItemTouchHelperCallback     <------>     Adpater <------->    Activity

                                            ItemMoveListener     ItemDragListener

 

 

1. 일단 ItemTouchHelperCallback 을 리사이클러뷰에 연결하는 순간 드래그앤 드랍을 할 수 있게 됨

itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback(myAdapter))
itemTouchHelper.attachToRecyclerView(rv)

2. 그리고 ItemTouchHelper.Callback 에서 발생하는 이벤트를 어댑터에 전달하려고 함 ( ItemMoveListener )

 

이벤트의 종류가 2가지인 이유 ( onStopDrag, onItemMove )

결국 "드래그해서 순서가 변경 되었다" 라는 사실을 어댑터에 알려주면 되는데 

 

아래의 메소드는 움직이고 있을때 (드롭하지 않고 드래그 상태) 계속 불리게 된다.

  override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        moveListener.onItemMove(viewHolder.absoluteAdapterPosition, target.absoluteAdapterPosition)
        return true
    }

그래서 IDLE때 List를 전달하기 위함.

override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        super.onSelectedChanged(viewHolder, actionState)
        when (actionState) {
            ItemTouchHelper.ACTION_STATE_IDLE -> moveListener.onStopDrag()
        }
    }

onItemMove -> 어댑터의 리스트 변화 

onStopDrag -> 뷰모델에 알려줘 싱크 맞추기 위함 ( 아래와 같이 )

 

 

3. 그리고 액티비티에서 ViewModel 의 리스트에 변경된 리스트를 전달하여 싱크를 맞추면 된다 

override fun onStopDrag(list: List<Differable>) {
    viewModel.updateState {
        copy(
            list = list as List<UiModel>
        )
    }
}

 

다시한번 한 문구로 정리하자면

ItemTouchHelper.Callback을 통해 아이템이 이동되었다는 사실을 어댑터에 알리고,

액티비티에(뷰모델) 알려서 서로 싱크를 맞추는 과정


위와 같이 하면 설명했듯이 LongClick일때 동작한다.

특정뷰를 터치했을때 드래그할 수 있도록 수정

ItemDragListener에

fun onStartDrag(viewHolder:RecyclerView.ViewHolder)

을 추가한다 ( 내가 원하는 시점에 startDrag를 시작하도록 ItemTouchHelper.Callback 에게 알려주기 위함 ) 

 

ItemTouchHelperCallback에 아래와 같이 롱클릭을 막고

override fun isLongPressDragEnabled(): Boolean {
    return false
}

어댑터에서 특정뷰에 아래의 리스너를 연결

ivDrag.setOnTouchListener { v, event ->
    if (event.action == MotionEvent.ACTION_DOWN) {
        listener.onStartDrag(viewHolder)
    }
    false
}

 

그리고 액티비티에서 ItemTouchHelper객체에 startDrag(viewHolder)를 전달 

MyAdapter(object : ItemDragListener {
    override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) {
        itemTouchHelper.startDrag(viewHolder)
    }

    override fun onStopDrag(list: List<Differable>) {
        viewModel.updateState {
            copy(
                list = list as List<MyUiModel>
            )
        }
    }
}

 

위의 방법은 지금까지 해왔던것과 반대로 어댑터에서 ItemTouchHelper에 이벤트를 전달하는 과정이다.


 

다른 블로그에서는 참고 소스를 복붙하여 적혀있어 알아보기가 힘들었는데 내가 이해할수 있는 방식대로 재정리하였다.