Android/Kotlin

[Kotlin][Android] 안드로이드 ItemTouchHelper에 대해서 (Drag and Drop 과 Swipe 구현하기)

뎁요 2023. 1. 2. 21:42

안녕하세요 👋

오늘은 ItemTouchHelper를 사용하는 방법에 대해 포스팅해보려 합니다.

ItemTouchHelper란?

ItemTouchHelper는 RecyclerView에 삭제를 위한 Swipte 및 Drag and Drop을 지원하는 유틸리티 클래스입니다.

 

ItemTouchHelper는 사용자가 액션을 수행할 때 이벤트를 수신하는 RecyclerView 및 이벤트에 반응하는 콜백 메서드가 선언되어 있는 Callback 클래스와 함께 사용합니다.


코드를 통해 사용 방법에 대해 알아보겠습니다. 😎

이번 에제에서는 ItemTouchHelper와 RecyclerView를 사용합니다.

RecyclerView에 대한 포스팅은 아래 페이지를 통해 확인 가능하니 함께 확인 부탁드립니다.

 

[Kotlin][Android] 안드로이드 RecyclerView(리싸이클러 뷰)의 사용방법

안녕하세요 👋 오늘은 RecyclerView에 대해서 공부한 내용을 포스팅해보려 합니다. RecyclerView(리싸이클러 뷰)란? RecyclerView는 ListView처럼 제한된 화면(Window)에 대용량 데이터 셋을 보여줄 때 사용합

devyo-111commit.tistory.com

 

먼저 ItemTouchHelper를 사용하기 위해서는 Callback class를 만들어 주어야 합니다.

import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView

class ItemTouchHelperCallBack(listener : ItemTouchHelperListener) : ItemTouchHelper.Callback() {
    private var itemTouchHelperListener : ItemTouchHelperListener = listener
    
	// ItemTouchHelper에서 callback 호출 시 실행되는 함수들
    interface ItemTouchHelperListener {
        fun onItemMove(from : Int , to : Int) : Boolean // drag and drop
        fun onItemSwipe(position : Int) // swipe
    }
      
	// drag and drop과 swipe에 대한 flag를 생성하는 함수
    override fun getMovementFlags(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder
    ): Int {
        val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
        val swipeFlags = ItemTouchHelper.END or ItemTouchHelper.START
        return makeMovementFlags(dragFlags , swipeFlags)
    }
    
    // Item이 이동될때 호출되는 Callback
    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        return itemTouchHelperListener.onItemMove(viewHolder.adapterPosition , target.adapterPosition)
    }
    
	// Item이 Swipe될때 호출되는 Callback
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        itemTouchHelperListener.onItemSwipe(viewHolder.adapterPosition)
    }
    
    // ItemTouchHelper의 LongClick 가능 여부를 설정
    override fun isLongPressDragEnabled(): Boolean {
        return true
    }
    
    // ItemTouchHelper의 Swipe 가능 여부를 설정
    override fun isItemViewSwipeEnabled(): Boolean {
        return true
    }
}

Callback이 호출되었을때 실행할 함수들을 정의해놓은 Interface를 하나 만들고

해당 interface를 정의한 구현부를 생성할 때 전달받아 사용합니다.

 

Interface 구현은 RecyclerViewAdapter에서 해주는데요 코드는 아래와 같습니다.

// RecyclerView의 Model class
data class RecyclerItem(
    var title : String = "" ,
    var image : Int
)
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.lee.itemtouchhelper.data.RecyclerItem
import com.lee.itemtouchhelper.databinding.RecyclerItemBinding

class MainRecyclerViewAdapter
    : RecyclerView.Adapter<MainRecyclerViewAdapter.MainRecyclerViewHolder>()
    , ItemTouchHelperCallBack.ItemTouchHelperListener {
    private var items = mutableListOf<RecyclerItem>()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainRecyclerViewHolder {
        val binding = RecyclerItemBinding.inflate(LayoutInflater.from(parent.context) , parent , false)
        return MainRecyclerViewHolder(binding)
    }

    override fun onBindViewHolder(holder: MainRecyclerViewHolder, position: Int) {
        holder.bind(items[position])
    }

    override fun getItemCount() = items.size

    fun setItems(list : MutableList<RecyclerItem>) {
        items = list
    }

    override fun onItemMove(from: Int, to: Int): Boolean {
        val item = items[from]
        items.removeAt(from)
        items.add(to , item)
        notifyItemMoved(from , to)
        return true
    }

    override fun onItemSwipe(position: Int) {
        items.removeAt(position)
        notifyItemRemoved(position)
    }

    inner class MainRecyclerViewHolder(private val binding : RecyclerItemBinding) : RecyclerView.ViewHolder(binding.root){
        fun bind(item : RecyclerItem){
            with(binding){
                itemTitle.text = item.title
                itemImage.setImageResource(item.image)
            }
        }
    }
}

위와 같이 RecyclerView에서 ItemTouchHelperListener를 implements 하여 함수 호출 시 할 동작을 정의 해줍니다.

 

 onItemMove는 from의 item을 삭제하고 해당 item을 to로 다시 add한 후 adapter에 notify를 합니다.

onItemSwipe은 선택된 position의 item을 삭제하고 adapter에 notify를 합니다. 

 

마지막으로 ItemTouchHelper를 생성하여 RecyclerView에 attach하여 사용합니다.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.lee.itemtouchhelper.adapter.ItemTouchHelperCallBack
import com.lee.itemtouchhelper.adapter.MainRecyclerViewAdapter
import com.lee.itemtouchhelper.data.RecyclerItem
import com.lee.itemtouchhelper.databinding.ActivityMainBinding
import com.lee.itemtouchhelper.viewmodel.MainActivityViewModel
import com.lee.itemtouchhelper.viewmodel.factory.MainViewModelFactory

class MainActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMainBinding
    private lateinit var viewModel : ViewModel
    private lateinit var mainRecyclerViewAdapter : MainRecyclerViewAdapter
    private lateinit var items : MutableList<RecyclerItem>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater).also {
            setContentView(it.root)
        }
        generateItems()
        initRecyclerView()
        viewModel = ViewModelProvider(this , MainViewModelFactory())[MainActivityViewModel::class.java]
    }

    override fun onStart() {
        super.onStart()
        observeData()
    }

    private fun generateItems() {
        items = mutableListOf()
        for(i in 1.. 10){
            val item = RecyclerItem("${i}번째" , R.mipmap.ic_launcher)
            items.add(item)
        }
    }

    private fun initRecyclerView() {
        mainRecyclerViewAdapter = MainRecyclerViewAdapter()
        mainRecyclerViewAdapter.setItems(items)
        binding.mainRecyclerView.run {
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = mainRecyclerViewAdapter
        }
        val itemTouchHelperCallBack = ItemTouchHelperCallBack(mainRecyclerViewAdapter)
        val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallBack)

        itemTouchHelper.attachToRecyclerView(binding.mainRecyclerView)
    }

    private fun observeData(){
        with(viewModel as MainActivityViewModel){
            items.observe(this@MainActivity){
                mainRecyclerViewAdapter.notifyItemRangeChanged(0 , mainRecyclerViewAdapter.itemCount)
            }
        }
    }
}

ViewModel관련 class 및 layout 리소스는 아래와 같습니다.

 

- ViewModel -

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lee.itemtouchhelper.data.RecyclerItem

class MainActivityViewModel : ViewModel() {
    val items = MutableLiveData<MutableList<RecyclerItem>>()
}

 

- ViewModelFactory -

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.lee.itemtouchhelper.viewmodel.MainActivityViewModel

class MainViewModelFactory : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if(modelClass.isAssignableFrom(MainActivityViewModel::class.java)){
            return MainActivityViewModel() as T
        } else {
            throw java.lang.IllegalArgumentException("해당 ViewModel을 찾을수 없습니다.")
        }
    }
}

 

- activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/mainRecyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        tools:listitem="@layout/recycler_item"
        />
</androidx.constraintlayout.widget.ConstraintLayout>

 

- recycler_item.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:clickable="true"
    android:focusable="true"
    android:foreground="?android:attr/selectableItemBackgroundBorderless"
    >
    <ImageView
        android:id="@+id/itemImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_margin="10dp"
        />

    <TextView
        android:id="@+id/itemTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toRightOf="@id/itemImage"
        android:text="타이틀"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_margin="10dp"
        />
</androidx.constraintlayout.widget.ConstraintLayout>

시연 영상을 끝으로 포스팅을 마치겠습니다.🤩

오늘도 즐코 하세요 :)

 

시연 영상