[안드로이드] ItemTouchHelper

hee09·2021년 12월 20일
0
post-thumbnail
post-custom-banner

ItemTouchHelper 개념

ItemTouchHelper는 RecyclerView에 삭제를 위한 스와이프 및 드래그 앤 드롭 지원을 지원하는 유틸리티 클래스입니다. ItemTouchHelper는 사용자가 액션을 수행할 때 이벤트를 수신하는 RecyclerView 및 이벤트에 반응하는 콜백 메소드가 선언되어 있는 Callback 클래스와 함께 사용합니다.

즉, 구조적으로 RecyclerView와 ItemTouchHelper.Callback을 ItemTouchHelper가 연결시켜주는 것입니다.

주요 메소드

  • attachToRecyclerView(RecyclerView recyclerView)
    ItemTouchHelper를 제공된 RecyclerView에 붙입니다.

ItemTouchHelper.Callback 개념

이 클래스는 앱과 ItemTouchHelper 사이의 계약에 해당하는 클래스입니다. ItemCallback 클래스는 각 ViewHolder에 대해 어떠한 터치 행동이 가능한지 제어하고, 사용자가 정의한 액션을 수행할 때 콜백을 받습니다.

주요 메소드

  • getMovementFlags(RecyclerView, ViewHolder)
    각각의 뷰에 수행할 수 있는 작업을 컨트롤 하기 위해서는, getMovementFlags(RecyclerView, ViewHolder) 메소드를 오버라이드하고 적절한 방향을 반환해야 합니다. makeMovementFlags(int, int)를 사용하여 쉽게 구성할 수도 있습니다. 또는 SimpleCallback을 사용할 수도 있습니다.

  • onMove(recyclerView, dragged, target)
    만약 사용자가 아이템을 드래그한다면, ItemTouchHelper는 onMove를 호출합니다. 이 콜백 메소드를 받으면 어댑터에서 아이템을 이전 위치(dragged.getAdapterPosition())에서 새로운 위치(target.getAdapterPosition())로 이동해야 하고, RecyclerView의 Adapter에서 notifyItemMoved(int, int)를 호출해야 합니다.

  • onSwiped(ViewHolder, int)
    View가 스와이프되면 ItemTouchHelper는 범위를 벗어날 때까지 View를 애니메이션화한 다음 이 메소드를 호출합니다. 여기서 어댑터를 업데이트 해야하고 관련된 Adapter의 notify 이벤트를 호출해야 합니다.

예제

직접 예제를 보면서 코드를 확인해보겠습니다.

RecyclerViewAdapter와 ItemTouchHelper.Callback을 연결시켜 주는 리스너

// RecyclerView의 Adapter와 ItemTouchHelper.Callback을 연결시켜 주는 리스너
interface ItemTouchHelperListener {
    fun onItemMove(from_position: Int, to_position: Int): Boolean
    fun onItemSwipe(position: Int)
}

RecyclerView의 아이템이 드래그되거나 스와이프될 때 호출되는 메소드를 가진 인터페이스를 선언합니다.

ItemTouchHelper.Callback 클래스를 상속받는 클래스

// ItemTouchHelper.Callback 클래스를 상속
class ItemTouchHelperCallback(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, 0)
    }

    // 드래그된 item을 이전 위치에서 새로운 위치로 옮길 때 호출
    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        // 리스너의 onMove 메소드 호출
        return listener.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
    }
    
    // 사용자에 의해 swipe될 때 호출
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        // 리스너의 onItemSwipe 메소드 호출
        listener.onItemSwipe(viewHolder.adapterPosition)
    }
}

getMovementFlags 메소드는 방향을 정의하는 flag 값을 반환하는 메소드입니다. 위의 코드에서는 dragFlags(드래그 flag)와 swipeFlags(스와이프 flag)를 makeMovementFlags의 인자로 모두 주어 반환하여 드래그 & 드랍과 스와이프 기능을 모두 가능하게 한 코드입니다. 만약 드래그 & 드랍의 기능만 수행할 것이면 makeMovementFlags(dragFlags, 0)를 반환하면 되고 스와이프 기능만 수행할 것이면 swipe(0, swipeFlags)를 반환하면 됩니다.

onMove와 onSwiped는 각각 아이템이 드래그되거나 스와이프되면 호출되는 메소드인데, 위의 코드에서는 리스너의 메소드를 호출하며 position을 매개변수로 주었습니다. Adapter에서 이 position을 적절히 활용하여 항목이 드래그되거나 스와이프될 때 RecyclerView를 어떻게 할지 작성하면 됩니다.

RecyclerViewAdapter

// Listener 인터페이스를 구현
class RecyclerViewAdapter(val nameList: MutableList<String>)
    : RecyclerView.Adapter<RecyclerViewHolder>(), ItemTouchHelperListener {
    // 뷰 초기화하는 메소드
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.sample_item, parent, false)
        return RecyclerViewHolder(view)
    }

    // 항목 초기화
    override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
        holder.onBind(nameList[position])
    }

    // 리스트의 사이즈
    override fun getItemCount(): Int {
        return nameList.size
    }

    // 아이템을 드래그되면 호출되는 메소드
    override fun onItemMove(from_position: Int, to_position: Int): Boolean {
        val name = nameList[from_position]
        // 리스트 갱신
        nameList.removeAt(from_position)
        nameList.add(to_position, name)

        // fromPosition에서 toPosition으로 아이템 이동 공지
        notifyItemMoved(from_position, to_position)
        return true
    }

    // 아이템 스와이프되면 호출되는 메소드
    override fun onItemSwipe(position: Int) {
        // 리스트 아이템 삭제
        nameList.removeAt(position)
        // 아이템 삭제되었다고 공지
        notifyItemRemoved(position)
    }
}

// ViewHolder 클래스
class RecyclerViewHolder(view: View): RecyclerView.ViewHolder(view) {
    val nameView = view.findViewById<TextView>(R.id.name_view)

    // 뷰에 값 셋팅
    fun onBind(name: String) {
        nameView.text = name
    }
}

RecyclerView.Adapter와 ViewHolder의 코드입니다. Adapter에 선언된 메소드인 onCreateViewHolder, onBindViewHolder, onItemCount 메소드는 기본적으로 사용되는 코드이고 특이점이 없습니다. 기본 RecyclerView와 다른 점은 ItemTouchHelperListener 리스너를 구현하여 onItemMove 메소드와 onItemSwipe 메소드를 오버라이드 하였습니다.

onItemMove 메소드는 항목이 드래그될 때 사용되는 메소드입니다. 코드는 아이템 리스트에서 드래그되는 아이템을 갱신(삭제 및 추가)하고 notifyItemMoved(fromPosition, toPosition)을 호출하여 아이템이 이전 위치에서 새로운 위치로 변경되었다고 알립니다.

onItemSwipe 메소드는 항목이 스와이프될 때 사용되는 메소드입니다. 코드는 항목이 스와이프되면 그 아이템을 삭제하기 위해 아이템 리스트에서 해당 위치의 아이템을 삭제하고, notifyItemRemoved(position)을 호출하여 해당 위치의 아이템이 삭제되었다는 것을 알립니다.

MainActivity

// 데이터 셋팅
val list = ArrayList<String>()
for(i in 1..30) {
    list.add("Item $i")
}

// 어댑터
val adapter = RecyclerViewAdapter(list)
recyclerView.adapter = adapter

// 리스너를 구현한 Adapter 클래스를 Callback 클래스의 생성자로 지정
val itemTouchHelperCallback = ItemTouchHelperCallback(adapter)

// ItemTouchHelper의 생성자로 ItemTouchHelper.Callback 객체 셋팅
val helper = ItemTouchHelper(itemTouchHelperCallback)
// RecyclerView에 ItemTouchHelper 연결
helper.attachToRecyclerView(recyclerView)

ItemTouchHelper.Callback 객체를 ItemTouchHelper 클래스의 생성자로 넣어서 주어진 콜백 클래스로 동작하는 ItemTouchHelper 객체를 생성합니다. 그리고 ItemTouchHelper.addachToRecyclerView(RecyclerView recyclerView) 메소드를 통해 RecyclerView에 연결해주면 됩니다.

참조
안드로이드 developer - ItemTouchHelper
리싸이클러뷰 아이템 이동, 삭제

위 예제의 소스코드는 깃허브 링크에 있습니다.

profile
되새기기 위해 기록
post-custom-banner

0개의 댓글