Drag and reorder RecyclerView items in a user friendly manner

What we want to do

As illustrated in the above animation, we want to enable users to

  • reorder items by dragging the handles on the right side of ViewHolders
  • reorder items by long-pressing -> dragging the ViewHolders

Even though they feel very basic actions, by-the-book Android coding lacks subtle user-friendliness in the experience. In the following short article, I will demonstrate how you can transform the by-the-book reorder implementation to a user friendly one with just small tweaks.

All example codes are written in Kotlin.

Step 1: Implement ItemTouchHelper by the book

InitializeItemTouchHelper instance in your Activity or Fragment.

private val itemTouchHelper by lazy {
  // 1. Note that I am specifying all 4 directions. 
// Specifying START and END also allows
// more organic dragging than just specifying UP and DOWN.
val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback(UP or
DOWN or
START or
END
, 0) {

override fun onMove(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean
{

val adapter = recyclerView.adapter as MainRecyclerViewAdapter
val from = viewHolder.adapterPosition
val to = target.adapterPosition
      // 2. Update the backing model. Custom implementation in 
// MainRecyclerViewAdapter. You need to implement
// reordering of the backing model inside the method.
adapter.moveItem(from, to)
      // 3. Tell adapter to render the model update.
adapter.notifyItemMoved(from, to)

return true
}
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder,
direction: Int) {
// 4. Code block for horizontal swipe.
// ItemTouchHelper handles horizontal swipe as well, but
// it is not relevant with reordering. Ignoring here.
}
}
  ItemTouchHelper(simpleItemTouchCallback)
}

After this, you just need to attach the ItemTouchHelper instance to RecyclerView instance and that’s all.

override fun onCreate(savedInstanceState: Bundle?) {
...
itemTouchHelper.attachToRecyclerView(recyclerView)
}

You get something like this :

Product of first step : by the book implementation of reorder action

You can long press each row, and initiate reordering!

However, we have following UX issues with this naive implementation.

  • It is not intuitive to learn that you actually have to long press to start reordering. Many of your users may not even realize that you can reorder items.
  • It is hard to know when you can start reordering after long press, because you don’t get any feedback when the row is selected.

Step 2 : Highlight the row while being selected.

We can solve the second issue “hard to know when you can start reordering”, by highlighting a row while the row is being selected. There are many ways to highlight a row, but in this example, we just make it half transparent.

private val itemTouchHelper by lazy {
val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback(UP or
DOWN or
START or
END, 0) {
// 1. This callback is called when a ViewHolder is selected.
// We highlight the ViewHolder here.
override fun onSelectedChanged(viewHolder: ViewHolder?,
actionState: Int)
{
super.onSelectedChanged(viewHolder, actionState)

if (actionState == ACTION_STATE_DRAG) {
viewHolder?.itemView?.alpha = 0.5f
}
}
    // 2. This callback is called when the ViewHolder is 
// unselected (dropped). We unhighlight the ViewHolder here.
override fun clearView(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder)
{
super.clearView(recyclerView, viewHolder)
viewHolder?.itemView?.alpha = 1.0f
}
}
  ...
}

That’s it! And now you have something like this:

It’s much easier to understand when you can actually start moving items after long press. Great!

However, we still have the issue that non tech savvy users may have hard to time to figure out how to reorder items. On the other hand, power users may feel it stressful that you always have to wait for a second to start reordering.

Step 3 : Implement a handle as an indicator

To solve the issue, we add a handle to the right side of each row, and we let the row to be selected immediately when user touches the handle. This is pretty easy implementation as well.

In the Activity/Fragment, you prepare a custom method to call ItemTouchHelper.startDrag(...). When you call this method, ViewHolder's state becomes selected for dragging.

fun startDragging(viewHolder: RecyclerView.ViewHolder) { 
itemTouchHelper.startDrag(viewHolder)
}

Let’s assume you have placed the handle icon in the layout xml of the ViewHolder and set id= @+id/handleView . You can implement RecyclerView adapter’s callback method like this.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
MainRecyclerViewHolder {
...
// 1. Implement `OnTouchListener` on handleView
viewHolder.itemView.handleView.setOnTouchListener {
view, event ->
    if (event.actionMasked == MotionEvent.ACTION_DOWN) {
// 2. When we detect touch-down event, we call the
// startDragging(...) method we prepared above
activity.startDragging(viewHolder)
}
    return@setOnTouchListener true
}
...
}

Aaand, that’s it!

When you touch handle, you can immediately start reordering. And reordering initiated by long press is also available for users who prefer to touch the entire row.

Summary

You can change RecyclerView’s by-the-book reordering behavior to a more user friendly one, just by adding small tweaks. I have put the sample code in GitHub. Each git tag (step1, step2, step3) corresponds to the (Step 1, Step 2, Step 3) of this article.