Skip to content

Instantly share code, notes, and snippets.

@markus2610
Forked from mgarnerdev/PagingRecyclerView
Created March 23, 2017 13:39
Show Gist options
  • Save markus2610/f09576e50da90712a6fe4c08e0a84b24 to your computer and use it in GitHub Desktop.
Save markus2610/f09576e50da90712a6fe4c08e0a84b24 to your computer and use it in GitHub Desktop.
Horizontal Snapping RecyclerView with Page Indication
import android.content.Context;
import android.graphics.PointF;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.MotionEvent;
/**
* Created by mgarner on 11/2/2015.
*/
public class PagingRecyclerView extends RecyclerView {
private LayoutManager mLayout;
private boolean mHorizontalScroll = false;
private Adapter mAdapter;
private boolean mScrollingFurther = false;
private float mTouchDownX = 0;
private float mTouchDownY = 0;
private boolean mTouchHandled = true;
public PagingRecyclerView(Context context) {
super(context);
}
public PagingRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PagingRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private void init() {
mLayout = getLayoutManager();
mAdapter = getAdapter();
if (mLayout != null) {
mHorizontalScroll = mLayout.canScrollHorizontally();
}
}
@Override
public void setLayoutManager(LayoutManager layout) {
super.setLayoutManager(layout);
init();
}
@Override
public boolean fling(int velocityX, int velocityY) {
boolean scrollFurther;
if (mHorizontalScroll) {
scrollFurther = velocityX > 0;
} else {
scrollFurther = velocityY > 0;
}
if (scrollFurther) {
if (mAdapter != null) {
pageMore();
}
} else {
pageLess();
}
return super.fling(0, 0);
}
private void pageLess() {
int newPosition = getCurrentPosition() - 1;
//Ensure the next position is >= 0
smoothScrollToPosition(newPosition < 0 ? 0 : newPosition);
}
private void pageMore() {
int newPosition = getCurrentPosition() + 1;
//Ensure the next position is not greater than the adapter's count.
smoothScrollToPosition(newPosition <= mAdapter.getItemCount() ? newPosition : mAdapter.getItemCount());
}
private int getCurrentPosition() {
//Assumes full-width / full-height children in recycler view adapter.
int currentPosition;
if (mHorizontalScroll) {
currentPosition = Math.round((computeHorizontalScrollOffset() * 1.0f) / (getWidth() * 1.0f));
} else {
currentPosition = Math.round((computeVerticalScrollOffset() * 1.0f) / (getHeight() * 1.0f));
}
return currentPosition;
}
@Override
public void onScrollStateChanged(int newState) {
// Make sure scrolling has stopped before scrolling to correct position.
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
int currentRoundedPosition = getCurrentPosition();
smoothScrollToPosition(currentRoundedPosition);
}
super.onScrollStateChanged(newState);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
float currX = e.getX();
float currY = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
mTouchHandled = false;
mTouchDownX = e.getX();
mTouchDownY = e.getY();
break;
case MotionEvent.ACTION_UP:
if (!mTouchHandled) {
mTouchHandled = true;
if (mHorizontalScroll) {
if (mTouchDownX < currX) {
//right swipe for left scroll
pageLess();
return false;
} else if (currX < mTouchDownX) {
//left swipe for right scroll
pageMore();
return false;
}
} else {
if (mTouchDownY < currY) {
//down swipe for up scroll
pageLess();
return false;
} else if (currY < mTouchDownY) {
//up swipe for down scroll
pageMore();
return false;
}
}
}
}
return super.onTouchEvent(e);
}
//As seen here: http://stackoverflow.com/questions/26370289/snappy-scrolling-in-recyclerview
public void smoothScrollToPosition(int position) {
// A good idea would be to create this instance in some initialization method, and just set the target position in this method.
LinearSmoothScroller smoothScroller = new LinearSmoothScroller(getContext()) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
int yDelta = calculateCurrentDistanceToPosition(targetPosition);
return new PointF(0, yDelta);
}
private int calculateCurrentDistanceToPosition(int targetPosition) {
if (mScrollingFurther) {
return getWidth() * (getCurrentPosition() + targetPosition);
} else {
return getWidth() * (getCurrentPosition() - targetPosition);
}
}
// This is the important method. This code will return the amount of time it takes to scroll 1 pixel.
// This code will request X milliseconds for every Y DP units.
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return 1.0f / TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, displayMetrics);
}
};
smoothScroller.setTargetPosition(position);
mLayout.startSmoothScroll(smoothScroller);
}
}
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by mgarner on 10/30/2015.
* RecyclerViewPositionIndicator based on WideCirclePageIndicator based on Jake Wharton's CirclePageIndicator.
*/
public class RecyclerViewPositionIndicator extends View {
private RecyclerView mRecyclerView;
private Paint mCircleStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint mCircleFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private float mRadius;
private RecyclerView.Adapter mAdapter = null;
private int mCurrentPosition;
private float mSpacing;
private int mSpacingFactor;
public RecyclerViewPositionIndicator(Context context) {
super(context);
}
public RecyclerViewPositionIndicator(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.vpiCirclePageIndicatorStyle);
}
public RecyclerViewPositionIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
public void init(Context context, AttributeSet attrs, int defStyle) {
final Resources res = getResources();
final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color);
final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color);
final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width);
final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius);
final int defaultSpacingFactor = res.getInteger(R.integer.default_circle_indicator_spacing_factor);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerViewPositionIndicator, defStyle, 0);
try {
mCircleStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCircleStrokePaint.setColor(a.getColor(R.styleable.RecyclerViewPositionIndicator_rvpi_stroke_color, defaultStrokeColor));
mCircleStrokePaint.setStyle(Paint.Style.STROKE);
mCircleStrokePaint.setStrokeWidth(a.getDimension(R.styleable.RecyclerViewPositionIndicator_rvpi_stroke_width, defaultStrokeWidth));
mCircleFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCircleFillPaint.setColor(a.getColor(R.styleable.RecyclerViewPositionIndicator_rvpi_fill_color, defaultFillColor));
mCircleFillPaint.setStyle(Paint.Style.FILL);
mRadius = a.getDimension(R.styleable.RecyclerViewPositionIndicator_rvpi_radius, defaultRadius);
mSpacingFactor = a.getInteger(R.styleable.RecyclerViewPositionIndicator_rvpi_spacing_factor, defaultSpacingFactor);
mSpacing = mRadius * mSpacingFactor;//4x the radius of circles between each circle. (2 circles)
} finally {
a.recycle();
}
}
public void setRecyclerView(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
if (mRecyclerView.getAdapter() != null) {
mAdapter = mRecyclerView.getAdapter();
initRecylerViewListeners();
invalidate();
}
}
private void initRecylerViewListeners() {
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
mCurrentPosition = Math.round((recyclerView.computeHorizontalScrollOffset() * 1.0f) / (recyclerView.getWidth() * 1.0f));
setCurrentIndicatorPosition(mCurrentPosition);
}
}
});
}
public RecyclerView getRecyclerView() {
return mRecyclerView;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mRecyclerView == null || mAdapter == null) {
return;
}
final int totalPageCount = mAdapter.getItemCount();
if (mCurrentPosition >= totalPageCount) {
setCurrentIndicatorPosition(totalPageCount - 1);
return;
}
float shortOffset = getPaddingTop() + mRadius; //Add radius to get to middle of circle
float longOffset = getPaddingLeft() + mRadius; //Add radius to get to middle of circle
float dX;
float dY;
for (int pageCounter = 0; pageCounter < totalPageCount; pageCounter++) {
if (pageCounter > 0) {
//get X distance with padding, radius, and all prev circles.
float prevCirclesWidth = (pageCounter * mRadius * 2) + pageCounter * mSpacing;
dX = longOffset + prevCirclesWidth;
} else {
//get X distance with padding and radius.
dX = longOffset;
}
dY = shortOffset;
// Only paint fill if not completely transparent
if (mCircleStrokePaint.getAlpha() > 0) {
canvas.drawCircle(dX, dY, mRadius, mCircleStrokePaint);
}
}
//Draw the filled circle according to the current position
float cx;
if (mCurrentPosition > 0) {
float circlesWidth = (mCurrentPosition * mRadius * 2);
float spacingBetweenCirclesWidth = mCurrentPosition * mSpacing;
cx = circlesWidth + spacingBetweenCirclesWidth;
} else {
cx = 0;
}
dX = longOffset + cx;
dY = shortOffset;
canvas.drawCircle(dX, dY, mRadius, mCircleFillPaint);
}
public void notifyDataSetChanged() {
invalidate();
}
public void setCurrentIndicatorPosition(int position) {
if (mRecyclerView == null) {
throw new IllegalStateException("ViewPager has not been bound.");
}
//mRecyclerView.scrollToPosition(position);
mCurrentPosition = position;
invalidate();
}
public void remeasure() {
requestLayout();
}
/*
* (non-Javadoc)
*
* @see android.view.View#onMeasure(int, int)
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec));
}
/**
* Determines the width of this view
*
* @param measureSpec A measureSpec packed into an int
* @return The width of the view, honoring constraints from measureSpec
*/
private int measureLong(int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if ((specMode == MeasureSpec.EXACTLY) || (mRecyclerView == null)) {
//We were told how big to be
return specSize;
} else {
//Calculate the width according the views count
final int count = mAdapter.getItemCount();
result = (int) (getPaddingLeft() + getPaddingRight() + (count * 2 * mRadius) + (count - 1) * mSpacing);
//Respect AT_MOST value if that was what is called for by measureSpec
if (specMode == MeasureSpec.AT_MOST) {
return Math.min(result, specSize);
}
if (result > specSize) {
return specSize;
}
}
return result;
}
/**
* Determines the height of this view
*
* @param measureSpec A measureSpec packed into an int
* @return The height of the view, honoring constraints from measureSpec
*/
private int measureShort(int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
//We were told how big to be
result = specSize;
} else {
//Measure the height
result = (int) (2 * mRadius + getPaddingTop() + getPaddingBottom() + 1);
//Respect AT_MOST value if that was what is called for by measureSpec
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
mCurrentPosition = savedState.currentPage;
requestLayout();
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState savedState = new SavedState(superState);
savedState.currentPage = mCurrentPosition;
return savedState;
}
static class SavedState extends BaseSavedState {
int currentPage;
public SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
currentPage = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(currentPage);
}
@SuppressWarnings("UnusedDeclaration")
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment