Android/RecyclerView

[Android] RecyclerView Drag and Drop 구현하기

O_Gyong 2022. 11. 18.

RecyclerView에서 Drag and Drop을 사용하려면 ItemTouchHelper를 사용해야 한다.

ItemTouchHelper는 RecyclerView에 Drag and Drop과 Swipe를 지원하는 유틸리티 클래스다.

 

ItemTouchHelper 객체를 RecyclerView에 연결하고 interface에 정의한 drag 관련 메서드들을 override하여 Drag and Drop을 할 수 있다.


Interface 정의

// Adapter에서 사용할 interface
interface ItemMoveListener {
    // Drag 처리를 위한 메서드
    fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
    // Drop 처리를 위한 메서드
    fun onDropAdapter()
}

// Activity에서 사용할 interface
interface ItemStartDragListener {
    // Drop이 됐을 때 Activity에서 사용할 메서드
    fun onDropActivity(initList : ArrayList<SampleData>, changeList: ArrayList<SampleData>)
}

Drag를 시도할 때 Adapter에서 어떤 아이템이 이동됐는지 알아야하기 때문에 onItemMove에 fromPosition과 toPosition을 넣었다.

 

ItemStartDragListener에서는 초기 리스트와 변화된 리스트를 비교하려고 인자로 넣었다.


ItemTouchHelper.Callback()을 반환하는 클래스 작성

/**
 * ItemTouchHelper는 RecyclerView에 Swipe와 Drag and Drop을 지원하는 유틸리티 클래스
 */
class RecyclerViewItemTouchHelperCallback(private val moveListener: ItemMoveListener) : ItemTouchHelper.Callback() {

    /**
     *  동작방식 구현(동작방향)
     */
    override fun getMovementFlags(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder
    ): Int {
        val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
        return makeMovementFlags(dragFlags, 0)
    }

    /**
     * Item이 위 아래로 움직일 때의 동작 구현
     * - ItemTouchHelper가 드래그된 항목을 이전 위치에서 새 위치로 이동하려고 할 때 호출
     */
    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        moveListener.onItemMove(viewHolder.adapterPosition, target.adapterPosition) // Adapter에 전달
        return true
    }

    /**
     * ItemTouchHelper로 Swipe 또는 Drag and Drop하여 ViewHolder가 변경될 때 호출
     */
    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        super.onSelectedChanged(viewHolder, actionState)
        when(actionState){
            // 드래그 또는 스와이프가 끝났을 때 ACTION_STATE_IDLE가 전달 됨.
            ItemTouchHelper.ACTION_STATE_IDLE -> moveListener.onDropAdapter() // Adapter에 전달
        }
    }

    /**
     * Item이 옆으로 움직일 때의 동작 구현
     */
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        TODO("Not yet implemented")
    }
}

ItemTouchHelper.Callback()을 반환하는 클래스에서 getMovementFlags, onMove, onSwiped, onSelectedChanged를 재정의한다. 

 

getMovementFlags는 Drag and Drop이나 Swipe를 사용할 것인가에 대한 동작방식을 선언하는 곳이다.

Drag and Drop만 사용할 것이기 때문에 Drag 플래그를(ItemTouchHelper.UP or ItemTouchHelper.DOWN) 반환해준다.

 

onMove는 Item이 움직이는게 감지되면 호출되는데 여기서 이동한 포지션을 알 수 있다.

Interface에 정의했던 onItemMove를 사용하여 Adapter에서 호출하도록 해야한다.

 

onSelectedChanged는 Drag and Drop을 하여 ViewHolder가 변경되었을 때 호출된다.

마찬가지로 Interface에 정의했던 onDropAdapter를 사용하여 Adapter에서 호출하도록 한다.


Adapter에서 ItemMoveListener 상속받기

class MainListAdapter :
        RecyclerView.Adapter<MainListAdapter.ViewHolder>(),
        ItemMoveListener {

    private var mSampleList: ArrayList<SampleData> = ArrayList()
    private var onItemDragListener: ItemStartDragListener? = null
    var initList: ArrayList<SampleData> = ArrayList()

    /**
     * Activity에서 호출할 메서드
     */
    fun itemDragListener(startDrag: ItemStartDragListener) {
        this.onItemDragListener = startDrag
    }

    /**
     * Drag and Drop하여 ViewHolder가 변경될 때 호출
     */
    override fun onDropAdapter() {
        onItemDragListener?.onDropActivity(initList, mSampleList) // Activity에 전달
    }

    /**
     * Item이 바뀌면 리스트에 적용
     */
    override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
        Collections.swap(mSampleList, fromPosition, toPosition)
        notifyItemMoved(fromPosition, toPosition)
        return true
    }
    
    /**
     * 생략
     */
}

 

onItemMove로 Item이 움직이는게 감지되면 Collections.swap을 통해 리스트를 바꿔준다.

 

onDrapAdapter로 ViewHolder가 변경된 것을 확인하여 interface에 정의했던 onDropActivity를 사용하여 Activity에서 호출하도록 한다.


MainActivity

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    mBinding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(mBinding.root)

    mAdapter = MainListAdapter()

    val mCallback = RecyclerViewItemTouchHelperCallback(mAdapter)
    mItemTouchHelper = ItemTouchHelper(mCallback)
    mItemTouchHelper?.attachToRecyclerView(mBinding.rvMain) // ItemTouchHelper를 RecyclerView에 연결

    mAdapter.setData(sampleList)
    mBinding.rvMain.adapter = mAdapter
    mBinding.rvMain.layoutManager = LinearLayoutManager(this)

    mAdapter.itemDragListener(object : ItemStartDragListener{
        override fun onDropActivity(
            initList: ArrayList<SampleData>,
            changeList: ArrayList<SampleData>
        ) {
            // TODO : 드랍됐을 때 처리
            println(initList) // 최초 리스트
            println(changeList) // Drag and Drop 이후 리스트
            println("------ \n")

        }
    })
}

attachToRecyclerView를 통해서 ItemTouchHelper 객체를 RecyclerView에 연결시키고,

onDropActivity를 override하여 Drop 됐을 때의 결과를 처리할 수 있다.


전체 코드

댓글