본문 바로가기

[Android/Kotlin] 안드로이드 Multi Selection RecyclerView 만들기

꿈꾸는블로그왕 2021. 2. 2.

안녕하세요.  오늘은 RecyclerView로 여러 아이템을 선택하는 방법에 대해서 알아보겠습니다.

흔히 Multi Selection RecyclerView라고 말합니다.

 

리스트뷰에서 특정 아이템을 선택하여 따로 저장하거나 삭제하는 경우에 유용하게 쓸 수 있습니다.

 

오늘 보여드릴 시나리오는 아이템 목록에서 다중선택을 통하여 삭제하는 흐름 입니다.

처음 아이템이 선택되면 버튼이 활성화 되고 삭제하기 버튼을 클릭하면 선택 된 아이템의 갯수가 토스트 메시지로 표시가 됩니다.

 

완성된 모습은 아래와 같습니다.

 

 

 

STEP01. 레이아웃 구성하기

레이아웃은 RecyclerView와 그 아래 삭제 버튼으로 구성했습니다.

[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/recycler_expense"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:overScrollMode="never"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/btn_delete"
        />

    <Button
        android:id="@+id/btn_delete"
        android:text="삭제하기"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

 

STEP02. RecyclerView 구현하기

RecyclerView에서 보여줄 데이터 클래스 Expense를 만들어 줍니다.

[Expense.kt]

data class Expense(
    var title: String,
    var amount: Int
)

RecyclerVIew에서 보여줄 레이아웃입니다.

[item_expense.xml]

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/layout_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <TextView
        android:id="@+id/txt_title"
        android:padding="16dp"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/txt_amount"
        app:layout_constraintBottom_toBottomOf="parent"
        tools:text="점심 식사"
        />

    <TextView
        android:id="@+id/txt_amount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        tools:text="8000"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

 

Adapter 클래스를 아래와 같이 만들어줍니다.

Main Activity에서 클릭 이벤트를 처리할 수 있게 setOnItemLickListener() 함수를 만들었습니다.

[ExpenseAdapter.kt]

class ExpenseAdapter : ListAdapter<Expense, ExpenseAdapter.MyViewHolder>(DiffUtils()) {

    inner class MyViewHolder(
        private val binding: ItemExpenseBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(expense: Expense) {
            with(binding) {
                txtTitle.text = expense.title
                txtAmount.text = expense.amount.toString()
            }
        }

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        MyViewHolder(ItemExpenseBinding.inflate(LayoutInflater.from(parent.context), parent, false))

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    private class DiffUtils : DiffUtil.ItemCallback<Expense>() {
        override fun areItemsTheSame(oldItem: Expense, newItem: Expense): Boolean {
            return oldItem.title == newItem.title
        }

        override fun areContentsTheSame(oldItem: Expense, newItem: Expense): Boolean {
            return oldItem == newItem
        }
    }

    private var onItemClickListener: ((Expense) -> Unit)? = null

    fun setOnItemClickListener(listener: (Expense) -> Unit) {
        onItemClickListener = listener
    }

}

 

액티비로 돌아가서 Fake Data를 만들어 RecyclerView 어댑터에 전달하는 코드를 작성했습니다.

[MainActivity.kt]

class MainActivity : AppCompatActivity() {

    private var _binding: ActivityMainBinding? = null
    private val binding get() = _binding!!

    private lateinit var expenseAdapter: ExpenseAdapter
    private lateinit var dataList: ArrayList<Expense>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        loadFakeData()

        binding.btnDelete.isEnabled = false

        binding.recyclerExpense.apply {
            setHasFixedSize(true)
            layoutManager = LinearLayoutManager(this@MainActivity)
            expenseAdapter = ExpenseAdapter()
            adapter = expenseAdapter
        }

        expenseAdapter.submitList(dataList)
    }

    private fun loadFakeData() {
        dataList = ArrayList()
        dataList.clear()

        val default = 1000
        for (i in 1..20) {
            val expense = Expense(title = "밥먹기 $i", amount = default * i)
            dataList.add(expense)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }

}

 

여기까지 작성하시면 RecyclerView 나타납니다. 

 

 

이제 RecyclerView에 선택할 수 있도록 코드를 수정해 보도록 하겠습니다.

선택된 expense를 담을 리스트를 하나 생성해 줍니다. RecyclerView 레이아웃 클릭 시 applySelection() 함수를 호출하여 selectedExpense 리스트에 담겨 있는지 여부에 따라서 추가와 삭제를 해줍니다.

추가적으로 changeBackground() 함수를 호출하여 UI를 변경해 줍니다.

 

[ExpenseAdapter.kt]

private var selectedExpense = arrayListOf<Expense>()

...

// bind 함수 수정
binding.root.setOnClickListener {
    applySelection(binding, expense)
    onItemClickListener?.let { it(expense) }
}

...

private fun applySelection(binding: ItemExpenseBinding, expense: Expense) {
    if (selectedExpense.contains(expense)) {
        selectedExpense.remove(expense)
        changeBackground(binding, R.color.white)
    } else {
        selectedExpense.add(expense)
        changeBackground(binding, R.color.purple_200)
    }
}

private fun changeBackground(binding: ItemExpenseBinding, resId: Int) {
    binding.layoutContainer.setBackgroundColor(ContextCompat.getColor(binding.root.context, resId))
}

fun getSelectedExpense() = selectedExpense.size

 

메인 액티비로 돌아가서 아이템이 클릭될 때 마다 Adpater 클래스의 getSelectedExpense() 함수를 호출하여,

아이템 여부에 따라서 버튼의 선택 가능 여부를 변경해줍니다.

[MainActivity.kt]

expenseAdapter.setOnItemClickListener { response ->
    binding.btnDelete.isEnabled = expenseAdapter.getSelectedExpense() > 0
}

binding.btnDelete.setOnClickListener {
    Toast.makeText(this, "삭제: ${expenseAdapter.getSelectedExpense()}개", 
        Toast.LENGTH_SHORT).show()
}

댓글