개발일지

Android in A..Z - Recyclerview (ItemTouchHelper) 본문

Android (안드로이드)/RecyclerView

Android in A..Z - Recyclerview (ItemTouchHelper)

강태종 2021. 1. 10. 17:28

ItemTouchHelper

OnDragListener, GestureDetectors 등을 활요한 View에 다양한 제스처를 반응할 수 있지만 매우 복잡하다는 단점을 가지고 있다.

 

RecyclerView에서는 쉽고 간편하게 제스처에 반응할 수 있도록 ItemTouchHelper를 제공한다.

ItemTouchHelper


ItemTouchHelper.SimpleCallback

ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
	override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
		return true
	}

	override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
		val index = binding.recyclerView.indexOfChild(viewHolder.itemView)
		list.removeAt(index)
		adapter.notifyItemRemoved(index)
	}

	override fun onMoved(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, fromPos: Int, target: RecyclerView.ViewHolder, toPos: Int, x: Int, y: Int) {
		super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y)
		Collections.swap(list, fromPos, toPos)
		adapter.notifyItemMoved(fromPos, toPos)
	}
}).attachToRecyclerView(binding.recyclerView)
  • 생성자 첫번째 매개변수(dragDirs) : 상하로 Drag되는 방향을 매개 변수로 전달한다.
  • 생성자 두번째 매개변수(swipeDirs) : 좌우로 Swipe되는 방향을 매개 변수로 전달한다.
  • onMove : ViewHolder를 drag할 때 호출하는 함수이다. false를 반환하면 drag를 실행하지 않기 때문에 drag가 필요할 때 true를 반환해야한다.
  • onSwiped : 좌우로 Swipe될 때 호출되는 함수이다. adapter에 remove를 알리는 코드를 작성해야 한다.
  • onMoved : 상하로 Move될 때 호출되는 함수이다. adapter에 move를 알리는 코드를 작성해야 한다.
  • attachToRecyclerview : ItemTouchHelper를 RecyclerView에 연결하는 함수이다.

BaseAdapter

abstract class BaseAdapter<E: Any>(diffCallback: DiffUtil.ItemCallback<E>) : ListAdapter<E, BaseHolder<out ViewDataBinding, E>>(diffCallback) {
    override fun onBindViewHolder(holder: BaseHolder<out ViewDataBinding, E>, position: Int) {
        holder.bind(getItem(position))
    }

    override fun onBindViewHolder(holder: BaseHolder<out ViewDataBinding, E>, position: Int, payloads: MutableList<Any>) {
        super.onBindViewHolder(holder, position, payloads)
        holder.bind(getItem(position), payloads)
    }
}

 

ToDoAdapter

class ToDoAdapter : BaseAdapter<ToDo>(ToDoItemCallback()) {
    init {
        setHasStableIds(true)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseHolder<out ViewDataBinding, ToDo> {
        return ToDoHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.context), viewType, parent, false))
    }

    override fun getItemId(position: Int): Long {
        return getItem(position).id
    }

    override fun getItemViewType(position: Int): Int {
        return R.layout.holder_todo
    }

    inner class ToDoHolder(binding: HolderTodoBinding) : BaseHolder<HolderTodoBinding, ToDo>(binding) {
        private var isFinished by Delegates.observable(false) { _, _, newValue ->
            if (newValue) {
                with(binding.isFinished) {
                    animate()
                            .withStartAction {
                                visibility = View.VISIBLE
                            }
                            .alpha(1F)
                            .scaleX(1F)
                            .scaleY(1F)
                            .setDuration(500)
                            .setInterpolator(OvershootInterpolator())
                }
            } else {
                with(binding.isFinished) {
                    animate()
                            .alpha(0F)
                            .scaleX(0F)
                            .scaleY(0F)
                            .setDuration(500)
                            .withEndAction {
                                visibility = View.GONE
                            }
                }
            }
        }

        init {
            itemView.setOnClickListener {
                this@ToDoAdapter.notifyItemChanged(adapterPosition, "onClick")
            }
        }

        private fun onClick() {
            isFinished = !isFinished
        }

        private fun onRefresh() {
            isFinished = false
        }

        override fun bind(element: ToDo) {
            super.bind(element)
            binding.todo = element
        }

        override fun bind(element: ToDo, payload: MutableList<Any>) {
            super.bind(element, payload)
            for (any in payload) {
                when(any) {
                    "onClick" -> {
                        onClick()
                    }
                    "onRefresh" -> {
                        onRefresh()
                    }
                }
            }
        }
    }

    private class ToDoItemCallback : DiffUtil.ItemCallback<ToDo>() {
        override fun areItemsTheSame(oldItem: ToDo, newItem: ToDo): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: ToDo, newItem: ToDo): Boolean {
            return oldItem.text == newItem.text
        }
    }
}

BaseHolder

abstract class BaseHolder<VB: ViewDataBinding, E: Any>(protected val binding: VB) : RecyclerView.ViewHolder(binding.root) {
    val context: Context
        get() { return itemView.context }

    lateinit var element: E

    open fun bind(element: E) {
        this.element = element
    }

    open fun bind(element: E, payload: MutableList<Any>) {
        this.element = element
    }
}

ToDoHolder

    inner class ToDoHolder(binding: HolderTodoBinding) : BaseHolder<HolderTodoBinding, ToDo>(binding) {
        private var isFinished by Delegates.observable(false) { _, _, newValue ->
            if (newValue) {
                with(binding.isFinished) {
                    animate()
                            .withStartAction {
                                visibility = View.VISIBLE
                            }
                            .alpha(1F)
                            .scaleX(1F)
                            .scaleY(1F)
                            .setDuration(500)
                            .setInterpolator(OvershootInterpolator())
                }
            } else {
                with(binding.isFinished) {
                    animate()
                            .alpha(0F)
                            .scaleX(0F)
                            .scaleY(0F)
                            .setDuration(500)
                            .withEndAction {
                                visibility = View.GONE
                            }
                }
            }
        }

        init {
            itemView.setOnClickListener {
                this@ToDoAdapter.notifyItemChanged(adapterPosition, "onClick")
            }
        }

        private fun onClick() {
            isFinished = !isFinished
        }

        private fun onRefresh() {
            isFinished = false
        }

        override fun bind(element: ToDo) {
            super.bind(element)
            binding.todo = element
        }

        override fun bind(element: ToDo, payload: MutableList<Any>) {
            super.bind(element, payload)
            for (any in payload) {
                when(any) {
                    "onClick" -> {
                        onClick()
                    }
                    "onRefresh" -> {
                        onRefresh()
                    }
                }
            }
        }
    }

holder_todo

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="todo"
            type="com.taetae98.recyclerview.data.ToDo" />
    </data>

    <androidx.cardview.widget.CardView
        app:cardCornerRadius="10dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/textView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="10dp"
                android:padding="5dp"
                android:text="@{todo.text}"
                android:textColor="@color/black"
                android:textSize="16sp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <ImageView
                android:id="@+id/isFinished"
                android:visibility="gone"
                android:alpha="0"
                android:scaleX="0"
                android:scaleY="0"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toEndOf="@+id/textView"
                app:layout_constraintTop_toTopOf="parent"
                app:srcCompat="@drawable/ic_finish"
                android:contentDescription="@string/is_finished" />
        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.cardview.widget.CardView>
</layout>

Git (예제코드)

github.com/KangTaeJong98/Example/tree/main/Android/RecyclerView

 

KangTaeJong98/Example

My Example Code. Contribute to KangTaeJong98/Example development by creating an account on GitHub.

github.com

 

Comments