Created
September 19, 2017 10:52
-
-
Save prasannaboppe/e7ed52e66414a3c4bd56b88414bf4ce5 to your computer and use it in GitHub Desktop.
RecyclerView Utils
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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