package io.virtualapp.widgets; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.os.Handler; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import io.virtualapp.R; /** * @author Aidan Follestad (afollestad) */ public class DragSelectRecyclerView extends RecyclerView { private static final boolean LOGGING = false; private static final int AUTO_SCROLL_DELAY = 25; private int mLastDraggedIndex = -1; private DragSelectRecyclerViewAdapter<?> mAdapter; private int mInitialSelection; private boolean mDragSelectActive; private int mMinReached; private int mMaxReached; private int mHotspotHeight; private int mHotspotOffsetTop; private int mHotspotOffsetBottom; private int mHotspotTopBoundStart; private int mHotspotTopBoundEnd; private int mHotspotBottomBoundStart; private int mHotspotBottomBoundEnd; private int mAutoScrollVelocity; private FingerListener mFingerListener; private boolean mInTopHotspot; private boolean mInBottomHotspot; private Handler mAutoScrollHandler; private Runnable mAutoScrollRunnable = new Runnable() { @Override public void run() { if (mAutoScrollHandler == null) return; if (mInTopHotspot) { scrollBy(0, -mAutoScrollVelocity); mAutoScrollHandler.postDelayed(this, AUTO_SCROLL_DELAY); } else if (mInBottomHotspot) { scrollBy(0, mAutoScrollVelocity); mAutoScrollHandler.postDelayed(this, AUTO_SCROLL_DELAY); } } }; private RectF mTopBoundRect; private RectF mBottomBoundRect; private Paint mDebugPaint; private boolean mDebugEnabled = false; public DragSelectRecyclerView(Context context) { super(context); init(context, null); } public DragSelectRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public DragSelectRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } private static void LOG(String message, Object... args) { //noinspection PointlessBooleanExpression if (!LOGGING) return; if (args != null) { Log.d("DragSelectRecyclerView", String.format(message, args)); } else { Log.d("DragSelectRecyclerView", message); } } private void init(Context context, AttributeSet attrs) { mAutoScrollHandler = new Handler(); final int defaultHotspotHeight = context.getResources().getDimensionPixelSize(R.dimen.dsrv_defaultHotspotHeight); if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DragSelectRecyclerView, 0, 0); try { boolean autoScrollEnabled = a.getBoolean(R.styleable.DragSelectRecyclerView_dsrv_autoScrollEnabled, true); if (!autoScrollEnabled) { mHotspotHeight = -1; mHotspotOffsetTop = -1; mHotspotOffsetBottom = -1; LOG("Auto-scroll disabled"); } else { mHotspotHeight = a.getDimensionPixelSize( R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspotHeight, defaultHotspotHeight); mHotspotOffsetTop = a.getDimensionPixelSize( R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspot_offsetTop, 0); mHotspotOffsetBottom = a.getDimensionPixelSize( R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspot_offsetBottom, 0); LOG("Hotspot height = %d", mHotspotHeight); } } finally { a.recycle(); } } else { mHotspotHeight = defaultHotspotHeight; LOG("Hotspot height = %d", mHotspotHeight); } } public void setFingerListener(@Nullable FingerListener listener) { this.mFingerListener = listener; } @Override protected void onMeasure(int widthSpec, int heightSpec) { super.onMeasure(widthSpec, heightSpec); if (mHotspotHeight > -1) { mHotspotTopBoundStart = mHotspotOffsetTop; mHotspotTopBoundEnd = mHotspotOffsetTop + mHotspotHeight; mHotspotBottomBoundStart = (getMeasuredHeight() - mHotspotHeight) - mHotspotOffsetBottom; mHotspotBottomBoundEnd = getMeasuredHeight() - mHotspotOffsetBottom; LOG("RecyclerView height = %d", getMeasuredHeight()); LOG("Hotspot top bound = %d to %d", mHotspotTopBoundStart, mHotspotTopBoundStart); LOG("Hotspot bottom bound = %d to %d", mHotspotBottomBoundStart, mHotspotBottomBoundEnd); } } public boolean setDragSelectActive(boolean active, int initialSelection) { if (active && mDragSelectActive) { LOG("Drag selection is already active."); return false; } mLastDraggedIndex = -1; mMinReached = -1; mMaxReached = -1; if (!mAdapter.isIndexSelectable(initialSelection)) { mDragSelectActive = false; mInitialSelection = -1; mLastDraggedIndex = -1; LOG("Index %d is not selectable.", initialSelection); return false; } mAdapter.setSelected(initialSelection, true); mDragSelectActive = active; mInitialSelection = initialSelection; mLastDraggedIndex = initialSelection; if (mFingerListener != null) mFingerListener.onDragSelectFingerAction(true); LOG("Drag selection initialized, starting at index %d.", initialSelection); return true; } /** * Use {@link #setAdapter(DragSelectRecyclerViewAdapter)} instead. */ @Override @Deprecated public void setAdapter(Adapter adapter) { if (!(adapter instanceof DragSelectRecyclerViewAdapter<?>)) throw new IllegalArgumentException("Adapter must be a DragSelectRecyclerViewAdapter."); setAdapter((DragSelectRecyclerViewAdapter<?>) adapter); } public void setAdapter(DragSelectRecyclerViewAdapter<?> adapter) { super.setAdapter(adapter); mAdapter = adapter; } private int getItemPosition(MotionEvent e) { final View v = findChildViewUnder(e.getX(), e.getY()); if (v == null) return NO_POSITION; if (v.getTag() == null || !(v.getTag() instanceof ViewHolder)) throw new IllegalStateException("Make sure your adapter makes a call to super.onBindViewHolder(), and doesn't override itemView tags."); final ViewHolder holder = (ViewHolder) v.getTag(); return holder.getAdapterPosition(); } public final void enableDebug() { mDebugEnabled = true; invalidate(); } @Override public void onDraw(Canvas c) { super.onDraw(c); if (mDebugEnabled) { if (mDebugPaint == null) { mDebugPaint = new Paint(); mDebugPaint.setColor(Color.BLACK); mDebugPaint.setAntiAlias(true); mDebugPaint.setStyle(Paint.Style.FILL); mTopBoundRect = new RectF(0, mHotspotTopBoundStart, getMeasuredWidth(), mHotspotTopBoundEnd); mBottomBoundRect = new RectF(0, mHotspotBottomBoundStart, getMeasuredWidth(), mHotspotBottomBoundEnd); } c.drawRect(mTopBoundRect, mDebugPaint); c.drawRect(mBottomBoundRect, mDebugPaint); } } @Override public boolean dispatchTouchEvent(MotionEvent e) { if (mAdapter.getItemCount() == 0) return super.dispatchTouchEvent(e); if (mDragSelectActive) { if (e.getAction() == MotionEvent.ACTION_UP) { mDragSelectActive = false; mInTopHotspot = false; mInBottomHotspot = false; mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); if (mFingerListener != null) mFingerListener.onDragSelectFingerAction(false); return true; } else if (e.getAction() == MotionEvent.ACTION_MOVE) { // Check for auto-scroll hotspot if (mHotspotHeight > -1) { if (e.getY() >= mHotspotTopBoundStart && e.getY() <= mHotspotTopBoundEnd) { mInBottomHotspot = false; if (!mInTopHotspot) { mInTopHotspot = true; LOG("Now in TOP hotspot"); mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); mAutoScrollHandler.postDelayed(mAutoScrollRunnable, AUTO_SCROLL_DELAY); } final float simulatedFactor = mHotspotTopBoundEnd - mHotspotTopBoundStart; final float simulatedY = e.getY() - mHotspotTopBoundStart; mAutoScrollVelocity = (int) (simulatedFactor - simulatedY) / 2; LOG("Auto scroll velocity = %d", mAutoScrollVelocity); } else if (e.getY() >= mHotspotBottomBoundStart && e.getY() <= mHotspotBottomBoundEnd) { mInTopHotspot = false; if (!mInBottomHotspot) { mInBottomHotspot = true; LOG("Now in BOTTOM hotspot"); mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); mAutoScrollHandler.postDelayed(mAutoScrollRunnable, AUTO_SCROLL_DELAY); } final float simulatedY = e.getY() + mHotspotBottomBoundEnd; final float simulatedFactor = mHotspotBottomBoundStart + mHotspotBottomBoundEnd; mAutoScrollVelocity = (int) (simulatedY - simulatedFactor) / 2; LOG("Auto scroll velocity = %d", mAutoScrollVelocity); } else if (mInTopHotspot || mInBottomHotspot) { LOG("Left the hotspot"); mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); mInTopHotspot = false; mInBottomHotspot = false; } } // Drag selection logic // NOTE: DISABLE IT // if (itemPosition != NO_POSITION && mLastDraggedIndex != itemPosition) { // mLastDraggedIndex = itemPosition; // if (mMinReached == -1) mMinReached = mLastDraggedIndex; // if (mMaxReached == -1) mMaxReached = mLastDraggedIndex; // if (mLastDraggedIndex > mMaxReached) // mMaxReached = mLastDraggedIndex; // if (mLastDraggedIndex < mMinReached) // mMinReached = mLastDraggedIndex; // if (mAdapter != null) // mAdapter.selectRange(mInitialSelection, mLastDraggedIndex, mMinReached, mMaxReached); // if (mInitialSelection == mLastDraggedIndex) { // mMinReached = mLastDraggedIndex; // mMaxReached = mLastDraggedIndex; // } // } return true; } } return super.dispatchTouchEvent(e); } public interface FingerListener { void onDragSelectFingerAction(boolean fingerDown); } }