Skip to content

Instantly share code, notes, and snippets.

@mgarnerdev
Created December 8, 2015 23:23
Show Gist options
  • Save mgarnerdev/8a31bdcf95c702e004dd to your computer and use it in GitHub Desktop.
Save mgarnerdev/8a31bdcf95c702e004dd 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];
}
};
}
}
@aemxn
Copy link

aemxn commented Apr 21, 2016

Working perfectly. Maybe you should add the missing resource files from ViewPagerIndicator library, should be good for the community to learn. Thanks for this!

@asbadve
Copy link

asbadve commented Dec 12, 2016

Can anybody please provide attr file?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment