์๋ ํ์ธ์ ๐
์ค๋์ 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>
์์ฐ ์์์ ๋์ผ๋ก ํฌ์คํ ์ ๋ง์น๊ฒ ์ต๋๋ค.๐คฉ
์ค๋๋ ์ฆ์ฝ ํ์ธ์ :)