+ * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set
+ * this flag to true so that views will be available to other RecyclerViews
+ * immediately.
+ *
+ * Note that, setting this flag will result in a performance drop if RecyclerView
+ * is restored.
+ *
+ * @param recycleChildrenOnDetach Whether children should be recycled in detach or not.
+ */
+ public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
+ mRecycleChildrenOnDetach = recycleChildrenOnDetach;
+ }
+
+ @Override
+ public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
+ super.onDetachedFromWindow(view, recycler);
+ if (mRecycleChildrenOnDetach) {
+ removeAndRecycleAllViews(recycler);
+ recycler.clear();
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (mPendingSavedState != null) {
+ return new SavedState(mPendingSavedState);
+ }
+ SavedState savedState = new SavedState();
+ savedState.position = mPendingScrollPosition;
+ savedState.offset = mOffset;
+ savedState.isReverseLayout = mShouldReverseLayout;
+ return savedState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ mPendingSavedState = new SavedState((SavedState) state);
+ requestLayout();
+ }
+ }
+
+ /**
+ * @return true if {@link #getOrientation()} is {@link #HORIZONTAL}
+ */
+ @Override
+ public boolean canScrollHorizontally() {
+ return mOrientation == HORIZONTAL;
+ }
+
+ /**
+ * @return true if {@link #getOrientation()} is {@link #VERTICAL}
+ */
+ @Override
+ public boolean canScrollVertically() {
+ return mOrientation == VERTICAL;
+ }
+
+ /**
+ * Returns the current orientation of the layout.
+ *
+ * @return Current orientation, either {@link #HORIZONTAL} or {@link #VERTICAL}
+ * @see #setOrientation(int)
+ */
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ /**
+ * Sets the orientation of the layout. {@link BannerLayoutManager}
+ * will do its best to keep scroll position.
+ *
+ * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL && orientation != VERTICAL) {
+ throw new IllegalArgumentException("invalid orientation:" + orientation);
+ }
+ assertNotInLayoutOrScroll(null);
+ if (orientation == mOrientation) {
+ return;
+ }
+ mOrientation = orientation;
+ mOrientationHelper = null;
+ mDistanceToBottom = INVALID_SIZE;
+ removeAllViews();
+ }
+
+ /**
+ * Returns the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now
+ * And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range
+ *
+ * @return Max visible item count
+ */
+ public int getMaxVisibleItemCount() {
+ return mMaxVisibleItemCount;
+ }
+
+ /**
+ * Set the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now
+ * And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range
+ *
+ * @param mMaxVisibleItemCount Max visible item count
+ */
+ public void setMaxVisibleItemCount(int mMaxVisibleItemCount) {
+ assertNotInLayoutOrScroll(null);
+ if (this.mMaxVisibleItemCount == mMaxVisibleItemCount) return;
+ this.mMaxVisibleItemCount = mMaxVisibleItemCount;
+ removeAllViews();
+ }
+
+ /**
+ * Calculates the view layout order. (e.g. from end to start or start to end)
+ * RTL layout support is applied automatically. So if layout is RTL and
+ * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
+ */
+ private void resolveShouldLayoutReverse() {
+ if (mOrientation == HORIZONTAL && getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) {
+ mReverseLayout = !mReverseLayout;
+ }
+ }
+
+ /**
+ * Returns if views are laid out from the opposite direction of the layout.
+ *
+ * @return If layout is reversed or not.
+ * @see #setReverseLayout(boolean)
+ */
+ public boolean getReverseLayout() {
+ return mReverseLayout;
+ }
+
+
+ public void setReverseLayout(boolean reverseLayout) {
+ assertNotInLayoutOrScroll(null);
+ if (reverseLayout == mReverseLayout) {
+ return;
+ }
+ mReverseLayout = reverseLayout;
+ removeAllViews();
+ }
+
+ public void setSmoothScrollInterpolator(Interpolator smoothScrollInterpolator) {
+ this.mSmoothScrollInterpolator = smoothScrollInterpolator;
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
+ final int offsetPosition = getOffsetToPosition(position);
+ if (mOrientation == VERTICAL) {
+ recyclerView.smoothScrollBy(0, offsetPosition, mSmoothScrollInterpolator);
+ } else {
+ recyclerView.smoothScrollBy(offsetPosition, 0, mSmoothScrollInterpolator);
+ }
+ }
+ @Override
+ public void scrollToPosition(int position) {
+ if (!mInfinite && (position < 0 || position >= getItemCount())) return;
+ mPendingScrollPosition = position;
+ mOffset = mShouldReverseLayout ? position * -mInterval : position * mInterval;
+ requestLayout();
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (state.getItemCount() == 0) {
+ removeAndRecycleAllViews(recycler);
+ mOffset = 0;
+ return;
+ }
+
+ ensureLayoutState();
+ resolveShouldLayoutReverse();
+
+ //make sure properties are correct while measure more than once
+ View scrap = recycler.getViewForPosition(0);
+ measureChildWithMargins(scrap, 0, 0);
+ mDecoratedMeasurement = mOrientationHelper.getDecoratedMeasurement(scrap);
+ mDecoratedMeasurementInOther = mOrientationHelper.getDecoratedMeasurementInOther(scrap);
+ mSpaceMain = (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2;
+ if (mDistanceToBottom == INVALID_SIZE) {
+ mSpaceInOther = (getTotalSpaceInOther() - mDecoratedMeasurementInOther) / 2;
+ } else {
+ mSpaceInOther =getTotalSpaceInOther() - mDecoratedMeasurementInOther - mDistanceToBottom;
+ }
+
+ mInterval = setInterval();
+ setUp();
+ mLeftItems = (int) Math.abs(minRemoveOffset() / mInterval) + 1;
+ mRightItems = (int) Math.abs(maxRemoveOffset() / mInterval) + 1;
+
+ if (mPendingSavedState != null) {
+ mShouldReverseLayout = mPendingSavedState.isReverseLayout;
+ mPendingScrollPosition = mPendingSavedState.position;
+ mOffset = mPendingSavedState.offset;
+ }
+
+ if (mPendingScrollPosition != NO_POSITION) {
+ mOffset = mShouldReverseLayout ?
+ mPendingScrollPosition * -mInterval : mPendingScrollPosition * mInterval;
+ }
+
+ detachAndScrapAttachedViews(recycler);
+ layoutItems(recycler);
+ }
+ public int getTotalSpaceInOther() {
+ if (mOrientation == HORIZONTAL) {
+ return getHeight() - getPaddingTop()
+ - getPaddingBottom();
+ } else {
+ return getWidth() - getPaddingLeft()
+ - getPaddingRight();
+ }
+ }
+ @Override
+ public void onLayoutCompleted(RecyclerView.State state) {
+ super.onLayoutCompleted(state);
+ mPendingSavedState = null;
+ mPendingScrollPosition = NO_POSITION;
+ }
+
+ @Override
+ public boolean onAddFocusables(RecyclerView recyclerView, ArrayList
+ * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based
+ * solely on the number of items in the adapter and the position of the visible items inside
+ * the adapter. This provides a stable scrollbar as the user navigates through a list of items
+ * with varying widths / heights.
+ *
+ * @param enabled Whether or not to enable smooth scrollbar.
+ * @see #setSmoothScrollbarEnabled(boolean)
+ */
+ public void setSmoothScrollbarEnabled(boolean enabled) {
+ mSmoothScrollbarEnabled = enabled;
+ }
+
+ public void setEnableBringCenterToFront(boolean bringCenterToTop) {
+ assertNotInLayoutOrScroll(null);
+ if (mEnableBringCenterToFront == bringCenterToTop) {
+ return;
+ }
+ this.mEnableBringCenterToFront = bringCenterToTop;
+ requestLayout();
+ }
+
+ public boolean getEnableBringCenterToFront() {
+ return mEnableBringCenterToFront;
+ }
+
+ /**
+ * Returns the current state of the smooth scrollbar feature. It is enabled by default.
+ *
+ * @return True if smooth scrollbar is enabled, false otherwise.
+ * @see #setSmoothScrollbarEnabled(boolean)
+ */
+ public boolean getSmoothScrollbarEnabled() {
+ return mSmoothScrollbarEnabled;
+ }
+
+ private static class SavedState implements Parcelable {
+ int position;
+ float offset;
+ boolean isReverseLayout;
+
+ SavedState() {
+
+ }
+
+ SavedState(Parcel in) {
+ position = in.readInt();
+ offset = in.readFloat();
+ isReverseLayout = in.readInt() == 1;
+ }
+
+ public SavedState(SavedState other) {
+ position = other.position;
+ offset = other.offset;
+ isReverseLayout = other.isReverseLayout;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(position);
+ dest.writeFloat(offset);
+ dest.writeInt(isReverseLayout ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator
+ * The implementation will snap the center of the target child view to the center of
+ * the attached {@link RecyclerView}.
+ */
+public class CenterSnapHelper extends RecyclerView.OnFlingListener {
+
+ RecyclerView mRecyclerView;
+ Scroller mGravityScroller;
+
+ /**
+ * when the dataSet is extremely large
+ * {@link #snapToCenterView(BannerLayoutManager, BannerLayoutManager.OnPageChangeListener)}
+ * may keep calling itself because the accuracy of float
+ */
+ private boolean snapToCenter = false;
+
+ // Handles the snap on scroll case.
+ private final RecyclerView.OnScrollListener mScrollListener =
+ new RecyclerView.OnScrollListener() {
+
+ boolean mScrolled = false;
+
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ super.onScrollStateChanged(recyclerView, newState);
+
+ final BannerLayoutManager layoutManager =
+ (BannerLayoutManager) recyclerView.getLayoutManager();
+ final BannerLayoutManager.OnPageChangeListener onPageChangeListener =
+ layoutManager.onPageChangeListener;
+ if (onPageChangeListener != null) {
+ onPageChangeListener.onPageScrollStateChanged(newState);
+ }
+
+ if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
+ mScrolled = false;
+ if (!snapToCenter) {
+ snapToCenter = true;
+ snapToCenterView(layoutManager, onPageChangeListener);
+ } else {
+ snapToCenter = false;
+ }
+ }
+ }
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (dx != 0 || dy != 0) {
+ mScrolled = true;
+ }
+ }
+ };
+
+ @Override
+ public boolean onFling(int velocityX, int velocityY) {
+ BannerLayoutManager layoutManager = (BannerLayoutManager) mRecyclerView.getLayoutManager();
+ if (layoutManager == null) {
+ return false;
+ }
+ RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
+ if (adapter == null) {
+ return false;
+ }
+
+ if (!layoutManager.getInfinite() &&
+ (layoutManager.mOffset == layoutManager.getMaxOffset()
+ || layoutManager.mOffset == layoutManager.getMinOffset())) {
+ return false;
+ }
+
+ final int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
+ mGravityScroller.fling(0, 0, velocityX, velocityY,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
+
+ if (layoutManager.mOrientation == BannerLayoutManager.VERTICAL
+ && Math.abs(velocityY) > minFlingVelocity) {
+ final int currentPosition = layoutManager.getCurrentPosition();
+ final int offsetPosition = (int) (mGravityScroller.getFinalY() /
+ layoutManager.mInterval / layoutManager.getDistanceRatio());
+ mRecyclerView.smoothScrollToPosition(layoutManager.getReverseLayout() ?
+ currentPosition - offsetPosition : currentPosition + offsetPosition);
+ return true;
+ } else if (layoutManager.mOrientation == BannerLayoutManager.HORIZONTAL
+ && Math.abs(velocityX) > minFlingVelocity) {
+ final int currentPosition = layoutManager.getCurrentPosition();
+ final int offsetPosition = (int) (mGravityScroller.getFinalX() /
+ layoutManager.mInterval / layoutManager.getDistanceRatio());
+ mRecyclerView.smoothScrollToPosition(layoutManager.getReverseLayout() ?
+ currentPosition - offsetPosition : currentPosition + offsetPosition);
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Attaches the {@link CenterSnapHelper} to the provided RecyclerView, by calling
+ * {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}.
+ * You can call this method with {@code null} to detach it from the current RecyclerView.
+ *
+ * @param recyclerView The RecyclerView instance to which you want to add this helper or
+ * {@code null} if you want to remove CenterSnapHelper from the current
+ * RecyclerView.
+ * @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener}
+ * attached to the provided {@link RecyclerView}.
+ */
+ public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
+ throws IllegalStateException {
+ if (mRecyclerView == recyclerView) {
+ return; // nothing to do
+ }
+ if (mRecyclerView != null) {
+ destroyCallbacks();
+ }
+ mRecyclerView = recyclerView;
+ if (mRecyclerView != null) {
+ final RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+ if (!(layoutManager instanceof BannerLayoutManager)) return;
+
+ setupCallbacks();
+ mGravityScroller = new Scroller(mRecyclerView.getContext(),
+ new DecelerateInterpolator());
+
+ snapToCenterView((BannerLayoutManager) layoutManager,
+ ((BannerLayoutManager) layoutManager).onPageChangeListener);
+ }
+ }
+
+ void snapToCenterView(BannerLayoutManager layoutManager,
+ BannerLayoutManager.OnPageChangeListener listener) {
+ final int delta = layoutManager.getOffsetToCenter();
+ if (delta != 0) {
+ if (layoutManager.getOrientation()
+ == BannerLayoutManager.VERTICAL)
+ mRecyclerView.smoothScrollBy(0, delta);
+ else
+ mRecyclerView.smoothScrollBy(delta, 0);
+ } else {
+ // set it false to make smoothScrollToPosition keep trigger the listener
+ snapToCenter = false;
+ }
+
+ if (listener != null)
+ listener.onPageSelected(layoutManager.getCurrentPosition());
+ }
+
+ /**
+ * Called when an instance of a {@link RecyclerView} is attached.
+ */
+ void setupCallbacks() throws IllegalStateException {
+ if (mRecyclerView.getOnFlingListener() != null) {
+ throw new IllegalStateException("An instance of OnFlingListener already set.");
+ }
+ mRecyclerView.addOnScrollListener(mScrollListener);
+ mRecyclerView.setOnFlingListener(this);
+ }
+
+ /**
+ * Called when the instance of a {@link RecyclerView} is detached.
+ */
+ void destroyCallbacks() {
+ mRecyclerView.removeOnScrollListener(mScrollListener);
+ mRecyclerView.setOnFlingListener(null);
+ }
+}
diff --git a/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/OverFlyingLayoutManager.java b/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/OverFlyingLayoutManager.java
new file mode 100644
index 0000000..eddfb48
--- /dev/null
+++ b/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/OverFlyingLayoutManager.java
@@ -0,0 +1,899 @@
+package com.example.library.banner.layoutmanager;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.core.view.ViewCompat;
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.OrientationHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+
+/**
+ * Please make sure your child view have the same size.
+ */
+
+@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue"})
+public class OverFlyingLayoutManager extends RecyclerView.LayoutManager
+ implements RecyclerView.SmoothScroller.ScrollVectorProvider {
+ private float minScale = 0.75f;//两侧图片缩放比
+ private float angle = 8f;//翻转角度
+ private int itemSpace = 385;
+ private boolean mInfinite = true;
+ public static final int DETERMINE_BY_MAX_AND_MIN = -1;
+
+ public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
+
+ public static final int VERTICAL = OrientationHelper.VERTICAL;
+
+ protected int mDecoratedMeasurement;
+
+ protected int mDecoratedMeasurementInOther;
+
+ public float getMinScale() {
+ return minScale;
+ }
+
+ public void setMinScale(float minScale) {
+ this.minScale = minScale;
+ }
+
+ public float getAngle() {
+ return angle;
+ }
+
+ public void setAngle(float angle) {
+ this.angle = angle;
+ }
+
+ public int getItemSpace() {
+ return itemSpace;
+ }
+
+ public void setItemSpace(int itemSpace) {
+ this.itemSpace = itemSpace;
+ }
+
+ /**
+ * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ int mOrientation;
+
+ protected int mSpaceMain;
+
+ protected int mSpaceInOther;
+
+ /**
+ * The offset of property which will change while scrolling
+ */
+ protected float mOffset;
+
+ /**
+ * Many calculations are made depending on orientation. To keep it clean, this interface
+ * Based on {@link #mOrientation}, an implementation is lazily created in
+ * {@link #ensureLayoutState} method.
+ */
+ protected OrientationHelper mOrientationHelper;
+
+ /**
+ * Defines if layout should be calculated from end to start.
+ */
+ private boolean mReverseLayout = false;
+
+ /**
+ * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}.
+ * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}
+ */
+ private boolean mSmoothScrollbarEnabled = true;
+
+ /**
+ * When LayoutManager needs to scroll to a position, it sets this variable and requests a
+ * layout which will check this variable and re-layout accordingly.
+ */
+ private int mPendingScrollPosition = RecyclerView.NO_POSITION;
+
+ private SavedState mPendingSavedState = null;
+
+ protected float mInterval; //the mInterval of each item's mOffset
+
+ /* package */ OnPageChangeListener onPageChangeListener;
+
+ private boolean mRecycleChildrenOnDetach;
+
+
+ private boolean mEnableBringCenterToFront;
+
+ /**
+ * ugly code for fix bug caused by float
+ */
+ private boolean mIntegerDy = false;
+
+ private int mLeftItems;
+
+ private int mRightItems;
+
+ /**
+ * max visible item count
+ */
+ private int mMaxVisibleItemCount = DETERMINE_BY_MAX_AND_MIN;
+
+ /**
+ * @return the mInterval of each item's mOffset
+ */
+ protected float setInterval() {
+ return mDecoratedMeasurement - itemSpace;
+ }
+
+ protected void setItemViewProperty(View itemView, float targetOffset) {
+ float scale = calculateScale(targetOffset + mSpaceMain);
+ itemView.setScaleX(scale);
+ itemView.setScaleY(scale);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ itemView.setElevation(0);
+ }
+ final float rotation = calRotation(targetOffset);
+ if (getOrientation() == HORIZONTAL) {
+ itemView.setRotationY(rotation);
+ } else {
+ itemView.setRotationX(-rotation);
+ }
+ }
+
+ private float calRotation(float targetOffset) {
+ return -angle / mInterval * targetOffset;
+ }
+
+ private float calculateScale(float x) {
+ float deltaX = Math.abs(x - (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2f);
+ return (minScale - 1) * deltaX / (mOrientationHelper.getTotalSpace() / 2f) + 1f;
+ }
+
+ /**
+ * cause elevation is not support below api 21,
+ * so you can set your elevation here for supporting it below api 21
+ * or you can just setElevation in {@link #setItemViewProperty(View, float)}
+ */
+ protected float setViewElevation(View itemView, float targetOffset) {
+ return itemView.getScaleX() * 5;
+ }
+
+ /**
+ * Creates a horizontal ViewPagerLayoutManager
+ */
+ public OverFlyingLayoutManager(Context context) {
+ this(HORIZONTAL, false);
+ }
+
+ /**
+ * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
+ * @param reverseLayout When set to true, layouts from end to start
+ */
+ public OverFlyingLayoutManager(int orientation, boolean reverseLayout) {
+ setOrientation(orientation);
+ setReverseLayout(reverseLayout);
+ setAutoMeasureEnabled(true);
+ setEnableBringCenterToFront(true);
+ setIntegerDy(true);
+ }
+
+ public OverFlyingLayoutManager(float minScale, int itemSpace, int orientation) {
+ this(orientation, false);
+ this.minScale = minScale;
+ this.itemSpace = itemSpace;
+ mOrientation = orientation;
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ /**
+ * Returns whether LayoutManager will recycle its children when it is detached from
+ * RecyclerView.
+ *
+ * @return true if LayoutManager will recycle its children when it is detached from
+ * RecyclerView.
+ */
+ public boolean getRecycleChildrenOnDetach() {
+ return mRecycleChildrenOnDetach;
+ }
+
+ /**
+ * Set whether LayoutManager will recycle its children when it is detached from
+ * RecyclerView.
+ *
+ * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set
+ * this flag to
+ * Note that, setting this flag will result in a performance drop if RecyclerView
+ * is restored.
+ *
+ * @param recycleChildrenOnDetach Whether children should be recycled in detach or not.
+ */
+ public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
+ mRecycleChildrenOnDetach = recycleChildrenOnDetach;
+ }
+
+ @Override
+ public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
+ super.onDetachedFromWindow(view, recycler);
+ if (mRecycleChildrenOnDetach) {
+ removeAndRecycleAllViews(recycler);
+ recycler.clear();
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (mPendingSavedState != null) {
+ return new SavedState(mPendingSavedState);
+ }
+ SavedState savedState = new SavedState();
+ savedState.position = mPendingScrollPosition;
+ savedState.offset = mOffset;
+ savedState.isReverseLayout = mReverseLayout;
+ return savedState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ mPendingSavedState = new SavedState((SavedState) state);
+ requestLayout();
+ }
+ }
+
+ /**
+ * @return true if {@link #getOrientation()} is {@link #HORIZONTAL}
+ */
+ @Override
+ public boolean canScrollHorizontally() {
+ return mOrientation == HORIZONTAL;
+ }
+
+ /**
+ * @return true if {@link #getOrientation()} is {@link #VERTICAL}
+ */
+ @Override
+ public boolean canScrollVertically() {
+ return mOrientation == VERTICAL;
+ }
+
+ /**
+ * Returns the current orientation of the layout.
+ *
+ * @return Current orientation, either {@link #HORIZONTAL} or {@link #VERTICAL}
+ * @see #setOrientation(int)
+ */
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ /**
+ * will do its best to keep scroll position.
+ *
+ * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL && orientation != VERTICAL) {
+ throw new IllegalArgumentException("invalid orientation:" + orientation);
+ }
+ assertNotInLayoutOrScroll(null);
+ if (orientation == mOrientation) {
+ return;
+ }
+ mOrientation = orientation;
+ mOrientationHelper = null;
+ removeAllViews();
+ }
+
+ /**
+ * Returns the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now
+ * And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range
+ *
+ * @return Max visible item count
+ */
+ public int getMaxVisibleItemCount() {
+ return mMaxVisibleItemCount;
+ }
+
+ /**
+ * Set the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now
+ * And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range
+ *
+ * @param mMaxVisibleItemCount Max visible item count
+ */
+ public void setMaxVisibleItemCount(int mMaxVisibleItemCount) {
+ assertNotInLayoutOrScroll(null);
+ if (this.mMaxVisibleItemCount == mMaxVisibleItemCount) return;
+ this.mMaxVisibleItemCount = mMaxVisibleItemCount;
+ removeAllViews();
+ }
+
+ /**
+ * see {@link #mIntegerDy}
+ */
+ public boolean isIntegerDy() {
+ return mIntegerDy;
+ }
+
+ /**
+ * see {@link #mIntegerDy}
+ */
+ public void setIntegerDy(boolean mIntegerDy) {
+ this.mIntegerDy = mIntegerDy;
+ }
+
+ /**
+ * Calculates the view layout order. (e.g. from end to start or start to end)
+ * RTL layout support is applied automatically. So if layout is RTL and
+ * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
+ */
+ private void resolveShouldLayoutReverse() {
+ if (mOrientation == HORIZONTAL && getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) {
+ mReverseLayout = !mReverseLayout;
+ }
+ }
+
+ /**
+ * Returns if views are laid out from the opposite direction of the layout.
+ *
+ * @return If layout is reversed or not.
+ * @see #setReverseLayout(boolean)
+ */
+ public boolean getReverseLayout() {
+ return mReverseLayout;
+ }
+
+ /**
+ * Used to reverse item traversal and layout order.
+ * This behaves similar to the layout change for RTL views. When set to true, first item is
+ * laid out at the end of the UI, second item is laid out before it etc.
+ *
+ * For horizontal layouts, it depends on the layout direction.
+ * from LTR.
+ */
+ public void setReverseLayout(boolean reverseLayout) {
+ assertNotInLayoutOrScroll(null);
+ if (reverseLayout == mReverseLayout) {
+ return;
+ }
+ mReverseLayout = reverseLayout;
+ removeAllViews();
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
+ LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext());
+ linearSmoothScroller.setTargetPosition(position);
+ startSmoothScroll(linearSmoothScroller);
+ }
+
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ if (getChildCount() == 0) {
+ return null;
+ }
+ final int firstChildPos = getPosition(getChildAt(0));
+ final float direction = targetPosition < firstChildPos == !mReverseLayout ?
+ -1 / getDistanceRatio() : 1 / getDistanceRatio();
+ if (mOrientation == HORIZONTAL) {
+ return new PointF(direction, 0);
+ } else {
+ return new PointF(0, direction);
+ }
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (state.getItemCount() == 0) {
+ removeAndRecycleAllViews(recycler);
+ mOffset = 0;
+ return;
+ }
+
+ ensureLayoutState();
+ resolveShouldLayoutReverse();
+
+ //make sure properties are correct while measure more than once
+ View scrap = recycler.getViewForPosition(0);
+ measureChildWithMargins(scrap, 0, 0);
+ mDecoratedMeasurement = mOrientationHelper.getDecoratedMeasurement(scrap);
+ mDecoratedMeasurementInOther = mOrientationHelper.getDecoratedMeasurementInOther(scrap);
+ mSpaceMain = (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2;
+ mSpaceInOther = (getTotalSpaceInOther() - mDecoratedMeasurementInOther) / 2;
+ mInterval = setInterval();
+ setUp();
+ mLeftItems = (int) Math.abs(minRemoveOffset() / mInterval) + 1;
+ mRightItems = (int) Math.abs(maxRemoveOffset() / mInterval) + 1;
+
+ if (mPendingSavedState != null) {
+ mReverseLayout = mPendingSavedState.isReverseLayout;
+ mPendingScrollPosition = mPendingSavedState.position;
+ mOffset = mPendingSavedState.offset;
+ }
+
+ if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
+ mOffset = mReverseLayout ?
+ mPendingScrollPosition * -mInterval : mPendingScrollPosition * mInterval;
+ }
+
+ detachAndScrapAttachedViews(recycler);
+ layoutItems(recycler);
+ }
+
+ public int getTotalSpaceInOther() {
+ if (mOrientation == HORIZONTAL) {
+ return getHeight() - getPaddingTop()
+ - getPaddingBottom();
+ } else {
+ return getWidth() - getPaddingLeft()
+ - getPaddingRight();
+ }
+ }
+
+ @Override
+ public void onLayoutCompleted(RecyclerView.State state) {
+ super.onLayoutCompleted(state);
+ mPendingSavedState = null;
+ mPendingScrollPosition = RecyclerView.NO_POSITION;
+ }
+
+ void ensureLayoutState() {
+ if (mOrientationHelper == null) {
+ mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
+ }
+ }
+
+ /**
+ * You can set up your own properties here or change the exist properties like mSpaceMain and mSpaceInOther
+ */
+ protected void setUp() {
+
+ }
+
+ private float getProperty(int position) {
+ return mReverseLayout ? position * -mInterval : position * mInterval;
+ }
+
+ @Override
+ public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
+ removeAllViews();
+ mOffset = 0;
+ }
+
+ @Override
+ public void scrollToPosition(int position) {
+ mPendingScrollPosition = position;
+ mOffset = mReverseLayout ? position * -mInterval : position * mInterval;
+ requestLayout();
+ }
+
+ @Override
+ public int computeHorizontalScrollOffset(RecyclerView.State state) {
+ return computeScrollOffset();
+ }
+
+ @Override
+ public int computeVerticalScrollOffset(RecyclerView.State state) {
+ return computeScrollOffset();
+ }
+
+ @Override
+ public int computeHorizontalScrollExtent(RecyclerView.State state) {
+ return computeScrollExtent();
+ }
+
+ @Override
+ public int computeVerticalScrollExtent(RecyclerView.State state) {
+ return computeScrollExtent();
+ }
+
+ @Override
+ public int computeHorizontalScrollRange(RecyclerView.State state) {
+ return computeScrollRange();
+ }
+
+ @Override
+ public int computeVerticalScrollRange(RecyclerView.State state) {
+ return computeScrollRange();
+ }
+
+ private int computeScrollOffset() {
+ if (getChildCount() == 0) {
+ return 0;
+ }
+
+ if (!mSmoothScrollbarEnabled) {
+ return !mReverseLayout ?
+ getCurrentPosition() : getItemCount() - getCurrentPosition() - 1;
+ }
+
+ final float realOffset = getOffsetOfRightAdapterPosition();
+ return !mReverseLayout ? (int) realOffset : (int) ((getItemCount() - 1) * mInterval + realOffset);
+ }
+
+ private int computeScrollExtent() {
+ if (getChildCount() == 0) {
+ return 0;
+ }
+
+ if (!mSmoothScrollbarEnabled) {
+ return 1;
+ }
+
+ return (int) mInterval;
+ }
+
+ private int computeScrollRange() {
+ if (getChildCount() == 0) {
+ return 0;
+ }
+
+ if (!mSmoothScrollbarEnabled) {
+ return getItemCount();
+ }
+
+ return (int) (getItemCount() * mInterval);
+ }
+
+ @Override
+ public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (mOrientation == VERTICAL) {
+ return 0;
+ }
+ return scrollBy(dx, recycler, state);
+ }
+
+ @Override
+ public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (mOrientation == HORIZONTAL) {
+ return 0;
+ }
+ return scrollBy(dy, recycler, state);
+ }
+
+ private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (getChildCount() == 0 || dy == 0) {
+ return 0;
+ }
+ ensureLayoutState();
+ int willScroll = dy;
+
+ float realDx = dy / getDistanceRatio();
+ if (Math.abs(realDx) < 0.00000001f) {
+ return 0;
+ }
+ float targetOffset = mOffset + realDx;
+
+ //handle the boundary
+ if (!mInfinite && targetOffset < getMinOffset()) {
+ willScroll -= (targetOffset - getMinOffset()) * getDistanceRatio();
+ } else if (!mInfinite && targetOffset > getMaxOffset()) {
+ willScroll = (int) ((getMaxOffset() - mOffset) * getDistanceRatio());
+ }
+
+ if (mIntegerDy) {
+ realDx = (int) (willScroll / getDistanceRatio());
+ } else {
+ realDx = willScroll / getDistanceRatio();
+ }
+
+ mOffset += realDx;
+
+ // we re-layout all current views in the right place
+ for (int i = 0; i < getChildCount(); i++) {
+ final View scrap = getChildAt(i);
+ final float delta = propertyChangeWhenScroll(scrap) - realDx;
+ layoutScrap(scrap, delta);
+ }
+
+ //handle recycle
+ layoutItems(recycler);
+
+ return willScroll;
+ }
+
+ private void layoutItems(RecyclerView.Recycler recycler) {
+ detachAndScrapAttachedViews(recycler);
+
+ // make sure that current position start from 0 to 1
+ final int currentPos = mReverseLayout ?
+ -getCurrentPositionOffset() : getCurrentPositionOffset();
+ int start = currentPos - mLeftItems;
+ int end = currentPos + mRightItems;
+
+ // handle max visible count
+ if (useMaxVisibleCount()) {
+ boolean isEven = mMaxVisibleItemCount % 2 == 0;
+ if (isEven) {
+ int offset = mMaxVisibleItemCount / 2;
+ start = currentPos - offset + 1;
+ end = currentPos + offset + 1;
+ } else {
+ int offset = (mMaxVisibleItemCount - 1) / 2;
+ start = currentPos - offset;
+ end = currentPos + offset + 1;
+ }
+ }
+
+ final int itemCount = getItemCount();
+ if (!mInfinite) {
+ if (start < 0) {
+ start = 0;
+ if (useMaxVisibleCount()) end = mMaxVisibleItemCount;
+ }
+ if (end > itemCount) end = itemCount;
+ }
+
+ float lastOrderWeight = Float.MIN_VALUE;
+ for (int i = start; i < end; i++) {
+ if (useMaxVisibleCount() || !removeCondition(getProperty(i) - mOffset)) {
+ // start and end base on current position,
+ // so we need to calculate the adapter position
+ int adapterPosition = i;
+ if (i >= itemCount) {
+ adapterPosition %= itemCount;
+ } else if (i < 0) {
+ int delta = (-adapterPosition) % itemCount;
+ if (delta == 0) delta = itemCount;
+ adapterPosition = itemCount - delta;
+ }
+ final View scrap = recycler.getViewForPosition(adapterPosition);
+ measureChildWithMargins(scrap, 0, 0);
+ resetViewProperty(scrap);
+ // we need i to calculate the real offset of current view
+ final float targetOffset = getProperty(i) - mOffset;
+ layoutScrap(scrap, targetOffset);
+ final float orderWeight = mEnableBringCenterToFront ?
+ setViewElevation(scrap, targetOffset) : adapterPosition;
+ if (orderWeight > lastOrderWeight) {
+ addView(scrap);
+ } else {
+ addView(scrap, 0);
+ }
+ lastOrderWeight = orderWeight;
+ }
+ }
+ }
+
+ private boolean useMaxVisibleCount() {
+ return mMaxVisibleItemCount != DETERMINE_BY_MAX_AND_MIN;
+ }
+
+ private boolean removeCondition(float targetOffset) {
+ return targetOffset > maxRemoveOffset() || targetOffset < minRemoveOffset();
+ }
+
+ private void resetViewProperty(View v) {
+ v.setRotation(0);
+ v.setRotationY(0);
+ v.setRotationX(0);
+ v.setScaleX(1f);
+ v.setScaleY(1f);
+ v.setAlpha(1f);
+ }
+
+ private float getMaxOffset() {
+ return !mReverseLayout ? (getItemCount() - 1) * mInterval : 0;
+ }
+
+ private float getMinOffset() {
+ return !mReverseLayout ? 0 : -(getItemCount() - 1) * mInterval;
+ }
+
+ private void layoutScrap(View scrap, float targetOffset) {
+ final int left = calItemLeft(scrap, targetOffset);
+ final int top = calItemTop(scrap, targetOffset);
+ if (mOrientation == VERTICAL) {
+ layoutDecorated(scrap, mSpaceInOther + left, mSpaceMain + top,
+ mSpaceInOther + left + mDecoratedMeasurementInOther, mSpaceMain + top + mDecoratedMeasurement);
+ } else {
+ layoutDecorated(scrap, mSpaceMain + left, mSpaceInOther + top,
+ mSpaceMain + left + mDecoratedMeasurement, mSpaceInOther + top + mDecoratedMeasurementInOther);
+ }
+ setItemViewProperty(scrap, targetOffset);
+ }
+
+ protected int calItemLeft(View itemView, float targetOffset) {
+ return mOrientation == VERTICAL ? 0 : (int) targetOffset;
+ }
+
+ protected int calItemTop(View itemView, float targetOffset) {
+ return mOrientation == VERTICAL ? (int) targetOffset : 0;
+ }
+
+ /**
+ * when the target offset reach this,
+ * the view will be removed and recycled in {@link #layoutItems(RecyclerView.Recycler)}
+ */
+ protected float maxRemoveOffset() {
+ return mOrientationHelper.getTotalSpace() - mSpaceMain;
+ }
+
+ /**
+ * when the target offset reach this,
+ * the view will be removed and recycled in {@link #layoutItems(RecyclerView.Recycler)}
+ */
+ protected float minRemoveOffset() {
+ return -mDecoratedMeasurement - mOrientationHelper.getStartAfterPadding() - mSpaceMain;
+ }
+
+ protected float propertyChangeWhenScroll(View itemView) {
+ if (mOrientation == VERTICAL)
+ return itemView.getTop() - mSpaceMain;
+ return itemView.getLeft() - mSpaceMain;
+ }
+
+ protected float getDistanceRatio() {
+ return 1;
+ }
+
+ public int getCurrentPosition() {
+ int position = getCurrentPositionOffset();
+ if (!mInfinite) return Math.abs(position);
+ position = !mReverseLayout ?
+ //take care of position = getItemCount()
+ (position >= 0 ?
+ position % getItemCount() :
+ getItemCount() + position % getItemCount()) :
+ (position > 0 ?
+ getItemCount() - position % getItemCount() :
+ -position % getItemCount());
+ return position;
+ }
+
+ private int getCurrentPositionOffset() {
+ return Math.round(mOffset / mInterval);
+ }
+
+ /**
+ * Sometimes we need to get the right offset of matching adapter position
+ * cause when {@link #mInfinite} is set true, there will be no limitation of {@link #mOffset}
+ */
+ private float getOffsetOfRightAdapterPosition() {
+ if (mReverseLayout)
+ return mInfinite ?
+ (mOffset <= 0 ?
+ (mOffset % (mInterval * getItemCount())) :
+ (getItemCount() * -mInterval + mOffset % (mInterval * getItemCount()))) :
+ mOffset;
+ else
+ return mInfinite ?
+ (mOffset >= 0 ?
+ (mOffset % (mInterval * getItemCount())) :
+ (getItemCount() * mInterval + mOffset % (mInterval * getItemCount()))) :
+ mOffset;
+ }
+
+ /**
+ * @return the dy between center and current position
+ */
+ public int getOffsetToCenter() {
+ if (mInfinite)
+ return (int) ((getCurrentPositionOffset() * mInterval - mOffset) * getDistanceRatio());
+ return (int) ((getCurrentPosition() *
+ (!mReverseLayout ? mInterval : -mInterval) - mOffset) * getDistanceRatio());
+ }
+
+ public void setOnPageChangeListener(OnPageChangeListener onPageChangeListener) {
+ this.onPageChangeListener = onPageChangeListener;
+ }
+
+ public void setInfinite(boolean enable) {
+ assertNotInLayoutOrScroll(null);
+ if (enable == mInfinite) {
+ return;
+ }
+ mInfinite = enable;
+ requestLayout();
+ }
+
+ public boolean getInfinite() {
+ return mInfinite;
+ }
+
+ /**
+ * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed
+ * based on the number of visible pixels in the visible items. This however assumes that all
+ * list items have similar or equal widths or heights (depending on list orientation).
+ * If you use a list in which items have different dimensions, the scrollbar will change
+ * appearance as the user scrolls through the list. To avoid this issue, you need to disable
+ * this property.
+ *
+ * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based
+ * solely on the number of items in the adapter and the position of the visible items inside
+ * the adapter. This provides a stable scrollbar as the user navigates through a list of items
+ * with varying widths / heights.
+ *
+ * @param enabled Whether or not to enable smooth scrollbar.
+ * @see #setSmoothScrollbarEnabled(boolean)
+ */
+ public void setSmoothScrollbarEnabled(boolean enabled) {
+ mSmoothScrollbarEnabled = enabled;
+ }
+
+ public void setEnableBringCenterToFront(boolean bringCenterToTop) {
+ assertNotInLayoutOrScroll(null);
+ if (mEnableBringCenterToFront == bringCenterToTop) {
+ return;
+ }
+ this.mEnableBringCenterToFront = bringCenterToTop;
+ requestLayout();
+ }
+
+ public boolean getEnableBringCenterToFront() {
+ return mEnableBringCenterToFront;
+ }
+
+ /**
+ * Returns the current state of the smooth scrollbar feature. It is enabled by default.
+ *
+ * @return True if smooth scrollbar is enabled, false otherwise.
+ * @see #setSmoothScrollbarEnabled(boolean)
+ */
+ public boolean getSmoothScrollbarEnabled() {
+ return mSmoothScrollbarEnabled;
+ }
+
+ private static class SavedState implements Parcelable {
+ int position;
+ float offset;
+ boolean isReverseLayout;
+
+ SavedState() {
+
+ }
+
+ SavedState(Parcel in) {
+ position = in.readInt();
+ offset = in.readFloat();
+ isReverseLayout = in.readInt() == 1;
+ }
+
+ public SavedState(SavedState other) {
+ position = other.position;
+ offset = other.offset;
+ isReverseLayout = other.isReverseLayout;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(position);
+ dest.writeFloat(offset);
+ dest.writeInt(isReverseLayout ? 1 : 0);
+ }
+
+ public static final Creatortrue so that views will be available to other RecyclerViews
+ * immediately.
+ *