Skip to content

Instantly share code, notes, and snippets.

@beersheba
Created June 9, 2016 11:43
Show Gist options
  • Save beersheba/7990c4924334a4f88dc8e1834cca80ad to your computer and use it in GitHub Desktop.
Save beersheba/7990c4924334a4f88dc8e1834cca80ad to your computer and use it in GitHub Desktop.
package me.superup.tools.tools.Extensions.RecyclerUtils;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.Display;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator;
import java.util.List;
import java.util.Vector;
import static android.widget.AbsListView.OnScrollListener.SCROLL_STATE_FLING;
import static android.widget.AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL;
public class SnappingRecyclerView extends RecyclerView {
private static final String TAG = "qwe";
private final static int MINIMUM_SCROLL_EVENT_OFFSET_MS = 20;
public Vector<ChildFocusListener> ChildFocusListeners = new Vector<>();
private boolean mSnapEnabled = false;
private boolean mUserScrolling = false;
private boolean mScrolling = false;
private int mScrollState;
private long lastScrollTime = 0;
private boolean mScaleUnfocusedViews = false;
private View targetView = null;
private float snapScale = 1f;
private float minScale = 0.5f;
private float spaceBetweenItemsCenter;
private float mFlingFactor;
private GestureDetector detector;
protected float scaleFactor = 1f;
private float innerScale = 1f;
/**
* layout manager to fix the scroll issuse
*/
public class SannpingLayoutManager extends LinearLayoutManager {
public SannpingLayoutManager(Context context) {
super(context);
}
@Override
public boolean supportsPredictiveItemAnimations() {
return false;
}
private static final int DEFAULT_EXTRA_LAYOUT_SPACE = 1200;
private int extraLayoutSpace = -1;
public void setExtraLayoutSpace(int extraLayoutSpace) {
this.extraLayoutSpace = extraLayoutSpace;
}
@Override
protected int getExtraLayoutSpace(RecyclerView.State state) {
if (extraLayoutSpace > 0) {
return extraLayoutSpace;
}
return DEFAULT_EXTRA_LAYOUT_SPACE;
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
LinearSmoothScroller linearSmoothScroller = new Scroller(recyclerView.getContext());
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
/**
* helpful for compute the middle of the product
*
* @return the offest need to center item
*/
private float getCenteredItemOffset(int itemPosition) {
float offest = spaceBetweenItemsCenter; //getMeasuredWidth() * 0.5f - ForceWidthRelativeLayout.getScreenWidth(getContext()) * 0.275f;
for (int i = 0; i < mItemDecorations.size(); i++) {
Rect outRect = new Rect();
mItemDecorations.get(i).getItemOffsets(outRect, itemPosition, SnappingRecyclerView.this);
offest -= outRect.left;
}
return offest;
}
/**
* ********** Inner Classes **********
*/
private class Scroller extends LinearSmoothScroller {
public Scroller(Context context) {
super(context);
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return SannpingLayoutManager.this.computeScrollVectorForPosition(targetPosition);
}
@Override
public int calculateDxToMakeVisible(View view, int snapPreference) {
return super.calculateDxToMakeVisible(view, snapPreference) + (int) getCenteredItemOffset(((LayoutParams) view.getLayoutParams()).getViewLayoutPosition());
}
@Override
protected int getHorizontalSnapPreference() {
return SNAP_TO_START;
}
}
@Override
public void scrollToPosition(final int position) {
//System.out.println("scrolltoPosition:" + position);
// ovveride for fixing scroll to position
//
super.scrollToPositionWithOffset(position, (int) getCenteredItemOffset(position));
}
}
private class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int space;
public SpacesItemDecoration() {
this.space = getScreenWidth(getContext()) / 2;
}
/**
* @deprecated Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
*/
@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
// for recycling perpose
outRect.left = 0;
outRect.right = 0;
if (ViewCompat.getLayoutDirection(SnappingRecyclerView.this) == ViewCompat.LAYOUT_DIRECTION_LTR) {
if (itemPosition + 1 == parent.getAdapter().getItemCount()) {
outRect.right = space;
}
if (itemPosition == 0) {
outRect.left = space;
}
} else {
if (itemPosition + 1 == parent.getAdapter().getItemCount()) {
outRect.left = space;
}
if (itemPosition == 0) {
outRect.right = space;
}
}
}
}
@Override
public boolean fling(int velocityX, int velocityY) {
if (mFlingFactor > 0) {
return super.fling((int) (velocityX * mFlingFactor), velocityY);
}
return super.fling(velocityX, velocityY);
}
private List<ItemDecoration> mItemDecorations = new Vector<>();
/**
* Vladislav Karpman
* <p>
* Is appear animation working
*/
protected boolean mAppearAnimationIsWorking = false;
/**
* Duration of each view which will be animated.
* By default
*/
private long mCenterViewAppearAnimationDuration = 750;
private long mLeftToCenterViewAppearAnimationDuration = 1500;
private long mRightToCenterViewAppearAnimationDuration = 1500;
public SnappingRecyclerView(Context context) {
this(context, null);
}
public SnappingRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
setChildrenDrawingOrderEnabled(true);
SannpingLayoutManager layoutManager = new SannpingLayoutManager(getContext());
layoutManager.setOrientation(HORIZONTAL);
setLayoutManager(layoutManager);
detector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (getScrollState() == SCROLL_STATE_IDLE) {
View centerView = getCenterView();
if (getChildClosestToPosition((int) e.getX()) == centerView) {
int Position = getChildAdapterPosition(centerView);
for (ChildFocusListener ChildFocusListener : ChildFocusListeners)
ChildFocusListener.onChildSelected(
getChildViewHolder(centerView),
Position);
}
}
return true;
}
});
}
public float getScaleFactor() {
return this.scaleFactor * innerScale;
}
public void setSnapScale(float snapScale, float minScale, float spaceBetweenItemsCenter) {
this.snapScale = snapScale;
this.minScale = minScale;
this.spaceBetweenItemsCenter = spaceBetweenItemsCenter;
}
public void addSpaceItemDecoration() {
addItemDecoration(new SpacesItemDecoration());
}
public NegativeItemDec createNegativeItemDecoration(float scaleX) {
return new NegativeItemDec(scaleX, 0f);
}
public class NegativeItemDec extends ItemDecoration {
private int spaceX;
private int spaceY;
public NegativeItemDec(float scaleX, float scaleY) {
this.spaceX = -(int) (scaleX * getScreenWidth(getContext()));
}
public int getAbsulteX() {
return this.spaceX;
}
public int getAbsulteY() {
return this.spaceY;
}
/*
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
outRect.bottom = spaceY;
outRect.top = spaceY;
outRect.left = spaceX;
outRect.right = spaceX;
}*/
/**
* @deprecated Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
*/
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.bottom = spaceY;
outRect.top = spaceY;
outRect.left = spaceX;
outRect.right = spaceX;
}
}
/**
* setting fling factor to control fling effect of recycler view
*
* @param flingFactor
*/
public void setFlingFactor(float flingFactor) {
mFlingFactor = flingFactor;
}
@Override
public void setAdapter(final Adapter adapter) {
setAdapter(adapter, true);
}
public void setAdapter(final Adapter adapter, boolean animateApprenece) {
super.setAdapter(adapter);
/** Vladislav Karpman
*
* It is used to hide all movement(such as scrolling to center view {@link #scrollToCenterView()})
* before all view's appearance
*/
adapter.registerAdapterDataObserver(new AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
postDelayed(new Runnable() {
@Override
public void run() {
if (getCenterView() != null) {
ViewHolder Contain = findContainingViewHolder(getCenterView());
if (Contain != null) {
int Position = getChildAdapterPosition(Contain.itemView);
for (ChildFocusListener ChildFocusListener : ChildFocusListeners)
ChildFocusListener.onChildInFocus(Contain, Position);
}
}
}
}, 1);
}
});
if (animateApprenece) {
mAppearAnimationIsWorking = true;
addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
final View centerView = getCenterView();
final View leftToCenter = getChildClosestToPosition(0);
final View rightToCenter = getChildClosestToPosition(getScreenWidth(getContext()));
if (centerView != null && leftToCenter != null && rightToCenter != null) {
runAppearAnimation();
removeOnLayoutChangeListener(this);
}
}
});
}
}
/**
* Vladislav Karpman
* <p>
* Runs appear animation
*/
public void runAppearAnimation() {
/**
* Vladislav Karpman
*
* Finding center view and views to the left and right of center
*/
final View centerView = getCenterView();
final View leftToCenter = getChildClosestToPosition(0);
final View rightToCenter = getChildClosestToPosition(getScreenWidth(getContext()));
/**
* Vladislav Karpman
*
* To be on the safe side setting scale and alpha for view which are going to be animated
*/
centerView.setScaleX(0);
centerView.setScaleY(0);
centerView.setAlpha(0);
leftToCenter.setScaleX(0);
leftToCenter.setScaleY(0);
leftToCenter.setAlpha(0);
rightToCenter.setAlpha(0);
rightToCenter.setScaleY(0);
rightToCenter.setScaleX(0);
/**
* Vladislav Karpman
*
* Set x coordinates for left and right views to achieve effect as follow "moving from sides to center"
*/
leftToCenter.setTranslationX(-leftToCenter.getMeasuredWidth());
rightToCenter.setTranslationX(getMeasuredWidth());
centerView.setTranslationX(0);
centerView.animate()
.scaleX(computeScale(centerView)).scaleY(computeScale(centerView))
.alpha(1)
.setDuration(mCenterViewAppearAnimationDuration)
.setInterpolator(new DecelerateInterpolator())
.start();
leftToCenter.animate().alpha(1).translationX(0)
.scaleX(computeScale(leftToCenter))
.scaleY(computeScale(leftToCenter))
.setDuration(mLeftToCenterViewAppearAnimationDuration)
.setInterpolator(new DecelerateInterpolator())
.start();
rightToCenter.animate().alpha(1).translationX(0)
.scaleX(computeScale(rightToCenter))
.scaleY(computeScale(rightToCenter))
.setDuration(mRightToCenterViewAppearAnimationDuration)
.setInterpolator(new DecelerateInterpolator())
.start();
/**
* Vladislav Karpman
*
* Clearing animation and indicating that appear animation doesn't works
*/
postDelayed(new Runnable() {
@Override
public void run() {
centerView.clearAnimation();
leftToCenter.clearAnimation();
rightToCenter.clearAnimation();
mAppearAnimationIsWorking = false;
}
}, Math.max(
mRightToCenterViewAppearAnimationDuration,
Math.max(mCenterViewAppearAnimationDuration, mLeftToCenterViewAppearAnimationDuration)));
}
@Override
public boolean onTouchEvent(MotionEvent e) {
/**
* Vladislav Karpman
*
* Prevents touching event while appear animation is working
*/
return !mAppearAnimationIsWorking && super.onTouchEvent(e);
}
/**
* Getting device's screen width
*
* @return screen width
*/
private int getScreenWidth(Context context) {
int width = 0;
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point size = new Point();
display.getSize(size);
width = size.x;
return width;
}
@Override
protected int getChildDrawingOrder(int childCount, int i) {
int centerChild;
//find center row
if ((childCount % 2) == 0) { //even childCount number
centerChild = childCount / 2; // if childCount 8 (actualy 0 - 7), then 4 and 4-1 = 3 is in centre.
int otherCenterChild = centerChild - 1;
//Which more in center?
View child = this.getChildAt(centerChild);
final int left = child.getLeft();
final int right = child.getRight();
//if this row goes through center then this
final int absParentCenterX = getLeft() + getWidth() / 2;
//Log.i("even", i + " from " + (childCount - 1) + ", while centerChild = " + centerChild);
if ((left < absParentCenterX) && (right > absParentCenterX)) {
//this child is in center line, so it is last
//centerChild is in center, no need to change
} else {
centerChild = otherCenterChild;
}
} else {//not even - done
centerChild = childCount / 2;
//Log.i("not even", i + " from " + (childCount - 1) + ", while centerChild = " + centerChild);
}
int rez;
//find drawIndex by centerChild
if (i > centerChild) {
//below center
rez = (childCount - 1) - i + centerChild;
} else if (i == centerChild) {
//center row
//draw it last
rez = childCount - 1;
} else {
//above center - draw as always
// i < centerChild
rez = i;
}
//Log.i("return", "" + rez);
return rez;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (!mSnapEnabled)
return;
updateViews();
}
View prevView = null;
@Override
public void onScrolled(int dx, int dy) {
super.onScrolled(dx, dy);
updateViews();
View View = getCenterView();
if (View != prevView) {
ViewHolder Contain = findContainingViewHolder(View);
if (Contain != null) {
int Position = getChildAdapterPosition(View);
for (ChildFocusListener ChildFocusListener : ChildFocusListeners)
ChildFocusListener.onChildInFocus(Contain, Position);
}
prevView = View;
}
}
@Override
public void onScrollStateChanged(int newState) {
super.onScrollStateChanged(newState);
if (!mSnapEnabled)
return;
/** if scroll is caused by a touch (scroll touch, not any touch) **/
if (newState == SCROLL_STATE_TOUCH_SCROLL) {
/** if scroll was initiated already, this is not a user scrolling, but probably a tap, else set userScrolling **/
if (!mScrolling) {
mUserScrolling = true;
}
} else if (newState == SCROLL_STATE_IDLE) {
if (mUserScrolling) {
scrollToView(getCenterView());
}
mUserScrolling = false;
mScrolling = false;
} else if (newState == SCROLL_STATE_FLING) {
mScrolling = true;
}
mScrollState = newState;
}
/**
* Enable snapping behaviour for this recyclerView
*
* @param enabled enable or disable the snapping behaviour
*/
void setSnapEnabled(boolean enabled) {
mSnapEnabled = enabled;
}
public ViewHolder getCenterViewHolder() {
if (getCenterView() != null)
return getChildViewHolder(getCenterView());
return null;
}
@Override
public void onDraw(Canvas c) {
/**
* important !!!! need to be called else system will think
* that we are in old version of android
*/
super.onDraw(c);
}
public void addChildFocusListener(ChildFocusListener ChildFocusListener) {
this.ChildFocusListeners.add(ChildFocusListener);
}
public void removeChildFocusListener(ChildFocusListener ChildFocusListener) {
this.ChildFocusListeners.remove(ChildFocusListener);
}
/**
* Enable snapping behaviour for this recyclerView
*
* @param enabled enable or disable the snapping behaviour
* @param scaleUnfocusedViews downScale the views which are not focused based on how far away they are from the center
*/
public void setSnapEnabled(boolean enabled, boolean scaleUnfocusedViews) {
this.mScaleUnfocusedViews = scaleUnfocusedViews;
setSnapEnabled(enabled);
}
/**
* Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
* affect both measurement and drawing of individual item views.
* <p>
* <p>Item decorations are ordered. Decorations placed earlier in the list will
* be run/queried/drawn first for their effects on item views. Padding added to views
* will be nested; a padding added by an earlier decoration will mean further
* item decorations in the list will be asked to draw/pad within the previous decoration's
* given area.</p>
*
* @param decor Decoration to add
* @param index Position in the decoration chain to insert this decoration at. If this value
* is negative the decoration will be added at the end.
*/
@Override
public void addItemDecoration(ItemDecoration decor, int index) {
if (index < 0) {
mItemDecorations.add(decor);
} else {
mItemDecorations.add(index, decor);
}
super.addItemDecoration(decor, index);
}
private float computeScale(View View) {
float percentage = getPercentageFromCenter(View);
float scale = 1f - (snapScale * percentage);
return Math.max(scale, minScale);
}
protected final void updateViews() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//setMarginsForChild(child);
if (mAppearAnimationIsWorking)
child.setAlpha(0);
else
child.setAlpha(1);
if (mScaleUnfocusedViews && !mAppearAnimationIsWorking) {
float scale = computeScale(child);
child.setTranslationY(-getMeasuredHeight() * (1 - scaleFactor) / 2);
child.setScaleX(scale * scaleFactor * innerScale);
child.setScaleY(scale * scaleFactor * innerScale);
}
}
}
public void setInnerScale(float scale) {
this.innerScale = scale;
this.updateViews();
}
private boolean isBlockScrollEvent = false;
public void setIsBlockScrollEvent(boolean isBlockScrollEvent) {
this.isBlockScrollEvent = isBlockScrollEvent;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
detector.onTouchEvent(event);
if (isBlockScrollEvent)
return true;
if (event.getAction() == MotionEvent.ACTION_DOWN) {
targetView = getChildClosestToPosition((int) event.getX());
}
if (!mSnapEnabled)
return super.dispatchTouchEvent(event);
long currentTime = System.currentTimeMillis();
/** if touch events are being spammed, this is due to user scrolling right after a tap,
* so set userScrolling to true **/
if (mScrolling && mScrollState == SCROLL_STATE_TOUCH_SCROLL) {
if ((currentTime - lastScrollTime) < MINIMUM_SCROLL_EVENT_OFFSET_MS) {
mUserScrolling = true;
}
}
lastScrollTime = currentTime;
if (!mUserScrolling) {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (targetView != null && targetView != getCenterView()) {
scrollToView(targetView);
targetView = null;
return true;
}
}
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (!mSnapEnabled)
return super.onInterceptTouchEvent(e);
View targetView = getChildClosestToPosition((int) e.getX());
if (targetView != getCenterView()) {
return true;
}
return super.onInterceptTouchEvent(e);
}
protected final View getChildClosestToPosition(int x) {
if (getChildCount() <= 0)
return null;
int itemWidth = getChildAt(0).getMeasuredWidth();
int closestX = 9999;
View closestChild = null;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int childCenterX = ((int) child.getX() + (itemWidth / 2));
int xDistance = childCenterX - x;
/** if child center is closer than previous closest, set it as closest **/
if (Math.abs(xDistance) < Math.abs(closestX)) {
closestX = xDistance;
closestChild = child;
}
}
return closestChild;
}
private View getCenterView() {
return getChildClosestToPosition(getMeasuredWidth() / 2);
}
protected void scrollToView(View child) {
if (child == null || mAppearAnimationIsWorking) // prevents scrolling when appear animation is working
return;
stopScroll();
int scrollDistance = getScrollDistance(child);
if (scrollDistance != 0)
super.smoothScrollBy(scrollDistance, 0);
}
/**
* Animate a scroll by the given amount of pixels along either axis.
*
* @param dx Pixels to scroll horizontally
* @param dy Pixels to scroll vertically
*/
public void smoothScrollBy(int dx, int dy) {
// super.smoothScrollBy(dx,dy);
}
private int getScrollDistance(View child) {
int itemWidth = getChildAt(0).getMeasuredWidth();
int centerX = getMeasuredWidth() / 2;
int childCenterX = ((int) child.getX() + (itemWidth / 2));
return childCenterX - centerX;
}
private float getPercentageFromCenter(View child) {
float centerX = (getMeasuredWidth() / 2);
float childCenterX = child.getLeft() + (child.getWidth() / 2);
float offSet = Math.max(centerX, childCenterX) - Math.min(centerX, childCenterX);
int maxOffset = (getMeasuredWidth() / 2) + child.getWidth();
return (offSet / maxOffset);
}
public boolean isChildCenterView(View child) {
return child == getCenterView();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
public static interface ChildFocusListener {
public void onChildInFocus(ViewHolder View, int Position);
public void onChildSelected(ViewHolder View, int Position);
}
public void setCenterViewAppearAnimationDuration(long centerViewAppearAnimationDuration) {
mCenterViewAppearAnimationDuration = centerViewAppearAnimationDuration;
}
public void setLeftToCenterViewAppearAnimationDuration(long leftToCenterViewAppearAnimationDuration) {
mLeftToCenterViewAppearAnimationDuration = leftToCenterViewAppearAnimationDuration;
}
public void setRightToCenterViewAppearAnimationDuration(long rightToCenterViewAppearAnimationDuration) {
mRightToCenterViewAppearAnimationDuration = rightToCenterViewAppearAnimationDuration;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment