-
-
Save Sevastyan/5a30cc11cb5f06a7bc2b8f0863eb2744 to your computer and use it in GitHub Desktop.
RecyclerView extension that "sticks" items in the center on scroll
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.graphics.Rect; | |
import android.support.v7.widget.RecyclerView; | |
import android.view.View; | |
public class ItemDecoration extends RecyclerView.ItemDecoration { | |
/** | |
* | |
* {@link #startPadding} and {@link #endPadding} are final and required on initialization | |
* because {@link android.support.v7.widget.RecyclerView.ItemDecoration} are drawn | |
* before the adapter's child views so you cannot rely on the child view measurements | |
* to determine padding as the two are connascent | |
* | |
* see {@see <a href="https://en.wikipedia.org/wiki/Connascence_(computer_programming)"} | |
*/ | |
/** | |
* @param startPadding | |
* @param endPadding | |
*/ | |
private final int startPadding; | |
private final int endPadding; | |
public ItemDecoration(int startPadding, int endPadding) { | |
this.startPadding = startPadding; | |
this.endPadding = endPadding; | |
} | |
@Override | |
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, | |
RecyclerView.State state) { | |
int totalWidth = parent.getWidth(); | |
//first element | |
if (parent.getChildAdapterPosition(view) == 0) { | |
int firstPadding = (totalWidth - startPadding) / 2; | |
firstPadding = Math.max(0, firstPadding); | |
outRect.set(firstPadding, 0, 0, 0); | |
} | |
//last element | |
if (parent.getChildAdapterPosition(view) == parent.getAdapter().getItemCount() - 1 && | |
parent.getAdapter().getItemCount() > 1) { | |
int lastPadding = (totalWidth - endPadding) / 2; | |
lastPadding = Math.max(0, lastPadding); | |
outRect.set(0, 0, lastPadding, 0); | |
} | |
} | |
} | |
Raw |
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.support.v7.widget.LinearLayoutManager; | |
import android.support.v7.widget.RecyclerView; | |
import android.util.AttributeSet; | |
import android.view.View; | |
/** | |
* A {@link android.support.v7.widget.RecyclerView} implementation which snaps the current visible | |
* item to the center of the screen based on scroll directions defined by {@link | |
* #getScrollDirection()} | |
* <p> | |
* Designed to work with {@link LinearLayoutManager} with {@link #HORIZONTAL} orientation *ONLY* | |
*/ | |
public class StickyRecyclerView extends RecyclerView { | |
/** | |
* The horizontal distance (in pixels) scrolled is > 0 | |
* | |
* @see #getScrollDirection() | |
*/ | |
public static final int SCROLL_DIRECTION_LEFT = 0; | |
/** | |
* The horizontal distance scrolled (in pixels) is < 0 | |
* | |
* @see #getScrollDirection() | |
*/ | |
public static final int SCROLL_DIRECTION_RIGHT = 1; | |
private int mScrollDirection; | |
private OnCenterItemChangedListener mCenterItemChangedListener; | |
public StickyRecyclerView(Context context) { | |
super(context); | |
} | |
public StickyRecyclerView(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
} | |
public StickyRecyclerView(Context context, AttributeSet attrs, int defStyle) { | |
super(context, attrs, defStyle); | |
} | |
@Override | |
public void onScrollStateChanged(int state) { | |
super.onScrollStateChanged(state); | |
if (state == SCROLL_STATE_IDLE) { | |
if (mCenterItemChangedListener != null) { | |
mCenterItemChangedListener.onCenterItemChanged(findCenterViewIndex()); | |
} | |
} | |
} | |
@Override | |
public void onScrolled(int dx, int dy) { | |
super.onScrolled(dx, dy); | |
setScrollDirection(dx); | |
for (int i = 0; i < getChildCount(); i++) { | |
View child = getChildAt(i); | |
float percentage = getPercentageFromCenter(child); | |
float scale = 1f - (0.4f * percentage); | |
child.setScaleX(scale); | |
child.setScaleY(scale); | |
} | |
} | |
@Override | |
public boolean fling(int velocityX, int velocityY) { | |
smoothScrollToCenter(); | |
return true; | |
} | |
private float getPercentageFromCenter(View child) { | |
float centerX = (getMeasuredWidth() / 2); | |
float childCenterX = child.getX() + (child.getWidth() / 2); | |
float offSet = Math.max(centerX, childCenterX) - Math.min(centerX, childCenterX); | |
int maxOffset = (getMeasuredWidth() / 2) + child.getWidth(); | |
return (offSet / maxOffset); | |
} | |
private int findCenterViewIndex() { | |
int count = getChildCount(); | |
int index = -1; | |
int closest = Integer.MAX_VALUE; | |
int centerX = (getMeasuredWidth() / 2); | |
for (int i = 0; i < count; ++i) { | |
View child = getLayoutManager().getChildAt(i); | |
int childCenterX = (int) (child.getX() + (child.getWidth() / 2)); | |
int distance = Math.abs(centerX - childCenterX); | |
if (distance < closest) { | |
closest = distance; | |
index = i; | |
} | |
} | |
if (index == -1) { | |
throw new IllegalStateException("Can\'t find central view."); | |
} else { | |
return index; | |
} | |
} | |
private void smoothScrollToCenter() { | |
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager(); | |
int lastVisibleView = linearLayoutManager.findLastVisibleItemPosition(); | |
int firstVisibleView = linearLayoutManager.findFirstVisibleItemPosition(); | |
View firstView = linearLayoutManager.findViewByPosition(firstVisibleView); | |
View lastView = linearLayoutManager.findViewByPosition(lastVisibleView); | |
int screenWidth = this.getWidth(); | |
//since views have variable sizes, we need to calculate side margins separately. | |
int leftMargin = (screenWidth - lastView.getWidth()) / 2; | |
int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth(); | |
int leftEdge = lastView.getLeft(); | |
int rightEdge = firstView.getRight(); | |
int scrollDistanceLeft = leftEdge - leftMargin; | |
int scrollDistanceRight = rightMargin - rightEdge; | |
if (mScrollDirection == SCROLL_DIRECTION_LEFT) { | |
smoothScrollBy(scrollDistanceLeft, 0); | |
} else if (mScrollDirection == SCROLL_DIRECTION_RIGHT) { | |
smoothScrollBy(-scrollDistanceRight, 0); | |
} | |
} | |
/** | |
* Returns the last recorded scrolling direction of the StickyRecyclerView as | |
* set in {@link #onScrolled} | |
* | |
* @return {@link #SCROLL_DIRECTION_LEFT} or {@link #SCROLL_DIRECTION_RIGHT} | |
*/ | |
public int getScrollDirection() { | |
return mScrollDirection; | |
} | |
private void setScrollDirection(int dx) { | |
mScrollDirection = dx >= 0 ? SCROLL_DIRECTION_LEFT : SCROLL_DIRECTION_RIGHT; | |
} | |
/** | |
* Set a listener that will be notified when the central item is changed. | |
* | |
* @param listener Listener to set or null to clear | |
*/ | |
public void setOnCenterItemChangedListener(OnCenterItemChangedListener listener) { | |
mCenterItemChangedListener = listener; | |
} | |
/** | |
* A listener interface that can be added to the {@link StickyRecyclerView} to get | |
* notified when the central item is changed. | |
*/ | |
public interface OnCenterItemChangedListener { | |
/** | |
* @param centerPosition position of the center item | |
*/ | |
void onCenterItemChanged(int centerPosition); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment