Skip to content

Instantly share code, notes, and snippets.

@prasannaboppe
Created September 19, 2017 10:52
Show Gist options
  • Save prasannaboppe/e7ed52e66414a3c4bd56b88414bf4ce5 to your computer and use it in GitHub Desktop.
Save prasannaboppe/e7ed52e66414a3c4bd56b88414bf4ce5 to your computer and use it in GitHub Desktop.
RecyclerView Utils
package recyclerview;
import android.content.Context;
import android.os.Build;
import android.support.v4.view.MotionEventCompat;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnItemTouchListener;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.View;
abstract class ClickItemTouchListener implements OnItemTouchListener {
private static final String LOGTAG = "ClickItemTouchListener";
private final GestureDetectorCompat mGestureDetector;
ClickItemTouchListener(RecyclerView hostView) {
mGestureDetector = new ItemClickGestureDetector(hostView.getContext(),
new ItemClickGestureListener(hostView));
}
private boolean isAttachedToWindow(RecyclerView hostView) {
if (Build.VERSION.SDK_INT >= 19) {
return hostView.isAttachedToWindow();
} else {
return (hostView.getHandler() != null);
}
}
private boolean hasAdapter(RecyclerView hostView) {
return (hostView.getAdapter() != null);
}
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
if (!isAttachedToWindow(recyclerView) || !hasAdapter(recyclerView)) {
return false;
}
mGestureDetector.onTouchEvent(event);
return false;
}
@Override
public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
// We can silently track tap and and long presses by silently
// intercepting touch events in the host RecyclerView.
}
abstract boolean performItemClick(RecyclerView parent, View view, int position, long id);
abstract boolean performItemLongClick(RecyclerView parent, View view, int position, long id);
private class ItemClickGestureDetector extends GestureDetectorCompat {
private final ItemClickGestureListener mGestureListener;
public ItemClickGestureDetector(Context context, ItemClickGestureListener listener) {
super(context, listener);
mGestureListener = listener;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final boolean handled = super.onTouchEvent(event);
final int action = event.getAction() & MotionEventCompat.ACTION_MASK;
if (action == MotionEvent.ACTION_UP) {
mGestureListener.dispatchSingleTapUpIfNeeded(event);
}
return handled;
}
}
private class ItemClickGestureListener extends SimpleOnGestureListener {
private final RecyclerView mHostView;
private View mTargetChild;
public ItemClickGestureListener(RecyclerView hostView) {
mHostView = hostView;
}
public void dispatchSingleTapUpIfNeeded(MotionEvent event) {
// When the long press hook is called but the long press listener
// returns false, the target child will be left around to be
// handled later. In this case, we should still treat the gesture
// as potential item click.
if (mTargetChild != null) {
onSingleTapUp(event);
}
}
@Override
public boolean onDown(MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
mTargetChild = mHostView.findChildViewUnder(x, y);
return (mTargetChild != null);
}
@Override
public void onShowPress(MotionEvent event) {
if (mTargetChild != null) {
mTargetChild.setPressed(true);
}
}
@Override
public boolean onSingleTapUp(MotionEvent event) {
boolean handled = false;
if (mTargetChild != null) {
mTargetChild.setPressed(false);
final int position = mHostView.getChildPosition(mTargetChild);
final long id = mHostView.getAdapter().getItemId(position);
handled = performItemClick(mHostView, mTargetChild, position, id);
mTargetChild = null;
}
return handled;
}
@Override
public boolean onScroll(MotionEvent event, MotionEvent event2, float v, float v2) {
if (mTargetChild != null) {
mTargetChild.setPressed(false);
mTargetChild = null;
return true;
}
return false;
}
@Override
public void onLongPress(MotionEvent event) {
if (mTargetChild == null) {
return;
}
final int position = mHostView.getChildPosition(mTargetChild);
final long id = mHostView.getAdapter().getItemId(position);
final boolean handled = performItemLongClick(mHostView, mTargetChild, position, id);
if (handled) {
mTargetChild.setPressed(false);
mTargetChild = null;
}
}
}
}
package recyclerview;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
/**
* Created by prasanna on 4/18/17.
* Copied from support library bcoz they made it final
*/
class GestureDetectorCompat {
interface GestureDetectorCompatImpl {
boolean isLongpressEnabled();
boolean onTouchEvent(MotionEvent ev);
void setIsLongpressEnabled(boolean enabled);
void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener listener);
}
private static class GestureDetectorCompatImplBase implements GestureDetectorCompatImpl {
private int mTouchSlopSquare;
private int mDoubleTapSlopSquare;
private int mMinimumFlingVelocity;
private int mMaximumFlingVelocity;
private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
// constants for Message.what used by GestureHandler below
private static final int SHOW_PRESS = 1;
private static final int LONG_PRESS = 2;
private static final int TAP = 3;
private final Handler mHandler;
final GestureDetector.OnGestureListener mListener;
GestureDetector.OnDoubleTapListener mDoubleTapListener;
boolean mStillDown;
boolean mDeferConfirmSingleTap;
private boolean mInLongPress;
private boolean mAlwaysInTapRegion;
private boolean mAlwaysInBiggerTapRegion;
MotionEvent mCurrentDownEvent;
private MotionEvent mPreviousUpEvent;
/**
* True when the user is still touching for the second tap (down, move, and
* up events). Can only be true if there is a double tap listener attached.
*/
private boolean mIsDoubleTapping;
private float mLastFocusX;
private float mLastFocusY;
private float mDownFocusX;
private float mDownFocusY;
private boolean mIsLongpressEnabled;
/**
* Determines speed during touch scrolling
*/
private VelocityTracker mVelocityTracker;
private class GestureHandler extends Handler {
GestureHandler() {
super();
}
GestureHandler(Handler handler) {
super(handler.getLooper());
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_PRESS:
mListener.onShowPress(mCurrentDownEvent);
break;
case LONG_PRESS:
dispatchLongPress();
break;
case TAP:
// If the user's finger is still down, do not count it as a tap
if (mDoubleTapListener != null) {
if (!mStillDown) {
mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
} else {
mDeferConfirmSingleTap = true;
}
}
break;
default:
throw new RuntimeException("Unknown message " + msg); //never
}
}
}
/**
* Creates a GestureDetector with the supplied listener.
* You may only use this constructor from a UI thread (this is the usual situation).
* @see android.os.Handler#Handler()
*
* @param context the application's context
* @param listener the listener invoked for all the callbacks, this must
* not be null.
* @param handler the handler to use
*
* @throws NullPointerException if {@code listener} is null.
*/
public GestureDetectorCompatImplBase(Context context, GestureDetector.OnGestureListener listener,
Handler handler) {
if (handler != null) {
mHandler = new GestureDetectorCompatImplBase.GestureHandler(handler);
} else {
mHandler = new GestureDetectorCompatImplBase.GestureHandler();
}
mListener = listener;
if (listener instanceof GestureDetector.OnDoubleTapListener) {
setOnDoubleTapListener((GestureDetector.OnDoubleTapListener) listener);
}
init(context);
}
private void init(Context context) {
if (context == null) {
throw new IllegalArgumentException("Context must not be null");
}
if (mListener == null) {
throw new IllegalArgumentException("OnGestureListener must not be null");
}
mIsLongpressEnabled = true;
final ViewConfiguration configuration = ViewConfiguration.get(context);
final int touchSlop = configuration.getScaledTouchSlop();
final int doubleTapSlop = configuration.getScaledDoubleTapSlop();
mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
mTouchSlopSquare = touchSlop * touchSlop;
mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
}
/**
* Sets the listener which will be called for double-tap and related
* gestures.
*
* @param onDoubleTapListener the listener invoked for all the callbacks, or
* null to stop listening for double-tap gestures.
*/
@Override
public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) {
mDoubleTapListener = onDoubleTapListener;
}
/**
* Set whether longpress is enabled, if this is enabled when a user
* presses and holds down you get a longpress event and nothing further.
* If it's disabled the user can press and hold down and then later
* moved their finger and you will get scroll events. By default
* longpress is enabled.
*
* @param isLongpressEnabled whether longpress should be enabled.
*/
@Override
public void setIsLongpressEnabled(boolean isLongpressEnabled) {
mIsLongpressEnabled = isLongpressEnabled;
}
/**
* @return true if longpress is enabled, else false.
*/
@Override
public boolean isLongpressEnabled() {
return mIsLongpressEnabled;
}
/**
* Analyzes the given motion event and if applicable triggers the
* appropriate callbacks on the {@link GestureDetector.OnGestureListener} supplied.
*
* @param ev The current motion event.
* @return true if the {@link GestureDetector.OnGestureListener} consumed the event,
* else false.
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final boolean pointerUp =
(action & MotionEventCompat.ACTION_MASK) == MotionEventCompat.ACTION_POINTER_UP;
final int skipIndex = pointerUp ? MotionEventCompat.getActionIndex(ev) : -1;
// Determine focal point
float sumX = 0, sumY = 0;
final int count = ev.getPointerCount();
for (int i = 0; i < count; i++) {
if (skipIndex == i) continue;
sumX += ev.getX(i);
sumY += ev.getY(i);
}
final int div = pointerUp ? count - 1 : count;
final float focusX = sumX / div;
final float focusY = sumY / div;
boolean handled = false;
switch (action & MotionEventCompat.ACTION_MASK) {
case MotionEventCompat.ACTION_POINTER_DOWN:
mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY;
// Cancel long press and taps
cancelTaps();
break;
case MotionEventCompat.ACTION_POINTER_UP:
mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY;
// Check the dot product of current velocities.
// If the pointer that left was opposing another velocity vector, clear.
mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
final int upIndex = MotionEventCompat.getActionIndex(ev);
final int id1 = ev.getPointerId(upIndex);
final float x1 = VelocityTrackerCompat.getXVelocity(mVelocityTracker, id1);
final float y1 = VelocityTrackerCompat.getYVelocity(mVelocityTracker, id1);
for (int i = 0; i < count; i++) {
if (i == upIndex) continue;
final int id2 = ev.getPointerId(i);
final float x = x1 * VelocityTrackerCompat.getXVelocity(mVelocityTracker, id2);
final float y = y1 * VelocityTrackerCompat.getYVelocity(mVelocityTracker, id2);
final float dot = x + y;
if (dot < 0) {
mVelocityTracker.clear();
break;
}
}
break;
case MotionEvent.ACTION_DOWN:
if (mDoubleTapListener != null) {
boolean hadTapMessage = mHandler.hasMessages(TAP);
if (hadTapMessage) mHandler.removeMessages(TAP);
if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
// This is a second tap
mIsDoubleTapping = true;
// Give a callback with the first tap of the double-tap
handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
// Give a callback with down event of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else {
// This is a first tap
mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
}
}
mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY;
if (mCurrentDownEvent != null) {
mCurrentDownEvent.recycle();
}
mCurrentDownEvent = MotionEvent.obtain(ev);
mAlwaysInTapRegion = true;
mAlwaysInBiggerTapRegion = true;
mStillDown = true;
mInLongPress = false;
mDeferConfirmSingleTap = false;
if (mIsLongpressEnabled) {
mHandler.removeMessages(LONG_PRESS);
mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
+ TAP_TIMEOUT + LONGPRESS_TIMEOUT);
}
mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
handled |= mListener.onDown(ev);
break;
case MotionEvent.ACTION_MOVE:
if (mInLongPress) {
break;
}
final float scrollX = mLastFocusX - focusX;
final float scrollY = mLastFocusY - focusY;
if (mIsDoubleTapping) {
// Give the move events of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else if (mAlwaysInTapRegion) {
final int deltaX = (int) (focusX - mDownFocusX);
final int deltaY = (int) (focusY - mDownFocusY);
int distance = (deltaX * deltaX) + (deltaY * deltaY);
if (distance > mTouchSlopSquare) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
mAlwaysInTapRegion = false;
mHandler.removeMessages(TAP);
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
}
if (distance > mTouchSlopSquare) {
mAlwaysInBiggerTapRegion = false;
}
} else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
}
break;
case MotionEvent.ACTION_UP:
mStillDown = false;
MotionEvent currentUpEvent = MotionEvent.obtain(ev);
if (mIsDoubleTapping) {
// Finally, give the up event of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else if (mInLongPress) {
mHandler.removeMessages(TAP);
mInLongPress = false;
} else if (mAlwaysInTapRegion) {
handled = mListener.onSingleTapUp(ev);
if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
mDoubleTapListener.onSingleTapConfirmed(ev);
}
} else {
// A fling must travel the minimum tap distance
final VelocityTracker velocityTracker = mVelocityTracker;
final int pointerId = ev.getPointerId(0);
velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
final float velocityY = VelocityTrackerCompat.getYVelocity(
velocityTracker, pointerId);
final float velocityX = VelocityTrackerCompat.getXVelocity(
velocityTracker, pointerId);
if ((Math.abs(velocityY) > mMinimumFlingVelocity)
|| (Math.abs(velocityX) > mMinimumFlingVelocity)){
handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
}
}
if (mPreviousUpEvent != null) {
mPreviousUpEvent.recycle();
}
// Hold the event we obtained above - listeners may have changed the original.
mPreviousUpEvent = currentUpEvent;
if (mVelocityTracker != null) {
// This may have been cleared when we called out to the
// application above.
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mIsDoubleTapping = false;
mDeferConfirmSingleTap = false;
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
break;
case MotionEvent.ACTION_CANCEL:
cancel();
break;
}
return handled;
}
private void cancel() {
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
mHandler.removeMessages(TAP);
mVelocityTracker.recycle();
mVelocityTracker = null;
mIsDoubleTapping = false;
mStillDown = false;
mAlwaysInTapRegion = false;
mAlwaysInBiggerTapRegion = false;
mDeferConfirmSingleTap = false;
if (mInLongPress) {
mInLongPress = false;
}
}
private void cancelTaps() {
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
mHandler.removeMessages(TAP);
mIsDoubleTapping = false;
mAlwaysInTapRegion = false;
mAlwaysInBiggerTapRegion = false;
mDeferConfirmSingleTap = false;
if (mInLongPress) {
mInLongPress = false;
}
}
private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
MotionEvent secondDown) {
if (!mAlwaysInBiggerTapRegion) {
return false;
}
if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
return false;
}
int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
}
void dispatchLongPress() {
mHandler.removeMessages(TAP);
mDeferConfirmSingleTap = false;
mInLongPress = true;
mListener.onLongPress(mCurrentDownEvent);
}
}
static class GestureDetectorCompatImplJellybeanMr2 implements GestureDetectorCompatImpl {
private final GestureDetector mDetector;
public GestureDetectorCompatImplJellybeanMr2(Context context, GestureDetector.OnGestureListener listener,
Handler handler) {
mDetector = new GestureDetector(context, listener, handler);
}
@Override
public boolean isLongpressEnabled() {
return mDetector.isLongpressEnabled();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return mDetector.onTouchEvent(ev);
}
@Override
public void setIsLongpressEnabled(boolean enabled) {
mDetector.setIsLongpressEnabled(enabled);
}
@Override
public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener listener) {
mDetector.setOnDoubleTapListener(listener);
}
}
private final GestureDetectorCompatImpl mImpl;
/**
* Creates a GestureDetectorCompat with the supplied listener.
* As usual, you may only use this constructor from a UI thread.
* @see android.os.Handler#Handler()
*
* @param context the application's context
* @param listener the listener invoked for all the callbacks, this must
* not be null.
*/
public GestureDetectorCompat(Context context, GestureDetector.OnGestureListener listener) {
this(context, listener, null);
}
/**
* Creates a GestureDetectorCompat with the supplied listener.
* As usual, you may only use this constructor from a UI thread.
* @see android.os.Handler#Handler()
*
* @param context the application's context
* @param listener the listener invoked for all the callbacks, this must
* not be null.
* @param handler the handler that will be used for posting deferred messages
*/
public GestureDetectorCompat(Context context, GestureDetector.OnGestureListener listener, Handler handler) {
if (Build.VERSION.SDK_INT > 17) {
mImpl = new GestureDetectorCompatImplJellybeanMr2(context, listener, handler);
} else {
mImpl = new GestureDetectorCompatImplBase(context, listener, handler);
}
}
/**
* @return true if longpress is enabled, else false.
*/
public boolean isLongpressEnabled() {
return mImpl.isLongpressEnabled();
}
/**
* Analyzes the given motion event and if applicable triggers the
* appropriate callbacks on the {@link GestureDetector.OnGestureListener} supplied.
*
* @param event The current motion event.
* @return true if the {@link GestureDetector.OnGestureListener} consumed the event,
* else false.
*/
public boolean onTouchEvent(MotionEvent event) {
return mImpl.onTouchEvent(event);
}
/**
* Set whether longpress is enabled, if this is enabled when a user
* presses and holds down you get a longpress event and nothing further.
* If it's disabled the user can press and hold down and then later
* moved their finger and you will get scroll events. By default
* longpress is enabled.
*
* @param enabled whether longpress should be enabled.
*/
public void setIsLongpressEnabled(boolean enabled) {
mImpl.setIsLongpressEnabled(enabled);
}
/**
* Sets the listener which will be called for double-tap and related
* gestures.
*
* @param listener the listener invoked for all the callbacks, or
* null to stop listening for double-tap gestures.
*/
public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener listener) {
mImpl.setOnDoubleTapListener(listener);
}
}
package recyclerview;
import android.support.v7.widget.RecyclerView;
import android.view.HapticFeedbackConstants;
import android.view.SoundEffectConstants;
import android.view.View;
import com.healthsignz.consumer.R;
public class ItemClickSupport {
/**
* Interface definition for a callback to be invoked when an item in the
* RecyclerView has been clicked.
*/
public interface OnItemClickListener {
/**
* Callback method to be invoked when an item in the RecyclerView
* has been clicked.
*
* @param parent The RecyclerView where the click happened.
* @param view The view within the RecyclerView that was clicked
* @param position The position of the view in the adapter.
* @param id The row id of the item that was clicked.
*/
void onItemClick(RecyclerView parent, View view, int position, long id);
}
/**
* Interface definition for a callback to be invoked when an item in the
* RecyclerView has been clicked and held.
*/
public interface OnItemLongClickListener {
/**
* Callback method to be invoked when an item in the RecyclerView
* has been clicked and held.
*
* @param parent The RecyclerView where the click happened
* @param view The view within the RecyclerView that was clicked
* @param position The position of the view in the list
* @param id The row id of the item that was clicked
*
* @return true if the callback consumed the long click, false otherwise
*/
boolean onItemLongClick(RecyclerView parent, View view, int position, long id);
}
private final RecyclerView mRecyclerView;
private final TouchListener mTouchListener;
private OnItemClickListener mItemClickListener;
private OnItemLongClickListener mItemLongClickListener;
private ItemClickSupport(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
mTouchListener = new TouchListener(recyclerView);
recyclerView.addOnItemTouchListener(mTouchListener);
}
/**
* Register a callback to be invoked when an item in the
* RecyclerView has been clicked.
*
* @param listener The callback that will be invoked.
*/
public void setOnItemClickListener(OnItemClickListener listener) {
mItemClickListener = listener;
}
/**
* Register a callback to be invoked when an item in the
* RecyclerView has been clicked and held.
*
* @param listener The callback that will be invoked.
*/
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
if (!mRecyclerView.isLongClickable()) {
mRecyclerView.setLongClickable(true);
}
mItemLongClickListener = listener;
}
public static ItemClickSupport addTo(RecyclerView recyclerView) {
ItemClickSupport itemClickSupport = from(recyclerView);
if (itemClickSupport == null) {
itemClickSupport = new ItemClickSupport(recyclerView);
recyclerView.setTag(R.id.recyclerview_item_click_support, itemClickSupport);
} else {
// TODO: Log warning
}
return itemClickSupport;
}
public static void removeFrom(RecyclerView recyclerView) {
final ItemClickSupport itemClickSupport = from(recyclerView);
if (itemClickSupport == null) {
// TODO: Log warning
return;
}
recyclerView.removeOnItemTouchListener(itemClickSupport.mTouchListener);
recyclerView.setTag(R.id.recyclerview_item_click_support, null);
}
public static ItemClickSupport from(RecyclerView recyclerView) {
if (recyclerView == null) {
return null;
}
return (ItemClickSupport) recyclerView.getTag(R.id.recyclerview_item_click_support);
}
private class TouchListener extends ClickItemTouchListener {
TouchListener(RecyclerView recyclerView) {
super(recyclerView);
}
@Override
boolean performItemClick(RecyclerView parent, View view, int position, long id) {
if (mItemClickListener != null) {
view.playSoundEffect(SoundEffectConstants.CLICK);
mItemClickListener.onItemClick(parent, view, position, id);
return true;
}
return false;
}
@Override
boolean performItemLongClick(RecyclerView parent, View view, int position, long id) {
if (mItemLongClickListener != null) {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
return mItemLongClickListener.onItemLongClick(parent, view, position, id);
}
return false;
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
}
}
package recyclerview;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.util.LongSparseArray;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.Adapter;
import android.util.SparseBooleanArray;
import android.view.View;
import android.widget.Checkable;
import com.healthsignz.consumer.R;
import static android.os.Build.VERSION_CODES.HONEYCOMB;
public class ItemSelectionSupport {
public static final int INVALID_POSITION = -1;
public static enum ChoiceMode {
NONE,
SINGLE,
MULTIPLE
}
private final RecyclerView mRecyclerView;
private final TouchListener mTouchListener;
private ChoiceMode mChoiceMode = ChoiceMode.NONE;
private CheckedStates mCheckedStates;
private CheckedIdStates mCheckedIdStates;
private int mCheckedCount;
private static final String STATE_KEY_CHOICE_MODE = "choiceMode";
private static final String STATE_KEY_CHECKED_STATES = "checkedStates";
private static final String STATE_KEY_CHECKED_ID_STATES = "checkedIdStates";
private static final String STATE_KEY_CHECKED_COUNT = "checkedCount";
private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
private ItemSelectionSupport(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
mTouchListener = new TouchListener(recyclerView);
recyclerView.addOnItemTouchListener(mTouchListener);
}
private void updateOnScreenCheckedViews() {
final int count = mRecyclerView.getChildCount();
for (int i = 0; i < count; i++) {
final View child = mRecyclerView.getChildAt(i);
final int position = mRecyclerView.getChildPosition(child);
setViewChecked(child, mCheckedStates.get(position));
}
}
/**
* Returns the number of items currently selected. This will only be valid
* if the choice mode is not {@link ChoiceMode#NONE} (default).
*
* <p>To determine the specific items that are currently selected, use one of
* the <code>getChecked*</code> methods.
*
* @return The number of items currently selected
*
* @see #getCheckedItemPosition()
* @see #getCheckedItemPositions()
* @see #getCheckedItemIds()
*/
public int getCheckedItemCount() {
return mCheckedCount;
}
/**
* Returns the checked state of the specified position. The result is only
* valid if the choice mode has been set to {@link ChoiceMode#SINGLE}
* or {@link ChoiceMode#MULTIPLE}.
*
* @param position The item whose checked state to return
* @return The item's checked state or <code>false</code> if choice mode
* is invalid
*
* @see #setChoiceMode(ChoiceMode)
*/
public boolean isItemChecked(int position) {
if (mChoiceMode != ChoiceMode.NONE && mCheckedStates != null) {
return mCheckedStates.get(position);
}
return false;
}
/**
* Returns the currently checked item. The result is only valid if the choice
* mode has been set to {@link ChoiceMode#SINGLE}.
*
* @return The position of the currently checked item or
* {@link #INVALID_POSITION} if nothing is selected
*
* @see #setChoiceMode(ChoiceMode)
*/
public int getCheckedItemPosition() {
if (mChoiceMode == ChoiceMode.SINGLE && mCheckedStates != null && mCheckedStates.size() == 1) {
return mCheckedStates.keyAt(0);
}
return INVALID_POSITION;
}
/**
* Returns the set of checked items in the list. The result is only valid if
* the choice mode has not been set to {@link ChoiceMode#NONE}.
*
* @return A SparseBooleanArray which will return true for each call to
* get(int position) where position is a position in the list,
* or <code>null</code> if the choice mode is set to
* {@link ChoiceMode#NONE}.
*/
public SparseBooleanArray getCheckedItemPositions() {
if (mChoiceMode != ChoiceMode.NONE) {
return mCheckedStates;
}
return null;
}
/**
* Returns the set of checked items ids. The result is only valid if the
* choice mode has not been set to {@link ChoiceMode#NONE} and the adapter
* has stable IDs.
*
* @return A new array which contains the id of each checked item in the
* list.
*
* @see android.support.v7.widget.RecyclerView.Adapter#hasStableIds()
*/
public long[] getCheckedItemIds() {
if (mChoiceMode == ChoiceMode.NONE
|| mCheckedIdStates == null || mRecyclerView.getAdapter() == null) {
return new long[0];
}
final int count = mCheckedIdStates.size();
final long[] ids = new long[count];
for (int i = 0; i < count; i++) {
ids[i] = mCheckedIdStates.keyAt(i);
}
return ids;
}
/**
* Sets the checked state of the specified position. The is only valid if
* the choice mode has been set to {@link ChoiceMode#SINGLE} or
* {@link ChoiceMode#MULTIPLE}.
*
* @param position The item whose checked state is to be checked
* @param checked The new checked state for the item
*/
public void setItemChecked(int position, boolean checked) {
if (mChoiceMode == ChoiceMode.NONE) {
return;
}
final Adapter adapter = mRecyclerView.getAdapter();
if (mChoiceMode == ChoiceMode.MULTIPLE) {
boolean oldValue = mCheckedStates.get(position);
mCheckedStates.put(position, checked);
if (mCheckedIdStates != null && adapter.hasStableIds()) {
if (checked) {
mCheckedIdStates.put(adapter.getItemId(position), position);
} else {
mCheckedIdStates.delete(adapter.getItemId(position));
}
}
if (oldValue != checked) {
if (checked) {
mCheckedCount++;
} else {
mCheckedCount--;
}
}
} else {
boolean updateIds = mCheckedIdStates != null && adapter.hasStableIds();
// Clear all values if we're checking something, or unchecking the currently
// selected item
if (checked || isItemChecked(position)) {
mCheckedStates.clear();
if (updateIds) {
mCheckedIdStates.clear();
}
}
// This may end up selecting the checked we just cleared but this way
// we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
if (checked) {
mCheckedStates.put(position, true);
if (updateIds) {
mCheckedIdStates.put(adapter.getItemId(position), position);
}
mCheckedCount = 1;
} else if (mCheckedStates.size() == 0 || !mCheckedStates.valueAt(0)) {
mCheckedCount = 0;
}
}
updateOnScreenCheckedViews();
}
@TargetApi(HONEYCOMB)
public void setViewChecked(View view, boolean checked) {
if (view instanceof Checkable) {
((Checkable) view).setChecked(checked);
} else if (Build.VERSION.SDK_INT >= HONEYCOMB) {
view.setActivated(checked);
}
}
/**
* Clears any choices previously set.
*/
public void clearChoices() {
if (mCheckedStates != null) {
mCheckedStates.clear();
}
if (mCheckedIdStates != null) {
mCheckedIdStates.clear();
}
mCheckedCount = 0;
updateOnScreenCheckedViews();
}
/**
* Returns the current choice mode.
*
* @see #setChoiceMode(ChoiceMode)
*/
public ChoiceMode getChoiceMode() {
return mChoiceMode;
}
/**
* Defines the choice behavior for the List. By default, Lists do not have any choice behavior
* ({@link ChoiceMode#NONE}). By setting the choiceMode to {@link ChoiceMode#SINGLE}, the
* List allows up to one item to be in a chosen state. By setting the choiceMode to
* {@link ChoiceMode#MULTIPLE}, the list allows any number of items to be chosen.
*
* @param choiceMode One of {@link ChoiceMode#NONE}, {@link ChoiceMode#SINGLE}, or
* {@link ChoiceMode#MULTIPLE}
*/
public void setChoiceMode(ChoiceMode choiceMode) {
if (mChoiceMode == choiceMode) {
return;
}
mChoiceMode = choiceMode;
if (mChoiceMode != ChoiceMode.NONE) {
if (mCheckedStates == null) {
mCheckedStates = new CheckedStates();
}
final Adapter adapter = mRecyclerView.getAdapter();
if (mCheckedIdStates == null && adapter != null && adapter.hasStableIds()) {
mCheckedIdStates = new CheckedIdStates();
}
}
}
public void onAdapterDataChanged() {
final Adapter adapter = mRecyclerView.getAdapter();
if (mChoiceMode == ChoiceMode.NONE || adapter == null || !adapter.hasStableIds()) {
return;
}
final int itemCount = adapter.getItemCount();
// Clear out the positional check states, we'll rebuild it below from IDs.
mCheckedStates.clear();
for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
final long currentId = mCheckedIdStates.keyAt(checkedIndex);
final int currentPosition = mCheckedIdStates.valueAt(checkedIndex);
final long newPositionId = adapter.getItemId(currentPosition);
if (currentId != newPositionId) {
// Look around to see if the ID is nearby. If not, uncheck it.
final int start = Math.max(0, currentPosition - CHECK_POSITION_SEARCH_DISTANCE);
final int end = Math.min(currentPosition + CHECK_POSITION_SEARCH_DISTANCE, itemCount);
boolean found = false;
for (int searchPos = start; searchPos < end; searchPos++) {
final long searchId = adapter.getItemId(searchPos);
if (currentId == searchId) {
found = true;
mCheckedStates.put(searchPos, true);
mCheckedIdStates.setValueAt(checkedIndex, searchPos);
break;
}
}
if (!found) {
mCheckedIdStates.delete(currentId);
mCheckedCount--;
checkedIndex--;
}
} else {
mCheckedStates.put(currentPosition, true);
}
}
}
public Bundle onSaveInstanceState() {
final Bundle state = new Bundle();
state.putInt(STATE_KEY_CHOICE_MODE, mChoiceMode.ordinal());
state.putParcelable(STATE_KEY_CHECKED_STATES, mCheckedStates);
state.putParcelable(STATE_KEY_CHECKED_ID_STATES, mCheckedIdStates);
state.putInt(STATE_KEY_CHECKED_COUNT, mCheckedCount);
return state;
}
public void onRestoreInstanceState(Bundle state) {
mChoiceMode = ChoiceMode.values()[state.getInt(STATE_KEY_CHOICE_MODE)];
mCheckedStates = state.getParcelable(STATE_KEY_CHECKED_STATES);
mCheckedIdStates = state.getParcelable(STATE_KEY_CHECKED_ID_STATES);
mCheckedCount = state.getInt(STATE_KEY_CHECKED_COUNT);
// TODO confirm ids here
}
public static ItemSelectionSupport addTo(RecyclerView recyclerView) {
ItemSelectionSupport itemSelectionSupport = from(recyclerView);
if (itemSelectionSupport == null) {
itemSelectionSupport = new ItemSelectionSupport(recyclerView);
recyclerView.setTag(R.id.recyclerview_item_selection_support, itemSelectionSupport);
} else {
// TODO: Log warning
}
return itemSelectionSupport;
}
public static void removeFrom(RecyclerView recyclerView) {
final ItemSelectionSupport itemSelection = from(recyclerView);
if (itemSelection == null) {
// TODO: Log warning
return;
}
itemSelection.clearChoices();
recyclerView.removeOnItemTouchListener(itemSelection.mTouchListener);
recyclerView.setTag(R.id.recyclerview_item_selection_support, null);
}
public static ItemSelectionSupport from(RecyclerView recyclerView) {
if (recyclerView == null) {
return null;
}
return (ItemSelectionSupport) recyclerView.getTag(R.id.recyclerview_item_selection_support);
}
private static class CheckedStates extends SparseBooleanArray implements Parcelable {
private static final int FALSE = 0;
private static final int TRUE = 1;
public CheckedStates() {
super();
}
private CheckedStates(Parcel in) {
final int size = in.readInt();
if (size > 0) {
for (int i = 0; i < size; i++) {
final int key = in.readInt();
final boolean value = (in.readInt() == TRUE);
put(key, value);
}
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
final int size = size();
parcel.writeInt(size);
for (int i = 0; i < size; i++) {
parcel.writeInt(keyAt(i));
parcel.writeInt(valueAt(i) ? TRUE : FALSE);
}
}
public static final Parcelable.Creator<CheckedStates> CREATOR
= new Parcelable.Creator<CheckedStates>() {
@Override
public CheckedStates createFromParcel(Parcel in) {
return new CheckedStates(in);
}
@Override
public CheckedStates[] newArray(int size) {
return new CheckedStates[size];
}
};
}
private static class CheckedIdStates extends LongSparseArray<Integer> implements Parcelable {
public CheckedIdStates() {
super();
}
private CheckedIdStates(Parcel in) {
final int size = in.readInt();
if (size > 0) {
for (int i = 0; i < size; i++) {
final long key = in.readLong();
final int value = in.readInt();
put(key, value);
}
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
final int size = size();
parcel.writeInt(size);
for (int i = 0; i < size; i++) {
parcel.writeLong(keyAt(i));
parcel.writeInt(valueAt(i));
}
}
public static final Creator<CheckedIdStates> CREATOR
= new Creator<CheckedIdStates>() {
@Override
public CheckedIdStates createFromParcel(Parcel in) {
return new CheckedIdStates(in);
}
@Override
public CheckedIdStates[] newArray(int size) {
return new CheckedIdStates[size];
}
};
}
private class TouchListener extends ClickItemTouchListener {
TouchListener(RecyclerView recyclerView) {
super(recyclerView);
}
@Override
boolean performItemClick(RecyclerView parent, View view, int position, long id) {
final Adapter adapter = mRecyclerView.getAdapter();
boolean checkedStateChanged = false;
if (mChoiceMode == ChoiceMode.MULTIPLE) {
boolean checked = !mCheckedStates.get(position, false);
mCheckedStates.put(position, checked);
if (mCheckedIdStates != null && adapter.hasStableIds()) {
if (checked) {
mCheckedIdStates.put(adapter.getItemId(position), position);
} else {
mCheckedIdStates.delete(adapter.getItemId(position));
}
}
if (checked) {
mCheckedCount++;
} else {
mCheckedCount--;
}
checkedStateChanged = true;
} else if (mChoiceMode == ChoiceMode.SINGLE) {
boolean checked = !mCheckedStates.get(position, false);
if (checked) {
mCheckedStates.clear();
mCheckedStates.put(position, true);
if (mCheckedIdStates != null && adapter.hasStableIds()) {
mCheckedIdStates.clear();
mCheckedIdStates.put(adapter.getItemId(position), position);
}
mCheckedCount = 1;
} else if (mCheckedStates.size() == 0 || !mCheckedStates.valueAt(0)) {
mCheckedCount = 0;
}
checkedStateChanged = true;
}
if (checkedStateChanged) {
updateOnScreenCheckedViews();
}
return false;
}
@Override
boolean performItemLongClick(RecyclerView parent, View view, int position, long id) {
return true;
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment