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);
}
}