Skip to content

Instantly share code, notes, and snippets.

@mandybess
Last active April 6, 2024 09:56
Show Gist options
  • Save mandybess/dada043b2e20bf3f9da4 to your computer and use it in GitHub Desktop.
Save mandybess/dada043b2e20bf3f9da4 to your computer and use it in GitHub Desktop.
RecyclerView extension that "sticks" items in the center on scroll
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
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);
}
}
@JuanAlejandro
Copy link

Hey guys, just copy the classes in your project and use it. Instead of RecyclerView, use StickyRecyclerView. As simple as that.

Thanks, @mandybess. This looks amazing in my app.

@asadmukhtar28
Copy link

How i achieve this, i have three items in recyclerview, on top of that i have a rounded icon that indicates which recyclerview item is selected. i want that only recyclerview moves left or right, rounded corner is just on top of that, i implement addOnScrollListener to achieve this behaviour with the help of SnapHelper class, but the problem is that i have 3 items only and when the user touch next item how i move recyclerview item to be selected, i use scrollTo and also smoothscroll but nothing happens.

According to my understanding, recyclerview shows all items thats why not able to scroll or smoothscroll to specific position. Kindly help me to get rid of this problem, i am facing this issue from yesterday, even i try to do an R & D for this behaviour but im unable to achieve this behavior.

@aliraza96
Copy link

A nice solution indeed. But I am facing a problem here. Which is that recycler view scrolls too much even on a small scroll or touch. So can you elaborate on where to fix this issue, please?

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