Drag and Swipe with RecyclerView

Part Two: Handles, Grids, and Custom Animations

July 22, 2015

In Part One, we looked at ItemTouchHelper, and an implementation of ItemTouchHelper.Callback that adds basic drag & drop and swipe-to-dismiss to linear RecyclerView lists. This article will expand on that example, adding support for grid layouts, “handle” initiated drags, indicating the selected view, and custom swipe animations.

Drag Handles

When designing a list that supports drag & drop, it’s common to include an affordance that initiates the drag on touch. This helps with discoverability and usability, and it’s recommended by the Material Guidelines when the list is in “edit mode”. Updating our example to include a “handle,” or, “reorder list control” is fairly trivial.

Source: google.com/design

First, update the item layout. (item_main.xml)

The image used for the drag handle can be found in the Material Design Icons and was added using the handy Android Material Design Icon Generator Plugin.

As briefly mentioned in the last article, you can use ItemTouchHelper.startDrag(ViewHolder) to programmatically start a drag. So all we have to do is update the ViewHolder to include the new handle view, and setup a simple touch event listener that triggers the startDrag() call.

We’ll need an interface to pass the event up the chain:

public interface OnStartDragListener {

/**
* Called when a view is requesting a start of a drag.
*
* @param viewHolder The holder of the view to drag.
*/
void onStartDrag(RecyclerView.ViewHolder viewHolder);
}

Then, instantiate the handle view in ItemViewHolder:

public final ImageView handleView;
public ItemViewHolder(View itemView) {
super(itemView);
// ...
handleView = (ImageView) itemView.findViewById(R.id.handle);
}

and update RecyclerListAdapter:

private final OnStartDragListener mDragStartListener;

public RecyclerListAdapter(OnStartDragListener dragStartListener) {
mDragStartListener = dragStartListener;
// ...
}
@Override
public void onBindViewHolder(final ItemViewHolder holder,
int position) {
// ...
    holder.handleView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (MotionEventCompat.getActionMasked(event) ==
MotionEvent.ACTION_DOWN) {
mDragStartListener.onStartDrag(holder);
}
return false;
}
});
}

The full RecyclerListAdapter class should now look something like this.

All that’s left to do is add the OnStartDragListener to the Fragment :

public class RecyclerListFragment extends Fragment implements 
OnStartDragListener {

// ...
    @Override
public void onViewCreated(View view, Bundle icicle) {
super.onViewCreated(view, icicle);

RecyclerListAdapter a = new RecyclerListAdapter(this);
// ...
}
    @Override
public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
mItemTouchHelper.startDrag(viewHolder);
}
}

The full RecyclerListFragment class should now look like this. When you run, you now should be able to start drags by touching the handle.

Indicating the Selected View

In our basic example, there is no visual indication that item being dragged is actually selected. For obvious reasons, this is undesirable, but also easy to fix. In fact, with ItemTouchHelper, you get free effects as long as your View Holder item has a background set. On Lollipop, and later, the Item View elevation is increased during a drag or swipe; on earlier versions you get a basic fade with swipe.

To see this using our existing example, simply add a background to the root FrameLayout of item_main.xml, or set one in the constructor of RecyclerListAdapter.ItemViewHolder. It’ll look something like this:

It looks great, but you may wish to have more control. One way to do this is let your View Holder handle the changes whenever it is “selected” or “cleared”. For this, ItemTouchHelper.Callback provides two callbacks.

  • onSelectedChanged(ViewHolder, int) is called every time the state of a View Holder changes to drag (ACTION_STATE_DRAG) or swipe (ACTION_STATE_SWIPE). This is the perfect place to change your item View state to active.
  • clearView(RecyclerView, ViewHolder) is called when a dragged view is dropped, and also when a swipe is cancelled or completed (ACTION_STATE_IDLE). This is where you would typically restore the idle state of your item View.

So we’ll just wire things together.

First, create an interface for interested View Holders to implement:

/**
* Notifies a View Holder of relevant callbacks from
* {@link ItemTouchHelper.Callback}.
*/
public interface ItemTouchHelperViewHolder {

/**
* Called when the {@link ItemTouchHelper} first registers an
* item as being moved or swiped.
* Implementations should update the item view to indicate
* it's active state.
*/
void onItemSelected();


/**
* Called when the {@link ItemTouchHelper} has completed the
* move or swipe, and the active item state should be cleared.
*/
void onItemClear();
}

Next, have SimpleItemTouchHelperCallback trigger the respective callbacks:

@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder,
int actionState) {
   // We only want the active item
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
if (viewHolder instanceof ItemTouchHelperViewHolder) {
ItemTouchHelperViewHolder itemViewHolder =
(ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemSelected();
}
}

super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);

if (viewHolder instanceof ItemTouchHelperViewHolder) {
ItemTouchHelperViewHolder itemViewHolder =
(ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemClear();
}
}

Now, all that’s left is to have RecyclerListAdapter.ItemViewHolder implement ItemTouchHelperViewHolder:

public class ItemViewHolder extends RecyclerView.ViewHolder 
implements ItemTouchHelperViewHolder {

// ...
    @Override
public void onItemSelected() {
itemView.setBackgroundColor(Color.LTGRAY);
}

@Override
public void onItemClear() {
itemView.setBackgroundColor(0);
}
}

For the sake of the example, we’re simply adding a gray background when the item is active, and removing it when it’s cleared. If your ItemTouchHelper and Adapter are tightly coupled, you can easily forgo this setup, and toggle the view state directly in your ItemTouchHelper.Callback.

Grid Layouts

If you try to alter this project to use a GridLayoutManager, you’ll quickly find that it doesn’t work properly. The reason and fix are simple: We must tell our ItemTouchHelper that we want to support dragging left and right. In SimpleItemTouchHelperCallback, we already specified:

@Override
public int getMovementFlags(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}

The only change required to support grid layouts is to add the left and right directions to dragFlags:

int dragFlags = ItemTouchHelper.UP   | ItemTouchHelper.DOWN | 
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;

However, swipe-to-dismiss is not a very natural pattern for grids, so you might end up with something more like:

@Override
public int getMovementFlags(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN |
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
}

To see a working GridLayoutManager example, see RecyclerGridFragment. This is what it looks like when run:

Custom Swipe Animations

ItemTouchHelper.Callback provides a really convenient way for us to take full control over the view animation during a drag or swipe. Since ItemTouchHelper is a RecyclerView.ItemDecoration, we can hook into the View drawing in a similar matter. In the next part, we’ll expand on this further, but here’s a simple example of overriding the default swipe animation to show a linear fade.

@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView,
ViewHolder viewHolder, float dX, float dY,
int actionState, boolean isCurrentlyActive) {

if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float width = (float) viewHolder.itemView.getWidth();
float alpha = 1.0f - Math.abs(dX) / width;
viewHolder.itemView.setAlpha(alpha);
viewHolder.itemView.setTranslationX(dX);
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY,
actionState, isCurrentlyActive);
}
}

The parameters dX and dY represent the current translation of the the selected view, where:

  • -1.0f is a full ItemTouchHelper.END to ItemTouchHelper.START swipe
  • 1.0f is a full ItemTouchHelper.START to ItemTouchHelper.END swipe

It’s important to call through to super for any actionState that you are not handling, so the other default animations run.

The next Part will include an example that takes control over drawing while dragging.

Conclusion

We’re just getting into the fun parts of what you can do to customize ItemTouchHelper. I was hoping to be able to include more in this part, but in the interest of length, I decided to split it up. The time between Part 2 and 3 will be much shorter.

The GitHub project is likely to be updated before the articles are published, so if you don’t need the walk-through, watch the repo for changes.


Source Code

This article series has a corresponding GitHub project, Android-ItemTouchHelper-Demo. This part spans commits ef8f149 trough d164fba.


Follow me on Google+ and Twitter

© 2015 Paul Burke

All code appearing in this article is licensed under Apache 2.0