Skip to content

Instantly share code, notes, and snippets.

@abbasshah17
Forked from lauw/SnappingRecyclerView.java
Last active September 27, 2016 04:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save abbasshah17/6239ae4b71112e82c5f3 to your computer and use it in GitHub Desktop.
Save abbasshah17/6239ae4b71112e82c5f3 to your computer and use it in GitHub Desktop.
Snapping RecyclerView (Horizontal)
/*
* Copyright 2015 Laurens Muller.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jadoo.jadooplus.player.core.android.ChannelsList;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import static android.widget.AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL;
import static android.widget.AbsListView.OnScrollListener.SCROLL_STATE_FLING;
public class ZoomingRecyclerView extends RecyclerView {
private static final String TAG = "ZoomingRecyclerView";
private boolean mSnapEnabled = false;
private boolean mUserScrolling = false;
private boolean mScrolling = false;
private int mScrollState;
private int lastScrollPosition = -1;
private long lastScrollTime = 0;
private Handler mHandler = new Handler();
private OnScrollListener scrollListener = null;
private ItemListener mListener = null;
private boolean mScaleUnfocusedViews = false;
private final static int MINIMUM_SCROLL_EVENT_OFFSET_MS = 20;
public ZoomingRecyclerView(Context context) {
super(context);
}
public ZoomingRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setItemListener(ItemListener listener) {
mListener = listener;
}
/**
* Enable snapping behaviour for this recyclerView
* @param enabled enable or disable the snapping behaviour
*/
public void setSnapEnabled(boolean enabled) {
mSnapEnabled = enabled;
if (enabled) {
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) {
if (left == oldLeft && right == oldRight && top == oldTop && bottom == oldBottom) {
removeOnLayoutChangeListener(this);
updateViews();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
scrollToView(getChildAt(0));
}
}, 20);
}
}
});
scrollListener = new OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
updateViews();
super.onScrolled(recyclerView, dx, dy);
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
/** 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;
}
};
addOnScrollListener(scrollListener);
}
else {
if (scrollListener != null) {
removeOnScrollListener(scrollListener);
}
}
}
/**
* 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);
}
private void updateViews() {
float percentage = 0.50f;
float scale = 1f - (0.7f * percentage);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
setMarginsForChild(child);
child.setBackgroundResource(0);
if (mScaleUnfocusedViews) {
child.setScaleX(scale);
child.setScaleY(scale);
}
}
View centerView = getCenterView();
if (centerView != null) {
centerView.setScaleX(1);
centerView.setScaleY(1);
centerView.setBackgroundColor(Color.parseColor("#FA0000"));
}
}
/**
* Adds the margins to a childView so a view will still center even if it's only a single child
* @param child childView to set margins for
*/
private void setMarginsForChild(View child) {
int lastItemIndex = getLayoutManager().getItemCount() - 1;
int childIndex = getChildPosition(child);
int startMargin = childIndex == 0 ? getMeasuredWidth() / 2 : 0;
int endMargin = childIndex == lastItemIndex ? getMeasuredWidth() / 2 : 0;
/** if sdk minimum level is 17, set RTL margins **/
if (Build.VERSION.SDK_INT >= 17) {
((ViewGroup.MarginLayoutParams) child.getLayoutParams()).setMarginStart(startMargin);
((ViewGroup.MarginLayoutParams) child.getLayoutParams()).setMarginEnd(endMargin);
}
((ViewGroup.MarginLayoutParams) child.getLayoutParams()).setMargins(startMargin, 0, endMargin, 0);
child.requestLayout();
}
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent event) {
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;
View targetView = getChildClosestToPosition((int) event.getX());
if (!mUserScrolling) {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (targetView != getCenterView() && getChildCount() > 0) {
scrollToView(targetView);
if (mListener != null && getAdapter() != null && getAdapter().getItemCount() >= 0) {
mListener.onChannelClick(getChildAdapterPosition(targetView));
}
return true;
}
if (mListener != null && getAdapter() != null && getAdapter().getItemCount() >= 0) {
mListener.onChannelClick(getChildAdapterPosition(targetView));
}
return true;
}
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (!mSnapEnabled)
return super.onInterceptTouchEvent(e);
View targetView = getChildClosestToPosition((int) e.getX());
return (targetView != getCenterView()) || super.onInterceptTouchEvent(e);
}
private 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);
}
private void scrollToView(View child) {
if (child == null)
return;
stopScroll();
int scrollDistance = getScrollDistance(child);
if (scrollDistance != 0)
smoothScrollBy(scrollDistance, 0);
}
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.getX() + (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();
}
public int getHorizontalScrollOffset() {
return computeHorizontalScrollOffset();
}
public int getVerticalScrollOffset() {
return computeVerticalScrollOffset();
}
public void smoothUserScrollBy(int x, int y) {
mUserScrolling = true;
smoothScrollBy(x, y);
}
public void smoothUserScrollTo(final int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}
if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}
if (getChildAdapterPosition(getCenterView()) == position) {
scrollToView(getCenterView());
return;
}
scrollToPosition(position);
if (lastScrollPosition == position || position <= getAdapter().getItemCount()) {
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) {
if (left == oldLeft && right == oldRight && top == oldTop && bottom == oldBottom) {
if (getChildAdapterPosition(getCenterView()) == position) {
removeOnLayoutChangeListener(this);
scrollToView(getCenterView());
return;
}
int childPosition;
for (int i = 0; i < getChildCount(); i++) {
childPosition = getChildAdapterPosition(getChildAt(i));
if (childPosition == position) {
updateViews();
scrollToView(getChildAt(i));
return;
}
}
childPosition = getChildAdapterPosition(getCenterView());
int delta = position - childPosition;
int scrollTo;
if (delta > 0) {
scrollTo = getChildCount();
}
else {
scrollTo = 0;
}
scrollToView(getChildAt(scrollTo));
}
}
});
}
lastScrollPosition = position;
}
@Override
public void scrollToPosition(int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}
if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}
stopScroll();
((LinearLayoutManager) getLayoutManager()).scrollToPositionWithOffset(position, 0);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeCallbacksAndMessages(null);
}
public interface ItemListener {
void onChannelClick(int position);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment