package com.azoft.carousellayoutmanager; import android.graphics.PointF; import android.os.Handler; import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.view.ViewCompat; import android.support.v7.widget.LinearSmoothScroller; import android.support.v7.widget.OrientationHelper; import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * An implementation of {@link RecyclerView.LayoutManager} that layout items like carousel. * Generally there is one center item and bellow this item there are maximum {@link CarouselLayoutManager#getMaxVisibleItems()} items on each side of the center * item. By default {@link CarouselLayoutManager#getMaxVisibleItems()} is {@link CarouselLayoutManager#MAX_VISIBLE_ITEMS}.<br /> * <br /> * This LayoutManager supports only fixedSized adapter items.<br /> * <br /> * This LayoutManager supports {@link CarouselLayoutManager#HORIZONTAL} and {@link CarouselLayoutManager#VERTICAL} orientations. <br /> * <br /> * This LayoutManager supports circle layout. By default it if disabled. We don't recommend to use circle layout with adapter items count less then 3. <br /> * <br /> * Please be sure that layout_width of adapter item is a constant value and not {@link ViewGroup.LayoutParams#MATCH_PARENT} * for {@link #HORIZONTAL} orientation. * So like layout_height is not {@link ViewGroup.LayoutParams#MATCH_PARENT} for {@link CarouselLayoutManager#VERTICAL}<br /> * <br /> */ @SuppressWarnings({"ClassWithTooManyMethods", "OverlyComplexClass", "unused"}) public class CarouselLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider { public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; public static final int VERTICAL = OrientationHelper.VERTICAL; public static final int INVALID_POSITION = -1; public static final int MAX_VISIBLE_ITEMS = 2; private static final boolean CIRCLE_LAYOUT = false; private Integer mDecoratedChildWidth; private Integer mDecoratedChildHeight; private final int mOrientation; private final boolean mCircleLayout; private int mPendingScrollPosition; private final LayoutHelper mLayoutHelper = new LayoutHelper(MAX_VISIBLE_ITEMS); private PostLayoutListener mViewPostLayout; private final List<OnCenterItemSelectionListener> mOnCenterItemSelectionListeners = new ArrayList<>(); private int mCenterItemPosition = INVALID_POSITION; private int mItemsCount; @Nullable private CarouselSavedState mPendingCarouselSavedState; /** * @param orientation should be {@link #VERTICAL} or {@link #HORIZONTAL} */ @SuppressWarnings("unused") public CarouselLayoutManager(final int orientation) { this(orientation, CIRCLE_LAYOUT); } /** * If circleLayout is true then all items will be in cycle. Scroll will be infinite on both sides. * * @param orientation should be {@link #VERTICAL} or {@link #HORIZONTAL} * @param circleLayout true for enabling circleLayout */ @SuppressWarnings("unused") public CarouselLayoutManager(final int orientation, final boolean circleLayout) { if (HORIZONTAL != orientation && VERTICAL != orientation) { throw new IllegalArgumentException("orientation should be HORIZONTAL or VERTICAL"); } mOrientation = orientation; mCircleLayout = circleLayout; mPendingScrollPosition = INVALID_POSITION; } /** * Setup {@link CarouselLayoutManager.PostLayoutListener} for this LayoutManager. * Its methods will be called for each visible view item after general LayoutManager layout finishes. <br /> * <br /> * Generally this method should be used for scaling and translating view item for better (different) view presentation of layouting. * * @param postLayoutListener listener for item layout changes. Can be null. */ @SuppressWarnings("unused") public void setPostLayoutListener(@Nullable final PostLayoutListener postLayoutListener) { mViewPostLayout = postLayoutListener; requestLayout(); } /** * Setup maximum visible (layout) items on each side of the center item. * Basically during scrolling there can be more visible items (+1 item on each side), but in idle state this is the only reached maximum. * * @param maxVisibleItems should be great then 0, if bot an {@link IllegalAccessException} will be thrown */ @CallSuper @SuppressWarnings("unused") public void setMaxVisibleItems(final int maxVisibleItems) { if (0 >= maxVisibleItems) { throw new IllegalArgumentException("maxVisibleItems can't be less then 1"); } mLayoutHelper.mMaxVisibleItems = maxVisibleItems; requestLayout(); } /** * @return current setup for maximum visible items. * @see #setMaxVisibleItems(int) */ @SuppressWarnings("unused") public int getMaxVisibleItems() { return mLayoutHelper.mMaxVisibleItems; } @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } /** * @return current layout orientation * @see #VERTICAL * @see #HORIZONTAL */ public int getOrientation() { return mOrientation; } @Override public boolean canScrollHorizontally() { return 0 != getChildCount() && HORIZONTAL == mOrientation; } @Override public boolean canScrollVertically() { return 0 != getChildCount() && VERTICAL == mOrientation; } /** * @return current layout center item */ public int getCenterItemPosition() { return mCenterItemPosition; } /** * @param onCenterItemSelectionListener listener that will trigger when ItemSelectionChanges. can't be null */ public void addOnItemSelectionListener(@NonNull final OnCenterItemSelectionListener onCenterItemSelectionListener) { mOnCenterItemSelectionListeners.add(onCenterItemSelectionListener); } /** * @param onCenterItemSelectionListener listener that was previously added by {@link #addOnItemSelectionListener(OnCenterItemSelectionListener)} */ public void removeOnItemSelectionListener(@NonNull final OnCenterItemSelectionListener onCenterItemSelectionListener) { mOnCenterItemSelectionListeners.remove(onCenterItemSelectionListener); } @SuppressWarnings("RefusedBequest") @Override public void scrollToPosition(final int position) { if (0 > position) { throw new IllegalArgumentException("position can't be less then 0. position is : " + position); } mPendingScrollPosition = position; requestLayout(); } @SuppressWarnings("RefusedBequest") @Override public void smoothScrollToPosition(@NonNull final RecyclerView recyclerView, @NonNull final RecyclerView.State state, final int position) { final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { @Override public int calculateDyToMakeVisible(final View view, final int snapPreference) { if (!canScrollVertically()) { return 0; } return getOffsetForCurrentView(view); } @Override public int calculateDxToMakeVisible(final View view, final int snapPreference) { if (!canScrollHorizontally()) { return 0; } return getOffsetForCurrentView(view); } }; linearSmoothScroller.setTargetPosition(position); startSmoothScroll(linearSmoothScroller); } @Override @Nullable public PointF computeScrollVectorForPosition(final int targetPosition) { if (0 == getChildCount()) { return null; } final float directionDistance = getScrollDirection(targetPosition); //noinspection NumericCastThatLosesPrecision final int direction = (int) -Math.signum(directionDistance); if (HORIZONTAL == mOrientation) { return new PointF(direction, 0); } else { return new PointF(0, direction); } } private float getScrollDirection(final int targetPosition) { final float currentScrollPosition = makeScrollPositionInRange0ToCount(getCurrentScrollPosition(), mItemsCount); if (mCircleLayout) { final float t1 = currentScrollPosition - targetPosition; final float t2 = Math.abs(t1) - mItemsCount; if (Math.abs(t1) > Math.abs(t2)) { return Math.signum(t1) * t2; } else { return t1; } } else { return currentScrollPosition - targetPosition; } } @Override public int scrollVerticallyBy(final int dy, @NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) { if (HORIZONTAL == mOrientation) { return 0; } return scrollBy(dy, recycler, state); } @Override public int scrollHorizontallyBy(final int dx, final RecyclerView.Recycler recycler, final RecyclerView.State state) { if (VERTICAL == mOrientation) { return 0; } return scrollBy(dx, recycler, state); } /** * This method is called from {@link #scrollHorizontallyBy(int, RecyclerView.Recycler, RecyclerView.State)} and * {@link #scrollVerticallyBy(int, RecyclerView.Recycler, RecyclerView.State)} to calculate needed scroll that is allowed. <br /> * <br /> * This method may do relayout work. * * @param diff distance that we want to scroll by * @param recycler Recycler to use for fetching potentially cached views for a position * @param state Transient state of RecyclerView * @return distance that we actually scrolled by */ @CallSuper protected int scrollBy(final int diff, @NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) { if (null == mDecoratedChildWidth || null == mDecoratedChildHeight) { return 0; } if (0 == getChildCount() || 0 == diff) { return 0; } final int resultScroll; if (mCircleLayout) { resultScroll = diff; mLayoutHelper.mScrollOffset += resultScroll; final int maxOffset = getScrollItemSize() * mItemsCount; while (0 > mLayoutHelper.mScrollOffset) { mLayoutHelper.mScrollOffset += maxOffset; } while (mLayoutHelper.mScrollOffset > maxOffset) { mLayoutHelper.mScrollOffset -= maxOffset; } mLayoutHelper.mScrollOffset -= resultScroll; } else { final int maxOffset = getMaxScrollOffset(); if (0 > mLayoutHelper.mScrollOffset + diff) { resultScroll = -mLayoutHelper.mScrollOffset; //to make it 0 } else if (mLayoutHelper.mScrollOffset + diff > maxOffset) { resultScroll = maxOffset - mLayoutHelper.mScrollOffset; //to make it maxOffset } else { resultScroll = diff; } } if (0 != resultScroll) { mLayoutHelper.mScrollOffset += resultScroll; fillData(recycler, state, false); } return resultScroll; } @Override public void onMeasure(final RecyclerView.Recycler recycler, final RecyclerView.State state, final int widthSpec, final int heightSpec) { mDecoratedChildHeight = null; mDecoratedChildWidth = null; super.onMeasure(recycler, state, widthSpec, heightSpec); } @SuppressWarnings("rawtypes") @Override public void onAdapterChanged(final RecyclerView.Adapter oldAdapter, final RecyclerView.Adapter newAdapter) { super.onAdapterChanged(oldAdapter, newAdapter); removeAllViews(); } @SuppressWarnings("RefusedBequest") @Override @CallSuper public void onLayoutChildren(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) { if (0 == state.getItemCount()) { removeAndRecycleAllViews(recycler); selectItemCenterPosition(INVALID_POSITION); return; } boolean childMeasuringNeeded = false; if (null == mDecoratedChildWidth) { final View view = recycler.getViewForPosition(0); addView(view); measureChildWithMargins(view, 0, 0); mDecoratedChildWidth = getDecoratedMeasuredWidth(view); mDecoratedChildHeight = getDecoratedMeasuredHeight(view); removeAndRecycleView(view, recycler); if (INVALID_POSITION == mPendingScrollPosition && null == mPendingCarouselSavedState) { mPendingScrollPosition = mCenterItemPosition; } childMeasuringNeeded = true; } if (INVALID_POSITION != mPendingScrollPosition) { final int itemsCount = state.getItemCount(); mPendingScrollPosition = 0 == itemsCount ? INVALID_POSITION : Math.max(0, Math.min(itemsCount - 1, mPendingScrollPosition)); } if (INVALID_POSITION != mPendingScrollPosition) { mLayoutHelper.mScrollOffset = calculateScrollForSelectingPosition(mPendingScrollPosition, state); mPendingScrollPosition = INVALID_POSITION; mPendingCarouselSavedState = null; } else if (null != mPendingCarouselSavedState) { mLayoutHelper.mScrollOffset = calculateScrollForSelectingPosition(mPendingCarouselSavedState.mCenterItemPosition, state); mPendingCarouselSavedState = null; } else if (state.didStructureChange() && INVALID_POSITION != mCenterItemPosition) { mLayoutHelper.mScrollOffset = calculateScrollForSelectingPosition(mCenterItemPosition, state); } fillData(recycler, state, childMeasuringNeeded); } private int calculateScrollForSelectingPosition(final int itemPosition, final RecyclerView.State state) { final int fixedItemPosition = itemPosition < state.getItemCount() ? itemPosition : state.getItemCount() - 1; return fixedItemPosition * (VERTICAL == mOrientation ? mDecoratedChildHeight : mDecoratedChildWidth); } private void fillData(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state, final boolean childMeasuringNeeded) { final float currentScrollPosition = getCurrentScrollPosition(); generateLayoutOrder(currentScrollPosition, state); detachAndScrapAttachedViews(recycler); final int width = getWidthNoPadding(); final int height = getHeightNoPadding(); if (VERTICAL == mOrientation) { fillDataVertical(recycler, width, height, childMeasuringNeeded); } else { fillDataHorizontal(recycler, width, height, childMeasuringNeeded); } recycler.clear(); detectOnItemSelectionChanged(currentScrollPosition, state); } private void detectOnItemSelectionChanged(final float currentScrollPosition, final RecyclerView.State state) { final float absCurrentScrollPosition = makeScrollPositionInRange0ToCount(currentScrollPosition, state.getItemCount()); final int centerItem = Math.round(absCurrentScrollPosition); if (mCenterItemPosition != centerItem) { mCenterItemPosition = centerItem; new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { selectItemCenterPosition(centerItem); } }); } } private void selectItemCenterPosition(final int centerItem) { for (final OnCenterItemSelectionListener onCenterItemSelectionListener : mOnCenterItemSelectionListeners) { onCenterItemSelectionListener.onCenterItemChanged(centerItem); } } private void fillDataVertical(final RecyclerView.Recycler recycler, final int width, final int height, final boolean childMeasuringNeeded) { final int start = (width - mDecoratedChildWidth) / 2; final int end = start + mDecoratedChildWidth; final int centerViewTop = (height - mDecoratedChildHeight) / 2; for (int i = 0, count = mLayoutHelper.mLayoutOrder.length; i < count; ++i) { final LayoutOrder layoutOrder = mLayoutHelper.mLayoutOrder[i]; final int offset = getCardOffsetByPositionDiff(layoutOrder.mItemPositionDiff); final int top = centerViewTop + offset; final int bottom = top + mDecoratedChildHeight; fillChildItem(start, top, end, bottom, layoutOrder, recycler, i, childMeasuringNeeded); } } private void fillDataHorizontal(final RecyclerView.Recycler recycler, final int width, final int height, final boolean childMeasuringNeeded) { final int top = (height - mDecoratedChildHeight) / 2; final int bottom = top + mDecoratedChildHeight; final int centerViewStart = (width - mDecoratedChildWidth) / 2; for (int i = 0, count = mLayoutHelper.mLayoutOrder.length; i < count; ++i) { final LayoutOrder layoutOrder = mLayoutHelper.mLayoutOrder[i]; final int offset = getCardOffsetByPositionDiff(layoutOrder.mItemPositionDiff); final int start = centerViewStart + offset; final int end = start + mDecoratedChildWidth; fillChildItem(start, top, end, bottom, layoutOrder, recycler, i, childMeasuringNeeded); } } @SuppressWarnings("MethodWithTooManyParameters") private void fillChildItem(final int start, final int top, final int end, final int bottom, @NonNull final LayoutOrder layoutOrder, @NonNull final RecyclerView.Recycler recycler, final int i, final boolean childMeasuringNeeded) { final View view = bindChild(layoutOrder.mItemAdapterPosition, recycler, childMeasuringNeeded); ViewCompat.setElevation(view, i); ItemTransformation transformation = null; if (null != mViewPostLayout) { transformation = mViewPostLayout.transformChild(view, layoutOrder.mItemPositionDiff, mOrientation); } if (null == transformation) { view.layout(start, top, end, bottom); } else { view.layout(Math.round(start + transformation.mTranslationX), Math.round(top + transformation.mTranslationY), Math.round(end + transformation.mTranslationX), Math.round(bottom + transformation.mTranslationY)); ViewCompat.setScaleX(view, transformation.mScaleX); ViewCompat.setScaleY(view, transformation.mScaleY); } } /** * @return current scroll position of center item. this value can be in any range if it is cycle layout. * if this is not, that then it is in [0, {@link #mItemsCount - 1}] */ private float getCurrentScrollPosition() { final int fullScrollSize = getMaxScrollOffset(); if (0 == fullScrollSize) { return 0; } return 1.0f * mLayoutHelper.mScrollOffset / getScrollItemSize(); } /** * @return maximum scroll value to fill up all items in layout. Generally this is only needed for non cycle layouts. */ private int getMaxScrollOffset() { return getScrollItemSize() * (mItemsCount - 1); } /** * Because we can support old Android versions, we should layout our children in specific order to make our center view in the top of layout * (this item should layout last). So this method will calculate layout order and fill up {@link #mLayoutHelper} object. * This object will be filled by only needed to layout items. Non visible items will not be there. * * @param currentScrollPosition current scroll position this is a value that indicates position of center item * (if this value is int, then center item is really in the center of the layout, else it is near state). * Be aware that this value can be in any range is it is cycle layout * @param state Transient state of RecyclerView * @see #getCurrentScrollPosition() */ private void generateLayoutOrder(final float currentScrollPosition, @NonNull final RecyclerView.State state) { mItemsCount = state.getItemCount(); final float absCurrentScrollPosition = makeScrollPositionInRange0ToCount(currentScrollPosition, mItemsCount); final int centerItem = Math.round(absCurrentScrollPosition); if (mCircleLayout && 1 < mItemsCount) { final int layoutCount = Math.min(mLayoutHelper.mMaxVisibleItems * 2 + 3, mItemsCount);// + 3 = 1 (center item) + 2 (addition bellow maxVisibleItems) mLayoutHelper.initLayoutOrder(layoutCount); final int countLayoutHalf = layoutCount / 2; // before center item for (int i = 1; i <= countLayoutHalf; ++i) { final int position = Math.round(absCurrentScrollPosition - i + mItemsCount) % mItemsCount; mLayoutHelper.setLayoutOrder(countLayoutHalf - i, position, centerItem - absCurrentScrollPosition - i); } // after center item for (int i = layoutCount - 1; i >= countLayoutHalf + 1; --i) { final int position = Math.round(absCurrentScrollPosition - i + layoutCount) % mItemsCount; mLayoutHelper.setLayoutOrder(i - 1, position, centerItem - absCurrentScrollPosition + layoutCount - i); } mLayoutHelper.setLayoutOrder(layoutCount - 1, centerItem, centerItem - absCurrentScrollPosition); } else { final int firstVisible = Math.max(centerItem - mLayoutHelper.mMaxVisibleItems - 1, 0); final int lastVisible = Math.min(centerItem + mLayoutHelper.mMaxVisibleItems + 1, mItemsCount - 1); final int layoutCount = lastVisible - firstVisible + 1; mLayoutHelper.initLayoutOrder(layoutCount); for (int i = firstVisible; i <= lastVisible; ++i) { if (i == centerItem) { mLayoutHelper.setLayoutOrder(layoutCount - 1, i, i - absCurrentScrollPosition); } else if (i < centerItem) { mLayoutHelper.setLayoutOrder(i - firstVisible, i, i - absCurrentScrollPosition); } else { mLayoutHelper.setLayoutOrder(layoutCount - (i - centerItem) - 1, i, i - absCurrentScrollPosition); } } } } public int getWidthNoPadding() { return getWidth() - getPaddingStart() - getPaddingEnd(); } public int getHeightNoPadding() { return getHeight() - getPaddingEnd() - getPaddingStart(); } private View bindChild(final int position, @NonNull final RecyclerView.Recycler recycler, final boolean childMeasuringNeeded) { final View view = recycler.getViewForPosition(position); addView(view); measureChildWithMargins(view, 0, 0); return view; } /** * Called during {@link #fillData(RecyclerView.Recycler, RecyclerView.State, boolean)} to calculate item offset from layout center line. <br /> * <br /> * Returns {@link #convertItemPositionDiffToSmoothPositionDiff(float)} * (size off area above center item when it is on the center). <br /> * Sign is: plus if this item is bellow center line, minus if not<br /> * <br /> * ----- - area above it<br /> * ||||| - center item<br /> * ----- - area bellow it (it has the same size as are above center item)<br /> * * @param itemPositionDiff current item difference with layout center line. if this is 0, then this item center is in layout center line. * if this is 1 then this item is bellow the layout center line in the full item size distance. * @return offset in scroll px coordinates. */ protected int getCardOffsetByPositionDiff(final float itemPositionDiff) { final double smoothPosition = convertItemPositionDiffToSmoothPositionDiff(itemPositionDiff); final int dimenDiff; if (VERTICAL == mOrientation) { dimenDiff = (getHeightNoPadding() - mDecoratedChildHeight) / 2; } else { dimenDiff = (getWidthNoPadding() - mDecoratedChildWidth) / 2; } //noinspection NumericCastThatLosesPrecision return (int) Math.round(Math.signum(itemPositionDiff) * dimenDiff * smoothPosition); } /** * Called during {@link #getCardOffsetByPositionDiff(float)} for better item movement. <br/> * Current implementation speed up items that are far from layout center line and slow down items that are close to this line. * This code is full of maths. If you want to make items move in a different way, probably you should override this method.<br /> * Please see code comments for better explanations. * * @param itemPositionDiff current item difference with layout center line. if this is 0, then this item center is in layout center line. * if this is 1 then this item is bellow the layout center line in the full item size distance. * @return smooth position offset. needed for scroll calculation and better user experience. * @see #getCardOffsetByPositionDiff(float) */ @SuppressWarnings({"MagicNumber", "InstanceMethodNamingConvention"}) protected double convertItemPositionDiffToSmoothPositionDiff(final float itemPositionDiff) { // generally item moves the same way above center and bellow it. So we don't care about diff sign. final float absIemPositionDiff = Math.abs(itemPositionDiff); // we detect if this item is close for center or not. We use (1 / maxVisibleItem) ^ (1/3) as close definer. if (absIemPositionDiff > StrictMath.pow(1.0f / mLayoutHelper.mMaxVisibleItems, 1.0f / 3)) { // this item is far from center line, so we should make it move like square root function return StrictMath.pow(absIemPositionDiff / mLayoutHelper.mMaxVisibleItems, 1 / 2.0f); } else { // this item is close from center line. we should slow it down and don't make it speed up very quick. // so square function in range of [0, (1/maxVisible)^(1/3)] is quite good in it; return StrictMath.pow(absIemPositionDiff, 2.0f); } } /** * @return full item size */ protected int getScrollItemSize() { if (VERTICAL == mOrientation) { return mDecoratedChildHeight; } else { return mDecoratedChildWidth; } } @Override public Parcelable onSaveInstanceState() { if (null != mPendingCarouselSavedState) { return new CarouselSavedState(mPendingCarouselSavedState); } final CarouselSavedState savedState = new CarouselSavedState(super.onSaveInstanceState()); savedState.mCenterItemPosition = mCenterItemPosition; return savedState; } @Override public void onRestoreInstanceState(final Parcelable state) { if (state instanceof CarouselSavedState) { mPendingCarouselSavedState = (CarouselSavedState) state; super.onRestoreInstanceState(mPendingCarouselSavedState.mSuperState); } else { super.onRestoreInstanceState(state); } } /** * @return Scroll offset from nearest item from center */ protected int getOffsetCenterView() { return Math.round(getCurrentScrollPosition()) * getScrollItemSize() - mLayoutHelper.mScrollOffset; } protected int getOffsetForCurrentView(@NonNull final View view) { final int targetPosition = getPosition(view); final float directionDistance = getScrollDirection(targetPosition); final int distance = Math.round(directionDistance * getScrollItemSize()); if (mCircleLayout) { return distance; } else { return distance; } } /** * Helper method that make scroll in range of [0, count). Generally this method is needed only for cycle layout. * * @param currentScrollPosition any scroll position range. * @param count adapter items count * @return good scroll position in range of [0, count) */ private static float makeScrollPositionInRange0ToCount(final float currentScrollPosition, final int count) { float absCurrentScrollPosition = currentScrollPosition; while (0 > absCurrentScrollPosition) { absCurrentScrollPosition += count; } while (Math.round(absCurrentScrollPosition) >= count) { absCurrentScrollPosition -= count; } return absCurrentScrollPosition; } /** * This interface methods will be called for each visible view item after general LayoutManager layout finishes. <br /> * <br /> * Generally this method should be used for scaling and translating view item for better (different) view presentation of layouting. */ @SuppressWarnings("InterfaceNeverImplemented") public interface PostLayoutListener { /** * Called after child layout finished. Generally you can do any translation and scaling work here. * * @param child view that was layout * @param itemPositionToCenterDiff view center line difference to layout center. if > 0 then this item is bellow layout center line, else if not * @param orientation layoutManager orientation {@link #getLayoutDirection()} */ ItemTransformation transformChild(@NonNull final View child, final float itemPositionToCenterDiff, final int orientation); } public interface OnCenterItemSelectionListener { /** * Listener that will be called on every change of center item. * This listener will be triggered on <b>every</b> layout operation if item was changed. * Do not do any expensive operations in this method since this will effect scroll experience. * * @param adapterPosition current layout center item */ void onCenterItemChanged(final int adapterPosition); } /** * Helper class that holds currently visible items. * Generally this class fills this list. <br /> * <br /> * This class holds all scroll and maxVisible items state. * * @see #getMaxVisibleItems() */ private static class LayoutHelper { private int mMaxVisibleItems; private int mScrollOffset; private LayoutOrder[] mLayoutOrder; private final List<WeakReference<LayoutOrder>> mReusedItems = new ArrayList<>(); LayoutHelper(final int maxVisibleItems) { mMaxVisibleItems = maxVisibleItems; } /** * Called before any fill calls. Needed to recycle old items and init new array list. Generally this list is an array an it is reused. * * @param layoutCount items count that will be layout */ void initLayoutOrder(final int layoutCount) { if (null == mLayoutOrder || mLayoutOrder.length != layoutCount) { if (null != mLayoutOrder) { recycleItems(mLayoutOrder); } mLayoutOrder = new LayoutOrder[layoutCount]; fillLayoutOrder(); } } /** * Called during layout generation process of filling this list. Should be called only after {@link #initLayoutOrder(int)} method call. * * @param arrayPosition position in layout order * @param itemAdapterPosition adapter position of item for future data filling logic * @param itemPositionDiff difference of current item scroll position and center item position. * if this is a center item and it is in real center of layout, then this will be 0. * if current layout is not in the center, then this value will never be int. * if this item center is bellow layout center line then this value is greater then 0, * else less then 0. */ void setLayoutOrder(final int arrayPosition, final int itemAdapterPosition, final float itemPositionDiff) { final LayoutOrder item = mLayoutOrder[arrayPosition]; item.mItemAdapterPosition = itemAdapterPosition; item.mItemPositionDiff = itemPositionDiff; } /** * Checks is this screen Layout has this adapterPosition view in layout * * @param adapterPosition adapter position of item for future data filling logic * @return true is adapterItem is in layout */ boolean hasAdapterPosition(final int adapterPosition) { if (null != mLayoutOrder) { for (final LayoutOrder layoutOrder : mLayoutOrder) { if (layoutOrder.mItemAdapterPosition == adapterPosition) { return true; } } } return false; } @SuppressWarnings("VariableArgumentMethod") private void recycleItems(@NonNull final LayoutOrder... layoutOrders) { for (final LayoutOrder layoutOrder : layoutOrders) { //noinspection ObjectAllocationInLoop mReusedItems.add(new WeakReference<>(layoutOrder)); } } private void fillLayoutOrder() { for (int i = 0, length = mLayoutOrder.length; i < length; ++i) { if (null == mLayoutOrder[i]) { mLayoutOrder[i] = createLayoutOrder(); } } } private LayoutOrder createLayoutOrder() { final Iterator<WeakReference<LayoutOrder>> iterator = mReusedItems.iterator(); while (iterator.hasNext()) { final WeakReference<LayoutOrder> layoutOrderWeakReference = iterator.next(); final LayoutOrder layoutOrder = layoutOrderWeakReference.get(); iterator.remove(); if (null != layoutOrder) { return layoutOrder; } } return new LayoutOrder(); } } /** * Class that holds item data. * This class is filled during {@link #generateLayoutOrder(float, RecyclerView.State)} and used during {@link #fillData(RecyclerView.Recycler, RecyclerView.State, boolean)} */ private static class LayoutOrder { /** * Item adapter position */ private int mItemAdapterPosition; /** * Item center difference to layout center. If center of item is bellow layout center, then this value is greater then 0, else it is less. */ private float mItemPositionDiff; } protected static class CarouselSavedState implements Parcelable { private final Parcelable mSuperState; private int mCenterItemPosition; protected CarouselSavedState(@Nullable final Parcelable superState) { mSuperState = superState; } private CarouselSavedState(@NonNull final Parcel in) { mSuperState = in.readParcelable(Parcelable.class.getClassLoader()); mCenterItemPosition = in.readInt(); } protected CarouselSavedState(@NonNull final CarouselSavedState other) { mSuperState = other.mSuperState; mCenterItemPosition = other.mCenterItemPosition; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(final Parcel parcel, final int i) { parcel.writeParcelable(mSuperState, i); parcel.writeInt(mCenterItemPosition); } public static final Parcelable.Creator<CarouselSavedState> CREATOR = new Parcelable.Creator<CarouselSavedState>() { @Override public CarouselSavedState createFromParcel(final Parcel parcel) { return new CarouselSavedState(parcel); } @Override public CarouselSavedState[] newArray(final int i) { return new CarouselSavedState[i]; } }; } }