๊พธ์ค€ํ•จ์ด ์ง„๋ฆฌ๋‹ค!!

์–ด์ œ๋ณด๋‹ค ๋ฐœ์ „ํ•œ ์˜ค๋Š˜์ด ๋˜๊ณ ํ”ˆ ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป ์˜ ๋ธ”๋กœ๊ทธ

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>

์‹œ์—ฐ ์˜์ƒ์„ ๋์œผ๋กœ ํฌ์ŠคํŒ…์„ ๋งˆ์น˜๊ฒ ์Šต๋‹ˆ๋‹ค.๐Ÿคฉ

์˜ค๋Š˜๋„ ์ฆ์ฝ” ํ•˜์„ธ์š” :)

 

์‹œ์—ฐ ์˜์ƒ