This is a combination of a CursorAdapter
fit for RecyclerViews
, and several helpers to support drag-and-drop and swipe-to-dismiss.
Note: we suggest using Room and LiveData for this; cursors are a thing of the past.
Usage
DragDropItemTouchCallback callback = new DragDropItemTouchCallback(context);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(mRecyclerView);
RecyclerViewCursorAdapter.java
An adapter for RecyclerViews
that uses a Cursor
. This class does not handle drag-drop yet, it is just a CursorAdapter
.
package com.pixplicity.example.ui;
import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.support.v7.widget.RecyclerView;
/**
* https://gist.github.com/skyfishjy/443b7448f59be978bc59
*/
public abstract class RecyclerViewCursorAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
private final Context mContext;
private Cursor mCursor;
private boolean mDataValid;
private int mRowIdColumn;
private final NotifyingDataSetObserver mDataSetObserver;
public RecyclerViewCursorAdapter(Context context, Cursor cursor) {
mContext = context;
mCursor = cursor;
mDataValid = cursor != null;
mRowIdColumn = mDataValid ? mCursor.getColumnIndex("_id") : -1;
mDataSetObserver = new NotifyingDataSetObserver();
if (mCursor != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
}
protected Context getContext() {
return mContext;
}
public Cursor getCursor() {
return mCursor;
}
@Override
public int getItemCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
}
return 0;
}
@Override
public long getItemId(int position) {
if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) {
return mCursor.getLong(mRowIdColumn);
}
return 0;
}
@Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(true);
}
public abstract void onBindViewHolder(VH viewHolder, Cursor cursor);
@Override
public void onBindViewHolder(VH viewHolder, int position) {
if (!mDataValid) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
onBindViewHolder(viewHolder, mCursor);
}
/**
* Change the underlying cursor to a new cursor. If there is an existing cursor it will be
* closed.
*/
public void changeCursor(Cursor cursor) {
Cursor old = swapCursor(cursor);
if (old != null) {
old.close();
}
}
/**
* Swap in a new Cursor, returning the old Cursor. Unlike {@link #changeCursor(Cursor)}, the
* returned old Cursor is <em>not</em> closed.
*/
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
final Cursor oldCursor = mCursor;
if (oldCursor != null && mDataSetObserver != null) {
oldCursor.unregisterDataSetObserver(mDataSetObserver);
}
mCursor = newCursor;
if (mCursor != null) {
if (mDataSetObserver != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
mRowIdColumn = newCursor.getColumnIndexOrThrow("_id");
mDataValid = true;
notifyDataSetChanged();
} else {
mRowIdColumn = -1;
mDataValid = false;
notifyDataSetChanged();
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
}
return oldCursor;
}
private class NotifyingDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
super.onChanged();
mDataValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
mDataValid = false;
notifyDataSetChanged();
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
}
}
}
RecyclerReorderCursorAdapter.java
Extends the previous class to add drag-and-drop support.
package com.pixplicity.example.ui.util;
import android.content.Context;
import android.database.Cursor;
import android.os.Build;
import android.support.v7.widget.RecyclerView;
import android.util.SparseIntArray;
import java.util.LinkedList;
import java.util.List;
/**
* An extension to the RecyclerCursorAdapter that allows reordering list items by dragging them
* (after a long-press) and dismissing items by swiping the to the right.
*/
public abstract class RecyclerReorderCursorAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerCursorAdapter<VH> {
private boolean mResetOnCursorChange = true;
/**
* Mapping from cursor index to the reordered index
*/
private SparseIntArray mPositionMap = new SparseIntArray();
public RecyclerReorderCursorAdapter(Context context, Cursor cursor) {
super(context, cursor);
}
@Override
public long getItemId(int position) {
return super.getItemId(mPositionMap.get(position, position));
}
@Override
public void changeCursor(Cursor newCursor) {
super.changeCursor(newCursor);
if (mResetOnCursorChange) {
resetMap();
}
}
/**
* Informs the adapter that an item was reordered. This will add a ReorderItem to
* the cache used to calculate the modified positions in the cursor. If either of
* the positions are outside the cursor bounds, or they are the same then
* no ReorderItem will be added to the cache
*
* @param originalPosition The original position of the item
* @param newPosition The new position for the item
*/
public void reorderItem(int originalPosition, int newPosition) {
//Make sure the positions aren't the same
if (originalPosition == newPosition) {
return;
}
//Make sure the positions aren't out of bounds
if (originalPosition < 0 || newPosition < 0 || originalPosition >= getItemCount() || newPosition >= getItemCount()) {
return;
}
int curFrom = mPositionMap.get(originalPosition, originalPosition);
//Iterates through the items that will be effected and changes their positions
if (originalPosition > newPosition) {
for (int i = originalPosition; i > newPosition; i--) {
mPositionMap.put(i, mPositionMap.get(i - 1, i - 1));
}
} else {
for (int i = originalPosition; i < newPosition; i++) {
mPositionMap.put(i, mPositionMap.get(i + 1, i + 1));
}
}
//Makes sure the actual change is in place
mPositionMap.put(newPosition, curFrom);
cleanMap();
notifyItemMoved(originalPosition, newPosition);
}
/**
* Determines if the position map for the cursor will be reset
* when the cursor is changed
*
* @return True if the map will be reset on cursor changes [default: true]
*/
public boolean getResetMapOnCursorChange() {
return mResetOnCursorChange;
}
/**
* Sets if the position map for the cursor will be reset when the cursor
* is changed
*
* @param resetOnSwap True if the map should be reset on cursor changes
*/
public void setResetMapOnCursorChange(boolean resetOnSwap) {
this.mResetOnCursorChange = resetOnSwap;
}
/**
* Resets the position map for the cursor. By default this will be
* called when a cursor is changed (see {@link #getResetMapOnCursorChange()})
*/
public void resetMap() {
mPositionMap.clear();
}
/**
* Retrieves the position map for the cursor. This is organized by the
* index being the list (visual) position and the values representing the corresponding
* cursor positions.
*
* @return A SparseIntArray representing a map of list positions to cursor positions
*/
public SparseIntArray getPositionMap() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return mPositionMap.clone();
}
return clone(mPositionMap);
}
/**
* Goes through the {@link #mPositionMap} removing any mappings that
* are unnecessary. This will help keep the map as small as possible
*/
private void cleanMap() {
List<Integer> removeList = new LinkedList<>();
//Finds all the mappings that point to themselves
for (int i = 0; i < mPositionMap.size(); i++) {
if (mPositionMap.keyAt(i) == mPositionMap.valueAt(i)) {
removeList.add(mPositionMap.keyAt(i));
}
}
//Actually removes the items
for (int i : removeList) {
mPositionMap.delete(i);
}
}
/**
* Clones the specified {@link SparseIntArray} using an iterator
*
* @param sparseIntArray The {@link SparseIntArray} to clone
* @return A clone of the specified <code>sparseIntArray</code>
*/
private SparseIntArray clone(SparseIntArray sparseIntArray) {
SparseIntArray clone = new SparseIntArray();
//Iterates through the keys, adding the value to the clone
for (int index = 0; index < sparseIntArray.size(); index++) {
int key = sparseIntArray.keyAt(index);
clone.put(key, sparseIntArray.get(key));
}
return clone;
}
}
DragDropItemCallback.java
Helper class for drag-and-drop and swipe-to-dismiss.
package com.pixplicity.example.ui.util;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
/**
* ItemTouchHelper.Callback that adds drag & drop behaviour to a RecyclerView.
*
* Kudos to Paul Burke:
* https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf
*/
public class DragDropItemTouchCallback extends ItemTouchHelper.Callback {
private final ReorderListener mListener;
public DragDropItemTouchCallback(ReorderListener listener) {
mListener = listener;
}
@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);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
mListener.onItemMove(viewHolder.getAdapterPosition(),
target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mListener.onItemDismiss(viewHolder.getAdapterPosition());
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
public interface ReorderListener {
void onItemMove(int fromPosition, int toPosition);
void onItemDismiss(int position);
}
}