[Kotlin/Android] RecyclerView 아이템의 순서 변경하기(드래그 앤 드롭)

djawnstj·2022년 8월 27일
0

Android-ItemTouchHelper

목록 보기
1/2

블로그 이전


RecyclerView의 아이템을 드래그 앤 드롭으로 순서를 변경하는 방법을 알아보겠습니다.

RecyclerView의 아이템을 드래그&드랍 할때 onTouchListener, onInterceptTouchEvent, GestureDetector 등을 통해 이벤트 감지를 할 수 있지만, 안드로이드 RecyclerView 라이브러리에서 지원하는 ItemTuchHelper 클래스를 통해 쉽게 감지할 수 있습니다.

ItemTuchHelper

This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.

It works with a RecyclerView and a Callback class, which configures what type of interactions are enabled and also receives events when user performs these actions.
(ItemTouchHelper)

공식문서에서 설명하는 ItemTouchHelper는 삭제를 위한 스와이프와 드래그&드랍을 지원하는 유틸리티 클래스이며 통해 스와이프와 드래그&드랍 이벤트가 발생하면 개발자가 정의한 콜백이 호출된다고 나와있습니다.

ItemTouchHelper.Callback

실제 아이템이 움직히는 이벤트가 발생했을때 호출할 행동을 정의하는 클래스입니다.

코드

ItemTouchHelperListener

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

ItemTouchHelper를 통해 아이템의 움직임이 감지되면 호출될 리스너를 만듭니다.
아직은 아이템의 드래그&드랍 상황만 구현할것이기 때문에 함수는 onItemMove만 선언해줍니다.
함수의 파라미터로 움직임이 시작한 위치(position)와 끝난 위치를 받습니다.

ItemTouchCallback

class ItemTouchCallback(private val listener: ItemTouchHelperListener): ItemTouchHelper.Callback() {

    /** 드래그 방향과 드래그 이동을 정의하는 함수 */
    override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
        // 드래그 방향
        val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
        // 스와이프 방향
        val swipeFlags = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT

        // 드래그 이동을 만드는 함수
        return makeMovementFlags(dragFlags, swipeFlags)
    }

    /** 아이템이 움직일떼 호출되는 함수 */
    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        listener.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
        return false
    }

    /** 아이템이 스와이프 될때 호출되는 함수 */
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
//        TODO("Not yet implemented")
    }
}

다음은 ItemTouchHelper의 Callback 구현체입니다.

getMovementFlags()

getMovementFlags 함수는 드래그의 방향을 정의하고 움직임을 리턴하는 함수로, makeMovementFlags 함수를 리턴해 아이템의 움직임을 리턴해줍니다.

makeMovementFlags 함수의 첫번째 파라미터로 드래그 방향을 넣어주고, 두번째 파라미터로 스와이프 방향을 넣어줍니다. 이때, 파라미터로 넣은 방향으로만 드래그/스와이프가 됩니다.
저는 상하좌우 모든 방향으로 움직일 수 있도록 설정해두었습니다.

onMove()

아이템이 움직일때 호출되는 함수입니다.

아이템이 움직일때 RecyclerView Adapter에서 화면에 보여지는 순서를 바꿔줘야하기 때문에 위 ItemTouchHelperListener 인터페이스로 선언한 onItemMove() 함수를 호출해줍니다.

onSwiped()

스와이프 이벤트에서 호출되는 함수입니다.

스와이프에 관련된 내용은 추후에 포스팅하겠습니다.

RecyclerViewAdapter

class RecyclerViewAdapter: RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>(), ItemTouchHelperListener {

    private val list = ArrayList<String>()
    
    override fun onItemMove(from: Int, to: Int) {
        val item: String = list[from]
        list.removeAt(from)
        list.add(to, item)
        notifyItemMoved(from, to)
    }
    ...
}

RecyclerView 클래스의 Adapter 클래스 구현체입니다.

기본적인 Adapter 설정과 동일하게 함수를 정의해주면 되고 추가해야하는 부분은 ItemTouchHelperListener 인터페이스를 상속받아야합니다.

onItemMove()

인터페이스에서 선언한 onItemMove() 함수를 재정의해줍니다.
이 부분이 ItemTouchCallback 클래스의 onMove() 함수에서 호출될때 작동되는 코드입니다.

먼저 드래그가 시작되는 위치(from 파라미터)를 이용해 해당 위치의 아이템(드래그 되고있는 아이템)을 변수로 정의해줍니다.

그 후 Adapter의 아이템 배열에서 드래그가 되고있는 아이템을 제거하고, 드래그가 끝나는 시점에 추가해줍니다.

MainActivity


class MainActivity : AppCompatActivity() {

    companion object { private const val TAG = "MainActivity" }

    // 뷰바인딩 변수
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    // 리싸이클러뷰 어댑터에 들어갈 아이템들이 담긴 배열 변수
    private val list by lazy { mutableListOf<String>() }
    // 리싸이클러뷰 어댑터
    private val recyclerViewAdapter by lazy { RecyclerViewAdapter() }
    // 리싸이클러뷰 아이템 이동 콜백 변수
    private val itemTouchHelper by lazy { ItemTouchHelper(ItemTouchCallback(recyclerViewAdapter)) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        initRecyclerView()

    }

    /** 리싸이클러뷰 초기화 함수 */
    private fun initRecyclerView() {
        binding.recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        binding.recyclerView.adapter = recyclerViewAdapter

        // 리싸이클러뷰에 itemTouchHelper 연결
        itemTouchHelper.attachToRecyclerView(binding.recyclerView)
    }
    ...
}

저의 경우 메인액티비티에 RecyclerView를 구현했습니다.

itemTouchHelper

ItemTouchHelper 변수입니다.

인수값으로 Callback 구현체를 할당해줍니다.(Callback 인수값은 ItemTouchHelperListener를 상속받은 adapter입니다.)

initRecyclerView()

RecyclerView 초기화 함수입니다.

LayoutManager를 설정해주고 adapter를 할당해줍니다.

추가적으로 위에 정의한 itemTouchHelper에 attachToRecyclerView() 함수를 통해 RecyclerView와 연결해줍니다.


코드는 이렇게 해주면 끝입니다.
프로젝트 전체 코드는 제 깃허브에 올려두었으니 참고 부탁드립니다.

간단하게 기능을 완성했지만 문제가 있습니다.

문제

RecyclerView를 이용할때 Adapter로 RecyclerView.Adapter와 ListAdapter를 많이 이용합니다.
ListAdapter가 RecyclerView.Adapter와 비교해 가지는 차이점은, notifyDataSetChanged()를 대신해 submitList() 함수를 통해 목록을 새로고침해준다는 점이고, 새로고침될 때 position별 이전 아이템과 비교해 달라진 아이템만 다시 그려주어 메모리를 효율적으로 쓴다는 장점이 있습니다.

하지만 ListAdapter를 이용해 ItemTouchHelper를 구현하면

위와같이 아이템을 드래그 하자마자 에러가 발생합니다.(이미지가 너무 빠르게 넘어가지만.... 에러 메시지가 떴습니다)

실제 저희회사 프로젝트에선 ListAdapter를 사용하였고, 팀원이 ItemTouchHelper를 신규기능으로 추가할때 발생한 문제입니다.

다음 포스팅은 ListAdapter에서 이런 에러가 발생한 원인과 해결방법을 적어보겠습니다.

profile
이용자가 아닌 개발자가 되자!

0개의 댓글