Created
December 8, 2015 23:23
-
-
Save mgarnerdev/8a31bdcf95c702e004dd to your computer and use it in GitHub Desktop.
Horizontal Snapping RecyclerView with Page Indication
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
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); | |
} | |
} |
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
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
Working perfectly. Maybe you should add the missing resource files from ViewPagerIndicator library, should be good for the community to learn. Thanks for this!