package com.cleveroad.loopbar.widget; import android.animation.Animator; import android.animation.AnimatorInflater; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.PorterDuff; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.MenuRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.view.menu.MenuBuilder; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import com.cleveroad.loopbar.BuildConfig; import com.cleveroad.loopbar.R; import com.cleveroad.loopbar.adapter.ICategoryItem; import com.cleveroad.loopbar.adapter.ILoopBarPagerAdapter; import com.cleveroad.loopbar.adapter.IOperationItem; import com.cleveroad.loopbar.adapter.SimpleCategoriesAdapter; import com.cleveroad.loopbar.adapter.SimpleCategoriesMenuAdapter; import com.cleveroad.loopbar.model.CategoryItem; import com.cleveroad.loopbar.model.MockedItemsFactory; import com.cleveroad.loopbar.util.AbstractAnimatorListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; public class LoopBarView extends FrameLayout implements OnItemClickListener { /** * Gravity constant for selector. * Representing state of selector attached to the left if LoopBar is horizontal * and to the top if LoopBar is vertical * * @see GravityAttr * @see #setGravity(int) * @see #getGravity() */ public static final int SELECTION_GRAVITY_START = 0; /** * Gravity constant for selector. * Representing state of selector attached to the right if LoopBar is horizontal * and to the bottom if LoopBar is vertical * * @see GravityAttr * @see #setGravity(int) * @see #getGravity() */ public static final int SELECTION_GRAVITY_END = 1; /** * Scroll mode constant for LoopBar * Representing automatic (adapting) scrolling state of LoopBar * If amount of items in LoopBar won't be enough to get out of bounds of LoopBar * (i.e. all items fit on screen) it will have finite behavior {@link #SCROLL_MODE_FINITE}. * In another case there will be infinite behavior {@link #SCROLL_MODE_INFINITE} * (there will be displayed only one appearance of each added item in LoopBar) * * @see ScrollAttr * @see #setScrollMode(int) * @see #getScrollMode() * @see #SCROLL_MODE_FINITE * @see #SCROLL_MODE_INFINITE */ public static final int SCROLL_MODE_AUTO = 2; /** * Scroll mode constant for LoopBar * Representing infinite scrolling state of LoopBar * (items will repeatedly show in the LoopBar while you scroll it) * * @see ScrollAttr * @see #setScrollMode(int) * @see #getScrollMode() */ public static final int SCROLL_MODE_INFINITE = 3; /** * Scroll mode constant for LoopBar * Representing finite scrolling state of LoopBar * (there will be displayed only one appearance of each added item in LoopBar) * * @see ScrollAttr * @see #setScrollMode(int) * @see #getScrollMode() */ public static final int SCROLL_MODE_FINITE = 4; private static final String TAG = LoopBarView.class.getSimpleName(); //Outside params private RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mInputAdapter; private List<OnItemClickListener> mClickListeners = new ArrayList<>(); private int mColorCodeSelectionView; //View settings private Animator mSelectionInAnimator; private Animator mSelectionOutAnimator; private int mSelectionMargin; private IOrientationState mOrientationState; private int mPlaceHolderId; private int mOverlaySize; //State settings below private int mCurrentItemPosition; @GravityAttr private int mSelectionGravity; private int mRealHidedPosition = 0; //Views private FrameLayout mFlContainerSelected; private RecyclerView mRvCategories; private View mVRvContainer; @Nullable private View mOverlayPlaceholder; private View mViewColorable; private ChangeScrollModeAdapter.ChangeScrollModeHolder mSelectorHolder; private ChangeScrollModeAdapter mOuterAdapter; private LinearLayoutManager mLinearLayoutManager; private boolean mSkipNextOnLayout; private boolean mIndeterminateInitialized; private boolean mInfinite; @ScrollAttr private int mScrollMode; private int mOrientation; private IndeterminateOnScrollListener mIndeterminateOnScrollListener = new IndeterminateOnScrollListener(this); public LoopBarView(Context context) { super(context); init(context, null); } public LoopBarView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public LoopBarView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public LoopBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs); } private void inflate(IOrientationState orientationState, int placeHolderId, int backgroundResource) { inflate(getContext(), orientationState.getLayoutId(), this); mFlContainerSelected = (FrameLayout) findViewById(R.id.flContainerSelected); mRvCategories = (RecyclerView) findViewById(R.id.rvCategories); mVRvContainer = findViewById(R.id.vRvContainer); mOverlayPlaceholder = getRootView().findViewById(placeHolderId); mViewColorable = getRootView().findViewById(R.id.flColorable); /* Background color must be set to container of recyclerView. * If you set it to main view, there will be any transparent part * when selector has overlay */ mVRvContainer.setBackgroundResource(backgroundResource); } private void init(Context context, @Nullable AttributeSet attrs) { //read customization attributes TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoopBarView); mColorCodeSelectionView = typedArray.getColor(R.styleable.LoopBarView_enls_selectionBackground, ContextCompat.getColor(getContext(), android.R.color.holo_blue_dark)); mOrientation = typedArray .getInteger(R.styleable.LoopBarView_enls_orientation, Orientation.ORIENTATION_HORIZONTAL_BOTTOM); int selectionAnimatorInId = typedArray .getResourceId(R.styleable.LoopBarView_enls_selectionInAnimation, R.animator.enls_scale_restore); int selectionAnimatorOutId = typedArray .getResourceId(R.styleable.LoopBarView_enls_selectionOutAnimation, R.animator.enls_scale_small); mPlaceHolderId = typedArray.getResourceId(R.styleable.LoopBarView_enls_placeholderId, -1); @GravityAttr int selectionGravity = typedArray .getInteger(R.styleable.LoopBarView_enls_selectionGravity, SELECTION_GRAVITY_START); mSelectionGravity = selectionGravity; mInfinite = typedArray.getBoolean(R.styleable.LoopBarView_enls_infiniteScrolling, true); @ScrollAttr int scrollMode = typedArray.getInt(R.styleable.LoopBarView_enls_scrollMode, mInfinite ? SCROLL_MODE_INFINITE : SCROLL_MODE_FINITE); mScrollMode = scrollMode; mSelectionMargin = typedArray.getDimensionPixelSize(R.styleable.LoopBarView_enls_selectionMargin, getResources().getDimensionPixelSize(R.dimen.enls_margin_selected_view)); mOverlaySize = typedArray.getDimensionPixelSize(R.styleable.LoopBarView_enls_overlaySize, 0); int backgroundResource = typedArray.getResourceId(R.styleable.LoopBarView_enls_listBackground, R.color.enls_default_list_background); mSelectionInAnimator = AnimatorInflater.loadAnimator(getContext(), selectionAnimatorInId); mSelectionOutAnimator = AnimatorInflater.loadAnimator(getContext(), selectionAnimatorOutId); //Current view has four state : horizontalBottom, horizontalTop & verticalLeft, verticalRight. State design pattern mOrientationState = getOrientationStateFromParam(mOrientation); inflate(mOrientationState, mPlaceHolderId, backgroundResource); setGravity(selectionGravity); ColorDrawable colorDrawable = new NegativeMarginFixColorDrawable(mColorCodeSelectionView); if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) { mViewColorable.setBackgroundDrawable(colorDrawable); } else { mViewColorable.setBackground(colorDrawable); } mLinearLayoutManager = mOrientationState.getLayoutManager(getContext()); mRvCategories.setLayoutManager(mLinearLayoutManager); if (isInEditMode()) { setCategoriesAdapter(new SimpleCategoriesAdapter(MockedItemsFactory.getCategoryItems(getContext()))); } int menuId = typedArray.getResourceId(R.styleable.LoopBarView_enls_menu, -1); if (menuId != -1) { setCategoriesAdapterFromMenu(menuId); } typedArray.recycle(); } /** * Set list background to a given resource * * @param backgroundResource The identifier of the resource */ public void setListBackgroundResource(@DrawableRes int backgroundResource) { mVRvContainer.setBackgroundResource(backgroundResource); } /** * Set list background color. * * @param color the color of the list background */ public void setListBackgroundColor(@ColorInt int color) { mVRvContainer.setBackgroundColor(color); } /** * Gets list background drawable * * @return The drawable used as list background * @see #setListBackground(Drawable) */ public Drawable getListBackground() { return mVRvContainer.getBackground(); } /** * Set list background to a given Drawable * * @param background The Drawable to use as the list background */ public void setListBackground(Drawable background) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mVRvContainer.setBackground(background); } else { mVRvContainer.setBackgroundDrawable(background); } } /** * Applies a tint to the list background drawable. * * @param tint the tint to apply * * @see #getListBackgroundTintList() */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public void setListBackgroundTintList(@Nullable ColorStateList tint) { mVRvContainer.setBackgroundTintList(tint); } /** * Return the tint applied to the list background drawable * * @return the tint applied to the list background drawable * * @see #setListBackgroundTintList(ColorStateList) */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Nullable public ColorStateList getListBackgroundTintList() { return mVRvContainer.getBackgroundTintList(); } /** * Specifies the blending mode used to apply the tint specified by * {@link #setListBackgroundTintList(ColorStateList)}} to the list background * drawable. * * @param tintMode the blending mode used to apply the tint * * @see #getListBackgroundTintMode() */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public void setListBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { mVRvContainer.setBackgroundTintMode(tintMode); } /** * Return the blending mode used to apply the tint to the list background * drawable * * @return the blending mode used to apply the tint to the list background * drawable * * @see #setListBackgroundTintMode(PorterDuff.Mode) */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Nullable public PorterDuff.Mode getListBackgroundTintMode() { return mVRvContainer.getBackgroundTintMode(); } /** * Gets current value of orientation * * @return int constant representing current orientation of loopbar * Will be one of {@link Orientation} */ public final int getOrientation() { return mOrientation; } /** * Sets orientation of loopbar * * @param orientation int value of orientation. Must be one of {@link Orientation} */ public final void setOrientation(int orientation) { mOrientation = orientation; mOrientationState = getOrientationStateFromParam(mOrientation); invalidate(); if (mOuterAdapter != null) { mOuterAdapter.setOrientation(mOrientation); } } /** * Gets current value of selector gravity * * @return int constant representing current gravity for selector. * Will be one of {@link GravityAttr} */ @GravityAttr public final int getGravity() { return mSelectionGravity; } /** * Sets new gravity for selector * * @param selectionGravity int value of gravity. Must be one of {@link GravityAttr} */ public final void setGravity(@GravityAttr int selectionGravity) { mOrientationState.setSelectionGravity(selectionGravity); //note that mFlContainerSelected should be in FrameLayout FrameLayout.LayoutParams params = (LayoutParams) mFlContainerSelected.getLayoutParams(); params.gravity = mOrientationState.getSelectionGravity(); mOrientationState.setSelectionMargin(mSelectionMargin, params); mFlContainerSelected.setLayoutParams(params); mSelectionGravity = selectionGravity; invalidate(); if (mOuterAdapter != null) { mOuterAdapter.setSelectedGravity(selectionGravity); } } /** * Sets scroll mode to infinite or finite * * @param isInfinite value presents is scroll mode need to be infinite * @deprecated use {@link #setScrollMode(int)} instead */ @Deprecated public void setIsInfinite(boolean isInfinite) { setScrollMode(isInfinite ? SCROLL_MODE_INFINITE : SCROLL_MODE_FINITE); } private void changeScrolling(boolean isInfinite) { if (mInfinite != isInfinite) { mInfinite = isInfinite; if (mOuterAdapter != null) { mOuterAdapter.setIsIndeterminate(isInfinite); } checkAndScroll(); } } /** * Returns constant representing current scroll mode * * @return one of {@link ScrollAttr} */ @ScrollAttr public final int getScrollMode() { return mScrollMode; } /** * Sets new Scroll mode for LoopBar * * @param scrollMode must be one of {@link ScrollAttr} */ public final void setScrollMode(@ScrollAttr int scrollMode) { if (scrollMode != mScrollMode) { mScrollMode = scrollMode; validateScrollMode(); } } /** * Returns boolean value representing if LoopBar is in infinite mode or not * * @return true if LoopBar is in infinite mode or false if is in finite */ public boolean isInfinite() { return mInfinite; } private void validateScrollMode() { if (mScrollMode == SCROLL_MODE_AUTO) { if (mOrientationState != null && mRvCategories != null && mOuterAdapter != null) { boolean isFitOnScreen = mOrientationState.isItemsFitOnScreen(mRvCategories, mOuterAdapter.getWrappedItems().size()); changeScrolling(!isFitOnScreen); } } else if (mScrollMode == SCROLL_MODE_INFINITE) { changeScrolling(true); } else if (mScrollMode == SCROLL_MODE_FINITE) { changeScrolling(false); } } /** * Initiate LoopBar with RecyclerView adapter * * @param inputAdapter Instance of {@link RecyclerView.Adapter} */ public void setCategoriesAdapter(@NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> inputAdapter) { mInputAdapter = inputAdapter; mOuterAdapter = new ChangeScrollModeAdapter(inputAdapter); boolean hasItems = inputAdapter.getItemCount() > 0; if (hasItems) { IOperationItem firstItem = mOuterAdapter.getItem(0); firstItem.setVisible(false); } validateScrollMode(); mOuterAdapter.setIsIndeterminate(mInfinite); mOuterAdapter.setListener(this); mOuterAdapter.setOrientation(mOrientationState.getOrientation()); mOuterAdapter.setSelectedGravity(mSelectionGravity); mRvCategories.setAdapter(mOuterAdapter); mSelectorHolder = (ChangeScrollModeAdapter.ChangeScrollModeHolder) mOuterAdapter .createViewHolder(mRvCategories, ChangeScrollModeAdapter.VIEW_TYPE_CHANGE_SCROLL_MODE); // Set first item to selectionView if (hasItems) { mSelectorHolder.bindItemWildcardHelper(inputAdapter, 0); } mSelectorHolder.itemView.setBackgroundColor(mColorCodeSelectionView); mFlContainerSelected.addView(mSelectorHolder.itemView); mOrientationState.initSelectionContainer(mFlContainerSelected); FrameLayout.LayoutParams layoutParams = (LayoutParams) mSelectorHolder.itemView.getLayoutParams(); layoutParams.gravity = Gravity.CENTER; } /** * Initiate LoopBar with menu * * @param menuRes id for inflating {@link Menu} */ public void setCategoriesAdapterFromMenu(@MenuRes int menuRes) { Menu menu = new MenuBuilder(getContext()); new MenuInflater(getContext()).inflate(menuRes, menu); setCategoriesAdapterFromMenu(menu); } /** * Initiate LoopBar with menu * * @param menu Instance of {@link Menu} */ public void setCategoriesAdapterFromMenu(@NonNull Menu menu) { setCategoriesAdapter(new SimpleCategoriesMenuAdapter(menu)); } /** * You can setup {@code {@link LoopBarView#mOuterAdapter }} through {@link ViewPager} adapter. * Your {@link ViewPager} adapter must implement {@link ILoopBarPagerAdapter} otherwise - the icons will not be shown * * @param viewPager - viewPager, which must have {@link ILoopBarPagerAdapter} */ public void setupWithViewPager(@NonNull ViewPager viewPager) { PagerAdapter pagerAdapter = viewPager.getAdapter(); List<ICategoryItem> categoryItems = new ArrayList<>(pagerAdapter.getCount()); ILoopBarPagerAdapter loopBarPagerAdapter = pagerAdapter instanceof ILoopBarPagerAdapter ? (ILoopBarPagerAdapter) pagerAdapter : null; for (int i = 0, size = pagerAdapter.getCount(); i < size; i++) { categoryItems.add(new CategoryItem( loopBarPagerAdapter != null ? loopBarPagerAdapter.getPageDrawable(i) : null, String.valueOf(pagerAdapter.getPageTitle(i)) )); } setCategoriesAdapter(new SimpleCategoriesAdapter(categoryItems)); } /** * Add item click listener to this view * * @param itemClickListener Instance of {@link OnItemClickListener} * @return always true. */ @SuppressWarnings("unused") public boolean addOnItemClickListener(OnItemClickListener itemClickListener) { return mClickListeners.add(itemClickListener); } /** * Remove item click listener from this view * * @param itemClickListener Instance of {@link OnItemClickListener} * @return true if this {@code List} was modified by this operation, false * otherwise. */ @SuppressWarnings("unused") public boolean removeOnItemClickListener(OnItemClickListener itemClickListener) { return mClickListeners.remove(itemClickListener); } private void notifyItemClickListeners(int normalizedPosition) { for (OnItemClickListener itemClickListener : mClickListeners) { itemClickListener.onItemClicked(normalizedPosition); } } /** * Returns RecyclerView wrapped inside of view for control animations * Don't use it for changing adapter inside. * Use {@link #setCategoriesAdapter(RecyclerView.Adapter)} instead * * @return instance of {@link RecyclerView} * @deprecated use {@link #setItemAnimator(RecyclerView.ItemAnimator)}, * {@link #isAnimating()}, * {@link #addItemDecoration(RecyclerView.ItemDecoration)}, * {@link #addItemDecoration(RecyclerView.ItemDecoration, int)}, * {@link #removeItemDecoration(RecyclerView.ItemDecoration)}, * {@link #invalidateItemDecorations()}, * {@link #addOnScrollListener(RecyclerView.OnScrollListener)}, * {@link #removeOnScrollListener(RecyclerView.OnScrollListener)} * {@link #clearOnScrollListeners()} instead */ @Deprecated public RecyclerView getWrappedRecyclerView() { return mRvCategories; } private RecyclerView getRvCategories() { return mRvCategories; } /** * Sets the {@link RecyclerView.ItemAnimator} that will handle animations involving changes * to the items in wrapped RecyclerView. By default, RecyclerView instantiates and * uses an instance of {@link androidx.recyclerview.widget.DefaultItemAnimator}. Whether item animations are * enabled for the RecyclerView depends on the ItemAnimator and whether * the LayoutManager {@link RecyclerView.LayoutManager#supportsPredictiveItemAnimations() * supports item animations}. * * @param animator The ItemAnimator being set. If null, no animations will occur * when changes occur to the items in this RecyclerView. */ @SuppressWarnings("unused") public final void setItemAnimator(RecyclerView.ItemAnimator animator) { getRvCategories().setItemAnimator(animator); } /** * Returns true if wrapped RecyclerView is currently running some animations. * <p> * If you want to be notified when animations are finished, use * {@link RecyclerView.ItemAnimator#isRunning(RecyclerView.ItemAnimator.ItemAnimatorFinishedListener)}. * * @return True if there are some item animations currently running or waiting to be started. */ @SuppressWarnings("unused") public final boolean isAnimating() { return getRvCategories().isAnimating(); } /** * Add an {@link RecyclerView.ItemDecoration} to wrapped RecyclerView. Item decorations can * affect both measurement and drawing of individual item views. * <p> * <p>Item decorations are ordered. Decorations placed earlier in the list will * be run/queried/drawn first for their effects on item views. Padding added to views * will be nested; a padding added by an earlier decoration will mean further * item decorations in the list will be asked to draw/pad within the previous decoration's * given area.</p> * * @param decor Decoration to add */ @SuppressWarnings("unused") public final void addItemDecoration(RecyclerView.ItemDecoration decor) { getRvCategories().addItemDecoration(decor); } /** * Add an {@link RecyclerView.ItemDecoration} to wrapped RecyclerView. Item decorations can * affect both measurement and drawing of individual item views. * <p> * <p>Item decorations are ordered. Decorations placed earlier in the list will * be run/queried/drawn first for their effects on item views. Padding added to views * will be nested; a padding added by an earlier decoration will mean further * item decorations in the list will be asked to draw/pad within the previous decoration's * given area.</p> * * @param decor Decoration to add * @param index Position in the decoration chain to insert this decoration at. If this value * is negative the decoration will be added at the end. */ @SuppressWarnings("unused") public final void addItemDecoration(RecyclerView.ItemDecoration decor, int index) { getRvCategories().addItemDecoration(decor, index); } /** * Remove an {@link RecyclerView.ItemDecoration} from wrapped RecyclerView. * <p> * <p>The given decoration will no longer impact the measurement and drawing of * item views.</p> * * @param decor Decoration to remove * @see #addItemDecoration(RecyclerView.ItemDecoration) */ @SuppressWarnings("unused") public final void removeItemDecoration(RecyclerView.ItemDecoration decor) { getRvCategories().removeItemDecoration(decor); } /** * Invalidates all ItemDecorations in wrapped RecyclerView. If RecyclerView has item decorations, calling this method * will trigger a {@link #requestLayout()} call. */ @SuppressWarnings("unused") public final void invalidateItemDecorations() { getRvCategories().invalidateItemDecorations(); } /** * Add a listener to wrapped RecyclerView that will be notified of any changes in scroll state or position. * <p> * <p>Components that add a listener should take care to remove it when finished. * Other components that take ownership of a view may call {@link #clearOnScrollListeners()} * to remove all attached listeners.</p> * * @param listener listener to set or null to clear */ @SuppressWarnings("unused") public final void addOnScrollListener(RecyclerView.OnScrollListener listener) { getRvCategories().addOnScrollListener(listener); } /** * Remove a listener from wrapped RecyclerView that was notified of any changes in scroll state or position. * * @param listener listener to set or null to clear */ @SuppressWarnings("unused") public final void removeOnScrollListener(RecyclerView.OnScrollListener listener) { getRvCategories().removeOnScrollListener(listener); } /** * Remove all secondary listener from wrapped RecyclerView that were notified of any changes in scroll state or position. */ @SuppressWarnings("unused") public final void clearOnScrollListeners() { getRvCategories().clearOnScrollListeners(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mOverlayPlaceholder = ((ViewGroup) getParent()).findViewById(mPlaceHolderId); } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); int adapterSize = ss.mAdapterSize; if (adapterSize > 0) { setCurrentItem(ss.mCurrentItemPosition); } setGravity(ss.mSelectionGravity); mScrollMode = ss.mScrollMode; changeScrolling(ss.mIsInfinite); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (!mSkipNextOnLayout) { if (mOverlaySize > 0 && mOverlayPlaceholder == null) { if (BuildConfig.DEBUG) { Log.e(TAG, "You have to add placeholder and set it id with #enls_placeHolderId parameter to use mOverlaySize"); } } mOrientationState.initPlaceHolderAndOverlay(mOverlayPlaceholder, mRvCategories, mFlContainerSelected, mOverlaySize); if (mRvCategories.getChildCount() > 0 && !mIndeterminateInitialized) { mIndeterminateInitialized = true; //scroll to middle of indeterminate recycler view on initialization and if user somehow scrolled to start or end mRvCategories.getViewTreeObserver() .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { boolean isFitOnScreen = mOrientationState .isItemsFitOnScreen(mRvCategories, mOuterAdapter.getWrappedItems().size()); if (mScrollMode == SCROLL_MODE_AUTO) { changeScrolling(!isFitOnScreen); } checkAndScroll(); updateCategoriesOffsetBySelector(!mInfinite); mRvCategories.addOnScrollListener(mIndeterminateOnScrollListener); if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) { mRvCategories.getViewTreeObserver().removeGlobalOnLayoutListener(this); } else { mRvCategories.getViewTreeObserver().removeOnGlobalLayoutListener(this); } } }); } mSkipNextOnLayout = true; } } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); int adapterSize = mInputAdapter != null ? mInputAdapter.getItemCount() : 0; return new SavedState(superState, mCurrentItemPosition, mSelectionGravity, mInfinite, mScrollMode, adapterSize); } private void startSelectedViewOutAnimation(final int position) { Animator animator = mSelectionOutAnimator; animator.setTarget(mSelectorHolder.itemView); animator.start(); animator.addListener(new AbstractAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { //replace selected view mSelectorHolder.bindItemWildcardHelper(mInputAdapter, position); startSelectedViewInAnimation(); } }); } private void startSelectedViewInAnimation() { Animator animator = mSelectionInAnimator; animator.setTarget(mSelectorHolder.itemView); animator.addListener(new AbstractAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); updateCategoriesOffsetBySelector(); } }); animator.start(); } private void updateCategoriesOffsetBySelector() { if (mSelectorHolder == null) { return; } int size; if (mOrientationState != null) { size = mOrientationState.getSize(mSelectorHolder.itemView) + mSelectionMargin; } else { size = 0; } updateCategoriesOffset(size); } private void updateCategoriesOffsetBySelector(boolean withRecyclerViewNotification) { if (mSelectorHolder == null) { return; } int size; if (mOrientationState != null) { size = mOrientationState.getSize(mSelectorHolder.itemView) + mOrientationState.getHeaderMargins(getContext()) + mSelectionMargin; } else { size = 0; } updateCategoriesOffset(size, withRecyclerViewNotification); } /** * Set selected item in endless view. OnItemSelected listeners won't be invoked * * @param currentItemPosition selected position */ @SuppressWarnings("unused") public void setCurrentItem(int currentItemPosition) { selectItem(currentItemPosition, false); } /** * Set selected item in endless view. * OnItemSelected listeners won't be invoked * * @param currentItemPosition selected position * @param isInvokeListeners should view notify OnItemSelected listeners about this selection */ @SuppressWarnings("unused") public void setCurrentItem(int currentItemPosition, boolean isInvokeListeners) { selectItem(currentItemPosition, isInvokeListeners); } /** * Select item by it's position * * @param position int value of item position to select * @param invokeListeners boolean value for invoking listeners */ @SuppressWarnings("unused") public void selectItem(int position, boolean invokeListeners) { IOperationItem item = mOuterAdapter.getItem(position); IOperationItem oldHidedItem = mOuterAdapter.getItem(mRealHidedPosition); int realPosition = mOuterAdapter.normalizePosition(position); //do nothing if position not changed if (realPosition == mCurrentItemPosition) { return; } int itemToShowAdapterPosition = position - realPosition + mRealHidedPosition; item.setVisible(false); startSelectedViewOutAnimation(position); mOuterAdapter.notifyRealItemChanged(position); mRealHidedPosition = realPosition; oldHidedItem.setVisible(true); mFlContainerSelected.requestLayout(); mOuterAdapter.notifyRealItemChanged(itemToShowAdapterPosition); mCurrentItemPosition = realPosition; if (invokeListeners) { notifyItemClickListeners(realPosition); } if (BuildConfig.DEBUG) { Log.i(TAG, "clicked on position =" + position); } } /** * Select item by it's position. Listeners will be invoked * * @param position int value of item position to select */ @Override public void onItemClicked(int position) { selectItem(position, true); } //orientation state factory method public IOrientationState getOrientationStateFromParam(int orientation) { switch (orientation) { case Orientation.ORIENTATION_HORIZONTAL_BOTTOM: return new OrientationStateHorizontalBottom(); case Orientation.ORIENTATION_HORIZONTAL_TOP: return new OrientationStateHorizontalTop(); case Orientation.ORIENTATION_VERTICAL_LEFT: return new OrientationStateVerticalLeft(); case Orientation.ORIENTATION_VERTICAL_RIGHT: return new OrientationStateVerticalRight(); default: return new OrientationStateHorizontalBottom(); } } private void checkAndScroll() { if (isInfinite() && (mLinearLayoutManager.findFirstVisibleItemPosition() == 0 || mLinearLayoutManager.findFirstVisibleItemPosition() == 1 || mLinearLayoutManager.findLastVisibleItemPosition() == Integer.MAX_VALUE)) { mLinearLayoutManager.scrollToPositionWithOffset(getPositionForScroll(), getScrollOffset()); } } private int getPositionForScroll() { if (mOuterAdapter == null || mOuterAdapter.getWrappedItems().isEmpty()) { return Integer.MAX_VALUE / 2; } int size = mOuterAdapter.getWrappedItems().size(); int count = (Integer.MAX_VALUE / 2) / size; return count * size; } private int getScrollOffset() { int headerOffset; if (mSelectionGravity == SELECTION_GRAVITY_START && mOrientationState != null && mSelectorHolder != null) { headerOffset = mOrientationState.getSize(mSelectorHolder.itemView) + mOrientationState.getHeaderMargins(getContext()) + mSelectionMargin; } else { headerOffset = 0; } return headerOffset; } private void updateCategoriesOffset(int size) { updateCategoriesOffset(size, true); } private void updateCategoriesOffset(int size, boolean withAdapterNotification) { if (mOuterAdapter != null) { mOuterAdapter.setHeaderSize(size); if (withAdapterNotification) { if (mSelectionGravity == SELECTION_GRAVITY_START) { mOuterAdapter.notifyItemChanged(0); } else { mOuterAdapter.notifyItemChanged(mOuterAdapter.getItemCount() - 1); } } } } /** * Interface with pre-defined limit of gravity constants for selector * * @see #SELECTION_GRAVITY_START * @see #SELECTION_GRAVITY_END */ @IntDef({SELECTION_GRAVITY_START, SELECTION_GRAVITY_END}) @Retention(RetentionPolicy.SOURCE) public @interface GravityAttr { } /** * Interface with pre-defined limit of constants for scroll mode * * @see #SCROLL_MODE_AUTO * @see #SCROLL_MODE_INFINITE * @see #SCROLL_MODE_FINITE */ @IntDef({SCROLL_MODE_AUTO, SCROLL_MODE_INFINITE, SCROLL_MODE_FINITE}) @Retention(RetentionPolicy.SOURCE) public @interface ScrollAttr { } /** * Encapsulate logic for LoopBar saving and restore state * * @see #onSaveInstanceState() * @see #onRestoreInstanceState(Parcelable) */ public static class SavedState extends BaseSavedState implements Parcelable { public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; private int mCurrentItemPosition; @GravityAttr private int mSelectionGravity; @ScrollAttr private int mScrollMode; private boolean mIsInfinite; private int mAdapterSize; public SavedState(Parcelable superState) { super(superState); } @SuppressWarnings("unused") private SavedState(Parcel parcel) { super(parcel); mCurrentItemPosition = parcel.readInt(); @GravityAttr int gravity = parcel.readInt(); mSelectionGravity = gravity; @ScrollAttr int scrollMode = parcel.readInt(); mScrollMode = scrollMode; boolean[] booleanValues = new boolean[1]; parcel.readBooleanArray(booleanValues); mIsInfinite = booleanValues[0]; mAdapterSize = parcel.readInt(); } SavedState(Parcelable superState, int currentItemPosition, int selectionGravity, boolean isInfinite, int scrollMode, int adapterSize) { super(superState); mCurrentItemPosition = currentItemPosition; mSelectionGravity = selectionGravity; mIsInfinite = isInfinite; mScrollMode = scrollMode; mAdapterSize = adapterSize; } public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int flags) { super.writeToParcel(parcel, flags); parcel.writeInt(mCurrentItemPosition); parcel.writeInt(mSelectionGravity); parcel.writeInt(mScrollMode); parcel.writeBooleanArray(new boolean[]{mIsInfinite}); parcel.writeInt(mAdapterSize); } } private static class IndeterminateOnScrollListener extends RecyclerView.OnScrollListener { private final WeakReference<LoopBarView> loopBarViewWeakReference; private IndeterminateOnScrollListener(LoopBarView loopBarView) { loopBarViewWeakReference = new WeakReference<>(loopBarView); } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); LoopBarView loopBarView = loopBarViewWeakReference.get(); if (loopBarView != null) { loopBarView.checkAndScroll(); } } } }