From 70caf141eb06c1b2cdd139a48afd6a0e5b1f6d0e Mon Sep 17 00:00:00 2001 From: Ali Hn Date: Sun, 16 Feb 2020 10:58:36 +0330 Subject: [PATCH 1/3] Slider Pager completely has a independent implementation. Infinite adapter wrapper added. api improvements. auto cycle bugs fixed. --- app/src/main/AndroidManifest.xml | 1 + .../smarteist/imageslider/MainActivity.java | 8 +- app/src/main/res/layout/activity_main.xml | 11 +- autoimageslider/build.gradle | 2 +- .../autoimageslider/CircularSliderHandle.java | 69 - .../IndicatorView/PageIndicatorView.java | 49 +- .../InfiniteAdapter/InfinitePagerAdapter.java | 106 + .../autoimageslider/SliderPager.java | 3171 ++++++++++++++++- .../smarteist/autoimageslider/SliderView.java | 195 +- .../autoimageslider/SliderViewAdapter.java | 22 +- .../AntiClockSpinTransformation.java | 4 +- .../Clock_SpinTransformation.java | 4 +- .../CubeInDepthTransformation.java | 4 +- .../CubeInRotationTransformation.java | 4 +- .../CubeInScalingTransformation.java | 5 +- .../CubeOutDepthTransformation.java | 4 +- .../CubeOutRotationTransformation.java | 4 +- .../CubeOutScalingTransformation.java | 4 +- .../Transformations/DepthTransformation.java | 4 +- .../Transformations/FadeTransformation.java | 4 +- .../Transformations/FanTransformation.java | 4 +- .../FidgetSpinTransformation.java | 4 +- .../Transformations/GateTransformation.java | 4 +- .../Transformations/HingeTransformation.java | 4 +- .../HorizontalFlipTransformation.java | 4 +- .../Transformations/PopTransformation.java | 4 +- .../Transformations/SimpleTransformation.java | 4 +- .../SpinnerTransformation.java | 4 +- .../Transformations/TossTransformation.java | 4 +- .../VerticalFlipTransformation.java | 4 +- .../VerticalShutTransformation.java | 4 +- .../ZoomOutTransformation.java | 4 +- 32 files changed, 3483 insertions(+), 240 deletions(-) delete mode 100644 autoimageslider/src/main/java/com/smarteist/autoimageslider/CircularSliderHandle.java create mode 100644 autoimageslider/src/main/java/com/smarteist/autoimageslider/InfiniteAdapter/InfinitePagerAdapter.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 559599a..ebc2704 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.smarteist.imageslider"> + + app:sliderScrollTimeInSec="1" /> @@ -42,19 +39,19 @@ android:gravity="center" android:orientation="vertical"> - - - lastPosition) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/InfiniteAdapter/InfinitePagerAdapter.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/InfiniteAdapter/InfinitePagerAdapter.java new file mode 100644 index 0000000..7823f44 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/InfiniteAdapter/InfinitePagerAdapter.java @@ -0,0 +1,106 @@ +package com.smarteist.autoimageslider.InfiniteAdapter; + +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.viewpager.widget.PagerAdapter; + +import com.smarteist.autoimageslider.SliderViewAdapter; + + +public class InfinitePagerAdapter extends PagerAdapter { + + public static final int INFINITE_SCROLL_LIMIT = 40000; + private static final String TAG = "InfinitePagerAdapter"; + + private SliderViewAdapter adapter; + private int virtualPosition; + + public InfinitePagerAdapter(SliderViewAdapter adapter) { + this.adapter = adapter; + } + + public PagerAdapter getRealAdapter() { + return this.adapter; + } + + @Override + public int getCount() { + if (getRealAdapter().getCount() < 1) { + return 0; + } + // warning: infinite scroller actually is not infinite! + // very big number will be cause memory problems. + return getRealCount() * INFINITE_SCROLL_LIMIT; + } + + /** + * @return the {@link #getCount()} result of the wrapped adapter + */ + public int getRealCount() { + return adapter.getCount(); + } + + @NonNull + @Override + public Object instantiateItem(ViewGroup container, int position) { + if (getRealCount() < 1) { + return null; + } + virtualPosition = position % getRealCount(); + + //Log.i(TAG, "instantiateItem: real position: " + position); + //Log.i(TAG, "instantiateItem: virtual position: " + virtualPosition); + + // only expose virtual position to the inner adapter + return adapter.instantiateItem(container, virtualPosition); + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + if (getRealCount() < 1) { + return; + } + int virtualPosition = position % getRealCount(); + //Log.i(TAG, "destroyItem: real position: " + position); + //Log.i(TAG, "destroyItem: virtual position: " + virtualPosition); + + // only expose virtual position to the inner adapter + adapter.destroyItem(container, virtualPosition, object); + } + + /* + * Delegate rest of methods directly to the inner adapter. + */ + @Override + public void finishUpdate(ViewGroup container) { + adapter.finishUpdate(container); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return adapter.isViewFromObject(view, object); + } + + @Override + public void restoreState(Parcelable bundle, ClassLoader classLoader) { + adapter.restoreState(bundle, classLoader); + } + + @Override + public Parcelable saveState() { + return adapter.saveState(); + } + + @Override + public void startUpdate(ViewGroup container) { + adapter.startUpdate(container); + } + + public int getVirtualPosition() { + return virtualPosition; + } + +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderPager.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderPager.java index d836163..5610a99 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderPager.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderPager.java @@ -1,46 +1,3109 @@ package com.smarteist.autoimageslider; +import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; import android.util.AttributeSet; +import android.util.Log; +import android.view.FocusFinder; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; +import android.widget.EdgeEffect; import android.widget.Scroller; +import androidx.annotation.CallSuper; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.Px; +import androidx.core.content.ContextCompat; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.accessibility.AccessibilityEventCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.customview.view.AbsSavedState; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.PagerTitleStrip; +import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.InfiniteAdapter.InfinitePagerAdapter; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class SliderPager extends ViewGroup { + + public static final int DEFAULT_SCROLL_DURATION = 250; + private static final String TAG = "SliderPager"; + private static final boolean DEBUG = false; + private static final boolean USE_CACHE = false; + private static final int DEFAULT_OFFSCREEN_PAGES = 1; + private static final int MAX_SETTLE_DURATION = 600; // ms + private static final int MIN_DISTANCE_FOR_FLING = 25; // dips + private static final int DEFAULT_GUTTER_SIZE = 16; // dips + private static final int MIN_FLING_VELOCITY = 400; // dips + + + static final int[] LAYOUT_ATTRS = new int[]{ + android.R.attr.layout_gravity + }; + + /** + * Used to track what the expected number of items in the adapter should be. + * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. + */ + private int mExpectedAdapterCount; + + static class ItemInfo { + Object object; + int position; + boolean scrolling; + float widthFactor; + float offset; + } + + private static final Comparator COMPARATOR = new Comparator() { + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + return lhs.position - rhs.position; + } + }; + + private static final Interpolator sInterpolator = new Interpolator() { + @Override + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final ArrayList mItems = new ArrayList(); + private final ItemInfo mTempItem = new ItemInfo(); + + private final Rect mTempRect = new Rect(); + + PagerAdapter mAdapter; + int mCurItem; // Index of currently displayed page. + private int mRestoredCurItem = -1; + private Parcelable mRestoredAdapterState = null; + private ClassLoader mRestoredClassLoader = null; + + private Scroller mScroller; + private boolean mIsScrollStarted; + + private PagerObserver mObserver; + + private int mPageMargin; + private Drawable mMarginDrawable; + private int mTopPageBounds; + private int mBottomPageBounds; + + // Offsets of the first and last items, if known. + // Set during population, used to determine if we are at the beginning + // or end of the pager data set during touch scrolling. + private float mFirstOffset = -Float.MAX_VALUE; + private float mLastOffset = Float.MAX_VALUE; + + private int mChildWidthMeasureSpec; + private int mChildHeightMeasureSpec; + private boolean mInLayout; + + private boolean mScrollingCacheEnabled; + + private boolean mPopulatePending; + private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; + + private boolean mIsBeingDragged; + private boolean mIsUnableToDrag; + private int mDefaultGutterSize; + private int mGutterSize; + private int mTouchSlop; + /** + * Position of the last motion event. + */ + private float mLastMotionX; + private float mLastMotionY; + private float mInitialMotionX; + private float mInitialMotionY; + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + private int mMinimumVelocity; + private int mMaximumVelocity; + private int mFlingDistance; + private int mCloseEnough; + + // If the pager is at least this close to its final position, complete the scroll + // on touch down and let the user interact with the content inside instead of + // "catching" the flinging pager. + private static final int CLOSE_ENOUGH = 2; // dp + + private boolean mFakeDragging; + private long mFakeDragBeginTime; + + private EdgeEffect mLeftEdge; + private EdgeEffect mRightEdge; + + private boolean mFirstLayout = true; + private boolean mNeedCalculatePageOffsets = false; + private boolean mCalledSuper; + private int mDecorChildCount; + + private List mOnPageChangeListeners; + private OnPageChangeListener mOnPageChangeListener; + private OnPageChangeListener mInternalPageChangeListener; + private List mAdapterChangeListeners; + private PageTransformer mPageTransformer; + private int mPageTransformerLayerType; + + private static final int DRAW_ORDER_DEFAULT = 0; + private static final int DRAW_ORDER_FORWARD = 1; + private static final int DRAW_ORDER_REVERSE = 2; + private int mDrawingOrder; + private ArrayList mDrawingOrderedChildren; + private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); + + /** + * Indicates that the pager is in an idle, settled state. The current page + * is fully in view and no animation is in progress. + */ + public static final int SCROLL_STATE_IDLE = 0; + + /** + * Indicates that the pager is currently being dragged by the user. + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * Indicates that the pager is in the process of settling to a final position. + */ + public static final int SCROLL_STATE_SETTLING = 2; + + private final Runnable mEndScrollRunnable = new Runnable() { + @Override + public void run() { + setScrollState(SCROLL_STATE_IDLE); + populate(); + } + }; + + private int mScrollState = SCROLL_STATE_IDLE; + + /** + * Callback interface for responding to changing state of the selected page. + */ + public interface OnPageChangeListener { + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param positionOffset Value from [0, 1) indicating the offset from the page at position. + * @param positionOffsetPixels Value in pixels indicating the offset from position. + */ + void onPageScrolled(int position, float positionOffset, @Px int positionOffsetPixels); + + /** + * This method will be invoked when a new page becomes selected. Animation is not + * necessarily complete. + * + * @param position Position index of the new selected page. + */ + void onPageSelected(int position); + + /** + * Called when the scroll state changes. Useful for discovering when the user + * begins dragging, when the pager is automatically settling to the current page, + * or when it is fully stopped/idle. + * + * @param state The new scroll state. + * @see SliderPager#SCROLL_STATE_IDLE + * @see SliderPager#SCROLL_STATE_DRAGGING + * @see SliderPager#SCROLL_STATE_SETTLING + */ + void onPageScrollStateChanged(int state); + } + + /** + * Simple implementation of the {@link OnPageChangeListener} interface with stub + * implementations of each method. Extend this if you do not intend to override + * every method of {@link OnPageChangeListener}. + */ + public static class SimpleOnPageChangeListener implements OnPageChangeListener { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // This space for rent + } + + @Override + public void onPageSelected(int position) { + // This space for rent + } + + @Override + public void onPageScrollStateChanged(int state) { + // This space for rent + } + } + + /** + * A PageTransformer is invoked whenever a visible/attached page is scrolled. + * This offers an opportunity for the application to apply a custom transformation + * to the page views using animation properties. + * + *

As property animation is only supported as of Android 3.0 and forward, + * setting a PageTransformer on a SliderPager on earlier platform versions will + * be ignored.

+ */ + public interface PageTransformer { + /** + * Apply a property transformation to the given page. + * + * @param page Apply the transformation to this page + * @param position Position of page relative to the current front-and-center + * position of the pager. 0 is front and center. 1 is one full + * page position to the right, and -1 is one page position to the left. + */ + void transformPage(@NonNull View page, float position); + } + + /** + * Callback interface for responding to adapter changes. + */ + public interface OnAdapterChangeListener { + /** + * Called when the adapter for the given view pager has changed. + * + * @param viewPager SliderPager where the adapter change has happened + * @param oldAdapter the previously set adapter + * @param newAdapter the newly set adapter + */ + void onAdapterChanged(@NonNull SliderPager viewPager, + @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter); + } + + /** + * Annotation which allows marking of views to be decoration views when added to a view + * pager. + * + *

Views marked with this annotation can be added to the view pager with a layout resource. + * An example being {@link PagerTitleStrip}.

+ * + *

You can also control whether a view is a decor view but setting + * {@link LayoutParams#isDecor} on the child's layout params.

+ */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Inherited + public @interface DecorView { + } + + public SliderPager(@NonNull Context context) { + super(context); + initSliderPager(); + } + + public SliderPager(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initSliderPager(); + } + + void initSliderPager() { + setWillNotDraw(false); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setFocusable(true); + final Context context = getContext(); + mScroller = new Scroller(context, sInterpolator); + final ViewConfiguration configuration = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + + mTouchSlop = configuration.getScaledPagingTouchSlop(); + mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mLeftEdge = new EdgeEffect(context); + mRightEdge = new EdgeEffect(context); + + mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); + mCloseEnough = (int) (CLOSE_ENOUGH * density); + mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); + + ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); + + if (ViewCompat.getImportantForAccessibility(this) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + ViewCompat.setOnApplyWindowInsetsListener(this, + new androidx.core.view.OnApplyWindowInsetsListener() { + private final Rect mTempRect = new Rect(); + + @Override + public WindowInsetsCompat onApplyWindowInsets(final View v, + final WindowInsetsCompat originalInsets) { + // First let the SliderPager itself try and consume them... + final WindowInsetsCompat applied = + ViewCompat.onApplyWindowInsets(v, originalInsets); + if (applied.isConsumed()) { + // If the SliderPager consumed all insets, return now + return applied; + } + + // Now we'll manually dispatch the insets to our children. Since SliderPager + // children are always full-height, we do not want to use the standard + // ViewGroup dispatchApplyWindowInsets since if child 0 consumes them, + // the rest of the children will not receive any insets. To workaround this + // we manually dispatch the applied insets, not allowing children to + // consume them from each other. We do however keep track of any insets + // which are consumed, returning the union of our children's consumption + final Rect res = mTempRect; + res.left = applied.getSystemWindowInsetLeft(); + res.top = applied.getSystemWindowInsetTop(); + res.right = applied.getSystemWindowInsetRight(); + res.bottom = applied.getSystemWindowInsetBottom(); + + for (int i = 0, count = getChildCount(); i < count; i++) { + final WindowInsetsCompat childInsets = ViewCompat + .dispatchApplyWindowInsets(getChildAt(i), applied); + // Now keep track of any consumed by tracking each dimension's min + // value + res.left = Math.min(childInsets.getSystemWindowInsetLeft(), + res.left); + res.top = Math.min(childInsets.getSystemWindowInsetTop(), + res.top); + res.right = Math.min(childInsets.getSystemWindowInsetRight(), + res.right); + res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(), + res.bottom); + } + + // Now return a new WindowInsets, using the consumed window insets + return applied.replaceSystemWindowInsets( + res.left, res.top, res.right, res.bottom); + } + }); + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(mEndScrollRunnable); + // To be on the safe side, abort the scroller + if ((mScroller != null) && !mScroller.isFinished()) { + mScroller.abortAnimation(); + } + super.onDetachedFromWindow(); + } + + void setScrollState(int newState) { + if (mScrollState == newState) { + return; + } + + mScrollState = newState; + if (mPageTransformer != null) { + // PageTransformers can do complex things that benefit from hardware layers. + enableLayers(newState != SCROLL_STATE_IDLE); + } + dispatchOnScrollStateChanged(newState); + } + + /** + * Set a PagerAdapter that will supply views for this pager as needed. + * + * @param adapter Adapter to use + */ + public void setAdapter(PagerAdapter adapter) { + if (mAdapter != null) { + setAdapterViewPagerObserver(null); + mAdapter.startUpdate(this); + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + mAdapter.destroyItem(this, ii.position, ii.object); + } + mAdapter.finishUpdate(this); + mItems.clear(); + removeNonDecorViews(); + mCurItem = 0; + scrollTo(0, 0); + } + + final PagerAdapter oldAdapter = mAdapter; + mAdapter = adapter; + mExpectedAdapterCount = 0; + + if (mAdapter != null) { + if (mObserver == null) { + mObserver = new PagerObserver(); + } + setAdapterViewPagerObserver(mObserver); + mAdapter.registerDataSetObserver(mObserver); + mPopulatePending = false; + final boolean wasFirstLayout = mFirstLayout; + mFirstLayout = true; + mExpectedAdapterCount = mAdapter.getCount(); + if (mRestoredCurItem >= 0) { + mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); + setCurrentItemInternal(mRestoredCurItem, false, true); + mRestoredCurItem = -1; + mRestoredAdapterState = null; + mRestoredClassLoader = null; + } else if (!wasFirstLayout) { + populate(); + } else { + requestLayout(); + } + } + + // Dispatch the change to any listeners + if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) { + for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) { + mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter); + } + } + } + + /** + * This method calls setViewPagerObserver defined in PagerAdapter by using + * reflection. + * + * @param observer pager observer + */ + private void setAdapterViewPagerObserver(PagerObserver observer) { + try { + Method setViewPagerObserver = PagerAdapter.class.getDeclaredMethod("setViewPagerObserver", DataSetObserver.class); + setViewPagerObserver.setAccessible(true); + setViewPagerObserver.invoke(mAdapter, observer); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void removeNonDecorViews() { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + removeViewAt(i); + i--; + } + } + } + + /** + * Retrieve the current adapter supplying pages. + * + * @return The currently registered PagerAdapter + */ + @Nullable + public PagerAdapter getAdapter() { + return mAdapter; + } + + /** + * Add a listener that will be invoked whenever the adapter for this SliderPager changes. + * + * @param listener listener to add + */ + public void addOnAdapterChangeListener(@NonNull OnAdapterChangeListener listener) { + if (mAdapterChangeListeners == null) { + mAdapterChangeListeners = new ArrayList<>(); + } + mAdapterChangeListeners.add(listener); + } + + /** + * Remove a listener that was previously added via + * {@link #addOnAdapterChangeListener(OnAdapterChangeListener)}. + * + * @param listener listener to remove + */ + public void removeOnAdapterChangeListener(@NonNull OnAdapterChangeListener listener) { + if (mAdapterChangeListeners != null) { + mAdapterChangeListeners.remove(listener); + } + } + + private int getClientWidth() { + return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + } + + /** + * Set the currently selected page. If the SliderPager has already been through its first + * layout with its current adapter there will be a smooth animated transition between + * the current item and the specified item. + * + * @param item Item index to select + */ + public void setCurrentItem(int item) { + mPopulatePending = false; + setCurrentItemInternal(item, !mFirstLayout, false); + } + + /** + * Set the currently selected page. + * + * @param item Item index to select + * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately + */ + public void setCurrentItem(int item, boolean smoothScroll) { + mPopulatePending = false; + setCurrentItemInternal(item, smoothScroll, false); + } + + public int getCurrentItem() { + return mCurItem; + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { + setCurrentItemInternal(item, smoothScroll, always, 0); + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { + if (mAdapter == null || mAdapter.getCount() <= 0) { + setScrollingCacheEnabled(false); + return; + } + if (!always && mCurItem == item && mItems.size() != 0) { + setScrollingCacheEnabled(false); + return; + } + + if (item < 0) { + item = 0; + } else if (item >= mAdapter.getCount()) { + item = mAdapter.getCount() - 1; + } + final int pageLimit = mOffscreenPageLimit; + if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { + // We are doing a jump by more than one page. To avoid + // glitches, we want to keep all current pages in the view + // until the scroll ends. + for (int i = 0; i < mItems.size(); i++) { + mItems.get(i).scrolling = true; + } + } + final boolean dispatchSelected = mCurItem != item; + + if (mFirstLayout) { + // We don't have any idea how big we are yet and shouldn't have any pages either. + // Just set things up and let the pending layout handle things. + mCurItem = item; + triggerOnPageChangeEvent(item); + requestLayout(); + } else { + populate(item); + scrollToItem(item, smoothScroll, velocity, dispatchSelected); + } + } + + private void scrollToItem(int item, boolean smoothScroll, int velocity, + boolean dispatchSelected) { + final ItemInfo curInfo = infoForPosition(item); + int destX = 0; + if (curInfo != null) { + final int width = getClientWidth(); + destX = (int) (width * Math.max(mFirstOffset, + Math.min(curInfo.offset, mLastOffset))); + } + if (smoothScroll) { + smoothScrollTo(destX, 0, velocity); + if (dispatchSelected) { + triggerOnPageChangeEvent(item); + } + } else { + if (dispatchSelected) { + triggerOnPageChangeEvent(item); + } + completeScroll(false); + scrollTo(destX, 0); + pageScrolled(destX); + } + } + + /** + * Set a listener that will be invoked whenever the page changes or is incrementally + * scrolled. See {@link OnPageChangeListener}. + * + * @param listener Listener to set + * @deprecated Use {@link #addOnPageChangeListener(OnPageChangeListener)} + * and {@link #removeOnPageChangeListener(OnPageChangeListener)} instead. + */ + @Deprecated + public void setOnPageChangeListener(OnPageChangeListener listener) { + mOnPageChangeListener = listener; + } + + /** + * Add a listener that will be invoked whenever the page changes or is incrementally + * scrolled. See {@link OnPageChangeListener}. + * + *

Components that add a listener should take care to remove it when finished. + * Other components that take ownership of a view may call {@link #clearOnPageChangeListeners()} + * to remove all attached listeners.

+ * + * @param listener listener to add + */ + public void addOnPageChangeListener(@NonNull OnPageChangeListener listener) { + if (mOnPageChangeListeners == null) { + mOnPageChangeListeners = new ArrayList<>(); + } + mOnPageChangeListeners.add(listener); + } + + /** + * Remove a listener that was previously added via + * {@link #addOnPageChangeListener(OnPageChangeListener)}. + * + * @param listener listener to remove + */ + public void removeOnPageChangeListener(@NonNull OnPageChangeListener listener) { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.remove(listener); + } + } + + /** + * Remove all listeners that are notified of any changes in scroll state or position. + */ + public void clearOnPageChangeListeners() { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.clear(); + } + } + + /** + * Sets a {@link PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding behavior. + * + *

Note: By default, calling this method will cause contained pages to use + * {@link View#LAYER_TYPE_HARDWARE}. This layer type allows custom alpha transformations, + * but it will cause issues if any of your pages contain a {@link android.view.SurfaceView} + * and you have not called {@link android.view.SurfaceView#setZOrderOnTop(boolean)} to put that + * {@link android.view.SurfaceView} above your app content. To disable this behavior, call + * {@link #setPageTransformer(boolean, PageTransformer, int)} and pass + * {@link View#LAYER_TYPE_NONE} for {@code pageLayerType}.

+ * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + */ + public void setPageTransformer(boolean reverseDrawingOrder, + @Nullable PageTransformer transformer) { + setPageTransformer(reverseDrawingOrder, transformer, View.LAYER_TYPE_HARDWARE); + } + + /** + * Sets a {@link PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding behavior. + * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + * @param pageLayerType View layer type that should be used for SliderPager pages. It should be + * either {@link View#LAYER_TYPE_HARDWARE}, + * {@link View#LAYER_TYPE_SOFTWARE}, or + * {@link View#LAYER_TYPE_NONE}. + */ + public void setPageTransformer(boolean reverseDrawingOrder, + @Nullable PageTransformer transformer, int pageLayerType) { + final boolean hasTransformer = transformer != null; + final boolean needsPopulate = hasTransformer != (mPageTransformer != null); + mPageTransformer = transformer; + setChildrenDrawingOrderEnabled(hasTransformer); + if (hasTransformer) { + mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; + mPageTransformerLayerType = pageLayerType; + } else { + mDrawingOrder = DRAW_ORDER_DEFAULT; + } + if (needsPopulate) populate(); + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; + final int result = + ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; + return result; + } + + /** + * Set a separate OnPageChangeListener for internal use by the support library. + * + * @param listener Listener to set + * @return The old listener that was set, if any. + */ + OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { + OnPageChangeListener oldListener = mInternalPageChangeListener; + mInternalPageChangeListener = listener; + return oldListener; + } + + /** + * Returns the number of pages that will be retained to either side of the + * current page in the view hierarchy in an idle state. Defaults to 1. + * + * @return How many pages will be kept offscreen on either side + * @see #setOffscreenPageLimit(int) + */ + public int getOffscreenPageLimit() { + return mOffscreenPageLimit; + } + + /** + * Set the number of pages that should be retained to either side of the + * current page in the view hierarchy in an idle state. Pages beyond this + * limit will be recreated from the adapter when needed. + * + *

This is offered as an optimization. If you know in advance the number + * of pages you will need to support or have lazy-loading mechanisms in place + * on your pages, tweaking this setting can have benefits in perceived smoothness + * of paging animations and interaction. If you have a small number of pages (3-4) + * that you can keep active all at once, less time will be spent in layout for + * newly created view subtrees as the user pages back and forth.

+ * + *

You should keep this limit low, especially if your pages have complex layouts. + * This setting defaults to 1.

+ * + * @param limit How many pages will be kept offscreen in an idle state. + */ + public void setOffscreenPageLimit(int limit) { + if (limit < DEFAULT_OFFSCREEN_PAGES) { + Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + + DEFAULT_OFFSCREEN_PAGES); + limit = DEFAULT_OFFSCREEN_PAGES; + } + if (limit != mOffscreenPageLimit) { + mOffscreenPageLimit = limit; + populate(); + } + } + + /** + * Set the margin between pages. + * + * @param marginPixels Distance between adjacent pages in pixels + * @see #getPageMargin() + * @see #setPageMarginDrawable(Drawable) + * @see #setPageMarginDrawable(int) + */ + public void setPageMargin(int marginPixels) { + final int oldMargin = mPageMargin; + mPageMargin = marginPixels; + + final int width = getWidth(); + recomputeScrollPosition(width, width, marginPixels, oldMargin); + + requestLayout(); + } + + /** + * Return the margin between pages. + * + * @return The size of the margin in pixels + */ + public int getPageMargin() { + return mPageMargin; + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param d Drawable to display between pages + */ + public void setPageMarginDrawable(@Nullable Drawable d) { + mMarginDrawable = d; + if (d != null) refreshDrawableState(); + setWillNotDraw(d == null); + invalidate(); + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param resId Resource ID of a drawable to display between pages + */ + public void setPageMarginDrawable(@DrawableRes int resId) { + setPageMarginDrawable(ContextCompat.getDrawable(getContext(), resId)); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mMarginDrawable; + } + + public void setScrollDuration(int millis, Interpolator interpolator) { + if (interpolator != null) { + OwnScroller ownScroller = new OwnScroller(getContext(), millis, interpolator); + mScroller = ownScroller; + } else { + OwnScroller ownScroller = new OwnScroller(getContext(), millis); + mScroller = ownScroller; + } + } + + public void setScrollDuration(int millis) { + setScrollDuration(millis, null); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + final Drawable d = mMarginDrawable; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * (float) Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + */ + void smoothScrollTo(int x, int y) { + smoothScrollTo(x, y, 0); + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) + */ + void smoothScrollTo(int x, int y, int velocity) { + if (getChildCount() == 0) { + // Nothing to do. + setScrollingCacheEnabled(false); + return; + } + + int sx; + boolean wasScrolling = (mScroller != null) && !mScroller.isFinished(); + if (wasScrolling) { + // We're in the middle of a previously initiated scrolling. Check to see + // whether that scrolling has actually started (if we always call getStartX + // we can get a stale value from the scroller if it hadn't yet had its first + // computeScrollOffset call) to decide what is the current scrolling position. + sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX(); + // And abort the current scrolling. + mScroller.abortAnimation(); + setScrollingCacheEnabled(false); + } else { + sx = getScrollX(); + } + int sy = getScrollY(); + int dx = x - sx; + int dy = y - sy; + if (dx == 0 && dy == 0) { + completeScroll(false); + populate(); + setScrollState(SCROLL_STATE_IDLE); + return; + } + + setScrollingCacheEnabled(true); + setScrollState(SCROLL_STATE_SETTLING); + + final int width = getClientWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); + final float distance = halfWidth + halfWidth + * distanceInfluenceForSnapDuration(distanceRatio); + + int duration; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float pageWidth = width * mAdapter.getPageWidth(mCurItem); + final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); + duration = (int) ((pageDelta + 1) * 100); + } + duration = Math.min(duration, MAX_SETTLE_DURATION); + + // Reset the "scroll started" flag. It will be flipped to true in all places + // where we call computeScrollOffset(). + mIsScrollStarted = false; + mScroller.startScroll(sx, sy, dx, dy, duration); + ViewCompat.postInvalidateOnAnimation(this); + } + + ItemInfo addNewItem(int position, int index) { + ItemInfo ii = new ItemInfo(); + ii.position = position; + ii.object = mAdapter.instantiateItem(this, position); + ii.widthFactor = mAdapter.getPageWidth(position); + if (index < 0 || index >= mItems.size()) { + mItems.add(ii); + } else { + mItems.add(index, ii); + } + return ii; + } + + void dataSetChanged() { + // This method only gets called if our observer is attached, so mAdapter is non-null. + + final int adapterCount = mAdapter.getCount(); + mExpectedAdapterCount = adapterCount; + boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 + && mItems.size() < adapterCount; + int newCurrItem = mCurItem; + + boolean isUpdating = false; + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + final int newPos = mAdapter.getItemPosition(ii.object); + + if (newPos == PagerAdapter.POSITION_UNCHANGED) { + continue; + } + + if (newPos == PagerAdapter.POSITION_NONE) { + mItems.remove(i); + i--; + + if (!isUpdating) { + mAdapter.startUpdate(this); + isUpdating = true; + } + + mAdapter.destroyItem(this, ii.position, ii.object); + needPopulate = true; + + if (mCurItem == ii.position) { + // Keep the current item in the valid range + newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); + needPopulate = true; + } + continue; + } + + if (ii.position != newPos) { + if (ii.position == mCurItem) { + // Our current item changed position. Follow it. + newCurrItem = newPos; + } + + ii.position = newPos; + needPopulate = true; + } + } + + if (isUpdating) { + mAdapter.finishUpdate(this); + } + + Collections.sort(mItems, COMPARATOR); + + if (needPopulate) { + // Reset our known page widths; populate will recompute them. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + lp.widthFactor = 0.f; + } + } + + setCurrentItemInternal(newCurrItem, false, true); + requestLayout(); + } + } + + void populate() { + populate(mCurItem); + } + + void populate(int newCurrentItem) { + ItemInfo oldCurInfo = null; + if (mCurItem != newCurrentItem) { + oldCurInfo = infoForPosition(mCurItem); + mCurItem = newCurrentItem; + } + + if (mAdapter == null) { + sortChildDrawingOrder(); + return; + } + + // Bail now if we are waiting to populate. This is to hold off + // on creating views from the time the user releases their finger to + // fling to a new position until we have finished the scroll to + // that position, avoiding glitches from happening at that point. + if (mPopulatePending) { + if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); + sortChildDrawingOrder(); + return; + } + + // Also, don't populate until we are attached to a window. This is to + // avoid trying to populate before we have restored our view hierarchy + // state and conflicting with what is restored. + if (getWindowToken() == null) { + return; + } + + mAdapter.startUpdate(this); + + final int pageLimit = mOffscreenPageLimit; + final int startPos = Math.max(0, mCurItem - pageLimit); + final int N = mAdapter.getCount(); + final int endPos = Math.min(N - 1, mCurItem + pageLimit); + + if (N != mExpectedAdapterCount) { + String resName; + try { + resName = getResources().getResourceName(getId()); + } catch (Resources.NotFoundException e) { + resName = Integer.toHexString(getId()); + } + throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + + " contents without calling PagerAdapter#notifyDataSetChanged!" + + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + + " Pager id: " + resName + + " Pager class: " + getClass() + + " Problematic adapter: " + mAdapter.getClass()); + } + + // Locate the currently focused item or add it if needed. + int curIndex = -1; + ItemInfo curItem = null; + for (curIndex = 0; curIndex < mItems.size(); curIndex++) { + final ItemInfo ii = mItems.get(curIndex); + if (ii.position >= mCurItem) { + if (ii.position == mCurItem) curItem = ii; + break; + } + } + + if (curItem == null && N > 0) { + curItem = addNewItem(mCurItem, curIndex); + } + + // Fill 3x the available width or up to the number of offscreen + // pages requested to either side, whichever is larger. + // If we have no current item we have no work to do. + if (curItem != null) { + float extraWidthLeft = 0.f; + int itemIndex = curIndex - 1; + ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + final int clientWidth = getClientWidth(); + final float leftWidthNeeded = clientWidth <= 0 ? 0 : + 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; + for (int pos = mCurItem - 1; pos >= 0; pos--) { + if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + itemIndex--; + curIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthLeft += ii.widthFactor; + itemIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex + 1); + extraWidthLeft += ii.widthFactor; + curIndex++; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } + + float extraWidthRight = curItem.widthFactor; + itemIndex = curIndex + 1; + if (extraWidthRight < 2.f) { + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + final float rightWidthNeeded = clientWidth <= 0 ? 0 : + (float) getPaddingRight() / (float) clientWidth + 2.f; + for (int pos = mCurItem + 1; pos < N; pos++) { + if (extraWidthRight >= rightWidthNeeded && pos > endPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthRight += ii.widthFactor; + itemIndex++; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex); + itemIndex++; + extraWidthRight += ii.widthFactor; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } + } + + calculatePageOffsets(curItem, curIndex, oldCurInfo); + + mAdapter.setPrimaryItem(this, mCurItem, curItem.object); + } + + if (DEBUG) { + Log.i(TAG, "Current page list:"); + for (int i = 0; i < mItems.size(); i++) { + Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); + } + } + + mAdapter.finishUpdate(this); + + // Check width measurement of current pages and drawing sort order. + // Update LayoutParams as needed. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.childIndex = i; + if (!lp.isDecor && lp.widthFactor == 0.f) { + // 0 means requery the adapter for this, it doesn't have a valid width. + final ItemInfo ii = infoForChild(child); + if (ii != null) { + lp.widthFactor = ii.widthFactor; + lp.position = ii.position; + } + } + } + sortChildDrawingOrder(); + + if (hasFocus()) { + View currentFocused = findFocus(); + ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; + if (ii == null || ii.position != mCurItem) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(View.FOCUS_FORWARD)) { + break; + } + } + } + } + } + } + + private void sortChildDrawingOrder() { + if (mDrawingOrder != DRAW_ORDER_DEFAULT) { + if (mDrawingOrderedChildren == null) { + mDrawingOrderedChildren = new ArrayList(); + } else { + mDrawingOrderedChildren.clear(); + } + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + mDrawingOrderedChildren.add(child); + } + Collections.sort(mDrawingOrderedChildren, sPositionComparator); + } + } + + private void triggerOnPageChangeEvent(int position) { + for (OnPageChangeListener eachListener : mOnPageChangeListeners) { + if (eachListener != null) { + if (mAdapter instanceof InfinitePagerAdapter) { + InfinitePagerAdapter infiniteAdapter = (InfinitePagerAdapter) mAdapter; + int realCount = infiniteAdapter.getRealCount(); + if (realCount < 1) { + return; + } + int n = position % realCount; + eachListener.onPageSelected(n); + } else { + eachListener.onPageSelected(mAdapter.getCount()); + } + + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(position); + } + } + + + private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { + final int N = mAdapter.getCount(); + final int width = getClientWidth(); + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + // Fix up offsets for later layout. + if (oldCurInfo != null) { + final int oldCurPosition = oldCurInfo.position; + // Base offsets off of oldCurInfo. + if (oldCurPosition < curItem.position) { + int itemIndex = 0; + ItemInfo ii = null; + float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; + for (int pos = oldCurPosition + 1; + pos <= curItem.position && itemIndex < mItems.size(); pos++) { + ii = mItems.get(itemIndex); + while (pos > ii.position && itemIndex < mItems.size() - 1) { + itemIndex++; + ii = mItems.get(itemIndex); + } + while (pos < ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset += mAdapter.getPageWidth(pos) + marginOffset; + pos++; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + } else if (oldCurPosition > curItem.position) { + int itemIndex = mItems.size() - 1; + ItemInfo ii = null; + float offset = oldCurInfo.offset; + for (int pos = oldCurPosition - 1; + pos >= curItem.position && itemIndex >= 0; pos--) { + ii = mItems.get(itemIndex); + while (pos < ii.position && itemIndex > 0) { + itemIndex--; + ii = mItems.get(itemIndex); + } + while (pos > ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset -= mAdapter.getPageWidth(pos) + marginOffset; + pos--; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + } + } + } + + // Base all offsets off of curItem. + final int itemCount = mItems.size(); + float offset = curItem.offset; + int pos = curItem.position - 1; + mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; + mLastOffset = curItem.position == N - 1 + ? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; + // Previous pages + for (int i = curIndex - 1; i >= 0; i--, pos--) { + final ItemInfo ii = mItems.get(i); + while (pos > ii.position) { + offset -= mAdapter.getPageWidth(pos--) + marginOffset; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + if (ii.position == 0) mFirstOffset = offset; + } + offset = curItem.offset + curItem.widthFactor + marginOffset; + pos = curItem.position + 1; + // Next pages + for (int i = curIndex + 1; i < itemCount; i++, pos++) { + final ItemInfo ii = mItems.get(i); + while (pos < ii.position) { + offset += mAdapter.getPageWidth(pos++) + marginOffset; + } + if (ii.position == N - 1) { + mLastOffset = offset + ii.widthFactor - 1; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + + mNeedCalculatePageOffsets = false; + } + + /** + * This is the persistent state that is saved by SliderPager. Only needed + * if you are creating a sublass of SliderPager that must save its own + * state, in which case it should implement a subclass of this which + * contains that state. + */ + public static class SavedState extends AbsSavedState { + int position; + Parcelable adapterState; + ClassLoader loader; + + public SavedState(@NonNull Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(position); + out.writeParcelable(adapterState, flags); + } + + @Override + public String toString() { + return "FragmentPager.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " position=" + position + "}"; + } + + public static final Creator CREATOR = new ClassLoaderCreator() { + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + + SavedState(Parcel in, ClassLoader loader) { + super(in, loader); + if (loader == null) { + loader = getClass().getClassLoader(); + } + position = in.readInt(); + adapterState = in.readParcelable(loader); + this.loader = loader; + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.position = mCurItem; + if (mAdapter != null) { + ss.adapterState = mAdapter.saveState(); + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mAdapter != null) { + mAdapter.restoreState(ss.adapterState, ss.loader); + setCurrentItemInternal(ss.position, false, true); + } else { + mRestoredCurItem = ss.position; + mRestoredAdapterState = ss.adapterState; + mRestoredClassLoader = ss.loader; + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + final LayoutParams lp = (LayoutParams) params; + // Any views added via inflation should be classed as part of the decor + lp.isDecor |= isDecorView(child); + if (mInLayout) { + if (lp != null && lp.isDecor) { + throw new IllegalStateException("Cannot add pager decor view during layout"); + } + lp.needsMeasure = true; + addViewInLayout(child, index, params); + } else { + super.addView(child, index, params); + } + + if (USE_CACHE) { + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(mScrollingCacheEnabled); + } else { + child.setDrawingCacheEnabled(false); + } + } + } + + private static boolean isDecorView(@NonNull View view) { + Class clazz = view.getClass(); + return clazz.getAnnotation(DecorView.class) != null; + } + + @Override + public void removeView(View view) { + if (mInLayout) { + removeViewInLayout(view); + } else { + super.removeView(view); + } + } + + ItemInfo infoForChild(View child) { + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (mAdapter.isViewFromObject(child, ii.object)) { + return ii; + } + } + return null; + } + + ItemInfo infoForAnyChild(View child) { + ViewParent parent; + while ((parent = child.getParent()) != this) { + if (parent == null || !(parent instanceof View)) { + return null; + } + child = (View) parent; + } + return infoForChild(child); + } + + ItemInfo infoForPosition(int position) { + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.position == position) { + return ii; + } + } + return null; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // For simple implementation, our internal size is always 0. + // We depend on the container to specify the layout size of + // our view. We can't really know what it is since we will be + // adding and removing different arbitrary views and do not + // want the layout to change as this happens. + setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), + getDefaultSize(0, heightMeasureSpec)); + + final int measuredWidth = getMeasuredWidth(); + final int maxGutterSize = measuredWidth / 10; + mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); + + // Children are just made to fill our space. + int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight(); + int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); + + /* + * Make sure all children have been properly measured. Decor views first. + * Right now we cheat and make this less complicated by assuming decor + * views won't intersect. We will pin to edges based on gravity. + */ + int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp != null && lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + int widthMode = MeasureSpec.AT_MOST; + int heightMode = MeasureSpec.AT_MOST; + boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; + boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT; + + if (consumeVertical) { + widthMode = MeasureSpec.EXACTLY; + } else if (consumeHorizontal) { + heightMode = MeasureSpec.EXACTLY; + } + + int widthSize = childWidthSize; + int heightSize = childHeightSize; + if (lp.width != LayoutParams.WRAP_CONTENT) { + widthMode = MeasureSpec.EXACTLY; + if (lp.width != LayoutParams.MATCH_PARENT) { + widthSize = lp.width; + } + } + if (lp.height != LayoutParams.WRAP_CONTENT) { + heightMode = MeasureSpec.EXACTLY; + if (lp.height != LayoutParams.MATCH_PARENT) { + heightSize = lp.height; + } + } + final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); + final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); + child.measure(widthSpec, heightSpec); + + if (consumeVertical) { + childHeightSize -= child.getMeasuredHeight(); + } else if (consumeHorizontal) { + childWidthSize -= child.getMeasuredWidth(); + } + } + } + } + + mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); + mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); + + // Make sure we have created all fragments that we need to have shown. + mInLayout = true; + populate(); + mInLayout = false; + + // Page views next. + size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + if (DEBUG) { + Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec); + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp == null || !lp.isDecor) { + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); + child.measure(widthSpec, mChildHeightMeasureSpec); + } + } + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + // Make sure scroll position is set correctly. + if (w != oldw) { + recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); + } + } + + private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) { + if (oldWidth > 0 && !mItems.isEmpty()) { + if (!mScroller.isFinished()) { + mScroller.setFinalX(getCurrentItem() * getClientWidth()); + } else { + final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; + final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() + + oldMargin; + final int xpos = getScrollX(); + final float pageOffset = (float) xpos / oldWidthWithMargin; + final int newOffsetPixels = (int) (pageOffset * widthWithMargin); + + scrollTo(newOffsetPixels, getScrollY()); + } + } else { + final ItemInfo ii = infoForPosition(mCurItem); + final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; + final int scrollPos = + (int) (scrollOffset * (width - getPaddingLeft() - getPaddingRight())); + if (scrollPos != getScrollX()) { + completeScroll(false); + scrollTo(scrollPos, getScrollY()); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + int width = r - l; + int height = b - t; + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + final int scrollX = getScrollX(); + + int decorCount = 0; + + // First pass - decor views. We need to do this in two passes so that + // we have the proper offsets for non-decor views later. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + int childLeft = 0; + int childTop = 0; + if (lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getMeasuredWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + switch (vgrav) { + default: + childTop = paddingTop; + break; + case Gravity.TOP: + childTop = paddingTop; + paddingTop += child.getMeasuredHeight(); + break; + case Gravity.CENTER_VERTICAL: + childTop = Math.max((height - child.getMeasuredHeight()) / 2, + paddingTop); + break; + case Gravity.BOTTOM: + childTop = height - paddingBottom - child.getMeasuredHeight(); + paddingBottom += child.getMeasuredHeight(); + break; + } + childLeft += scrollX; + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + decorCount++; + } + } + } + + final int childWidth = width - paddingLeft - paddingRight; + // Page views. Do this once we have the right padding offsets from above. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + ItemInfo ii; + if (!lp.isDecor && (ii = infoForChild(child)) != null) { + int loff = (int) (childWidth * ii.offset); + int childLeft = paddingLeft + loff; + int childTop = paddingTop; + if (lp.needsMeasure) { + // This was added during layout and needs measurement. + // Do it now that we know what we're working with. + lp.needsMeasure = false; + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidth * lp.widthFactor), + MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec( + (int) (height - paddingTop - paddingBottom), + MeasureSpec.EXACTLY); + child.measure(widthSpec, heightSpec); + } + if (DEBUG) { + Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object + + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() + + "x" + child.getMeasuredHeight()); + } + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + } + } + } + mTopPageBounds = paddingTop; + mBottomPageBounds = height - paddingBottom; + mDecorChildCount = decorCount; + + if (mFirstLayout) { + scrollToItem(mCurItem, false, 0, false); + } + mFirstLayout = false; + } + + @Override + public void computeScroll() { + mIsScrollStarted = true; + if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (!pageScrolled(x)) { + mScroller.abortAnimation(); + scrollTo(0, y); + } + } + + // Keep on drawing until the animation has finished. + ViewCompat.postInvalidateOnAnimation(this); + return; + } + + // Done with scroll, clean up state. + completeScroll(true); + } + + private boolean pageScrolled(int xpos) { + if (mItems.size() == 0) { + if (mFirstLayout) { + // If we haven't been laid out yet, we probably just haven't been populated yet. + // Let's skip this call since it doesn't make sense in this state + return false; + } + mCalledSuper = false; + onPageScrolled(0, 0, 0); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return false; + } + final ItemInfo ii = infoForCurrentScrollPosition(); + final int width = getClientWidth(); + final int widthWithMargin = width + mPageMargin; + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) xpos / width) - ii.offset) + / (ii.widthFactor + marginOffset); + final int offsetPixels = (int) (pageOffset * widthWithMargin); + + mCalledSuper = false; + onPageScrolled(currentPage, pageOffset, offsetPixels); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return true; + } + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * If you override this method you must call through to the superclass implementation + * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled + * returns. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param offset Value from [0, 1) indicating the offset from the page at position. + * @param offsetPixels Value in pixels indicating the offset from position. + */ + @CallSuper + protected void onPageScrolled(int position, float offset, int offsetPixels) { + // Offset any decor views if needed - keep them on-screen at all times. + if (mDecorChildCount > 0) { + final int scrollX = getScrollX(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + final int width = getWidth(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) continue; + + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int childLeft = 0; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + childLeft += scrollX; + + final int childOffset = childLeft - child.getLeft(); + if (childOffset != 0) { + child.offsetLeftAndRight(childOffset); + } + } + } + + dispatchOnPageScrolled(position, offset, offsetPixels); + + if (mPageTransformer != null) { + final int scrollX = getScrollX(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.isDecor) continue; + final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); + mPageTransformer.transformPage(child, transformPos); + } + } + + mCalledSuper = true; + } + + private void dispatchOnPageScrolled(int position, float offset, int offsetPixels) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageScrolled(position, offset, offsetPixels); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + } + + private void dispatchOnPageSelected(int position) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(position); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageSelected(position); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(position); + } + } + + private void dispatchOnScrollStateChanged(int state) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrollStateChanged(state); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageScrollStateChanged(state); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrollStateChanged(state); + } + } + + private void completeScroll(boolean postEvents) { + boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; + if (needPopulate) { + // Done with scroll, no longer want to cache view drawing. + setScrollingCacheEnabled(false); + boolean wasScrolling = !mScroller.isFinished(); + if (wasScrolling) { + mScroller.abortAnimation(); + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (x != oldX) { + pageScrolled(x); + } + } + } + } + mPopulatePending = false; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.scrolling) { + needPopulate = true; + ii.scrolling = false; + } + } + if (needPopulate) { + if (postEvents) { + ViewCompat.postOnAnimation(this, mEndScrollRunnable); + } else { + mEndScrollRunnable.run(); + } + } + } -import androidx.viewpager.widget.ViewPager; + private boolean isGutterDrag(float x, float dx) { + return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0); + } -import java.lang.reflect.Field; + private void enableLayers(boolean enable) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final int layerType = enable + ? mPageTransformerLayerType : View.LAYER_TYPE_NONE; + getChildAt(i).setLayerType(layerType, null); + } + } -public class SliderPager extends ViewPager { + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ - public static final int DEFAULT_SCROLL_DURATION = 200; + final int action = ev.getAction() & MotionEvent.ACTION_MASK; - public SliderPager(Context context) { - super(context); + // Always take care of the touch gesture being complete. + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + // Release the drag. + if (DEBUG) Log.v(TAG, "Intercept done!"); + resetTouch(); + return false; + } + + // Nothing more to do here if we have decided whether or not we + // are dragging. + if (action != MotionEvent.ACTION_DOWN) { + if (mIsBeingDragged) { + if (DEBUG) Log.v(TAG, "Intercept returning true!"); + return true; + } + if (mIsUnableToDrag) { + if (DEBUG) Log.v(TAG, "Intercept returning false!"); + return false; + } + } + + switch (action) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = ev.findPointerIndex(activePointerId); + final float x = ev.getX(pointerIndex); + final float dx = x - mLastMotionX; + final float xDiff = Math.abs(dx); + final float y = ev.getY(pointerIndex); + final float yDiff = Math.abs(y - mInitialMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + + if (dx != 0 && !isGutterDrag(mLastMotionX, dx) + && canScroll(this, false, (int) dx, (int) x, (int) y)) { + // Nested view has scrollable area under this point. Let it be handled there. + mLastMotionX = x; + mLastMotionY = y; + mIsUnableToDrag = true; + return false; + } + if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + mLastMotionX = dx > 0 + ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollingCacheEnabled(true); + } else if (yDiff > mTouchSlop) { + // The finger has moved enough in the vertical + // direction to be counted as a drag... abort + // any attempt to drag horizontally, to work correctly + // with children that have scrolling containers. + if (DEBUG) Log.v(TAG, "Starting unable to drag!"); + mIsUnableToDrag = true; + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + if (performDrag(x)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + mIsUnableToDrag = false; + + mIsScrollStarted = true; + mScroller.computeScrollOffset(); + if (mScrollState == SCROLL_STATE_SETTLING + && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { + // Let the user 'catch' the pager as it animates. + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } else { + completeScroll(false); + mIsBeingDragged = false; + } + + if (DEBUG) { + Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + + " mIsBeingDragged=" + mIsBeingDragged + + "mIsUnableToDrag=" + mIsUnableToDrag); + } + break; + } + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; } - public SliderPager(Context context, AttributeSet attrs) { - super(context, attrs); + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mFakeDragging) { + // A fake drag is in progress already, ignore this real one + // but still eat the touch events. + // (It is likely that the user is multi-touching the screen.) + return true; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (mAdapter == null || mAdapter.getCount() == 0) { + // Nothing to present or scroll; nothing to touch. + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + boolean needsInvalidate = false; + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + + // Remember where the motion event started + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + break; + } + case MotionEvent.ACTION_MOVE: + if (!mIsBeingDragged) { + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + // A child has consumed some touch events and put us into an inconsistent + // state. + needsInvalidate = resetTouch(); + break; + } + final float x = ev.getX(pointerIndex); + final float xDiff = Math.abs(x - mLastMotionX); + final float y = ev.getY(pointerIndex); + final float yDiff = Math.abs(y - mLastMotionY); + if (DEBUG) { + Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + } + if (xDiff > mTouchSlop && xDiff > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollState(SCROLL_STATE_DRAGGING); + setScrollingCacheEnabled(true); + + // Disallow Parent Intercept, just in case + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + } + // Not else! Note that mIsBeingDragged can be set above. + if (mIsBeingDragged) { + // Scroll to follow the motion event + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(activePointerIndex); + needsInvalidate |= performDrag(x); + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) + / (ii.widthFactor + marginOffset); + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(activePointerIndex); + final int totalDelta = (int) (x - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + + needsInvalidate = resetTouch(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged) { + scrollToItem(mCurItem, true, 0, false); + needsInvalidate = resetTouch(); + } + break; + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + final float x = ev.getX(index); + mLastMotionX = x; + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)); + break; + } + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + return true; } - public void setScrollDuration(int millis, Interpolator interpolator) { - try { - Class viewpager = ViewPager.class; - Field scroller = viewpager.getDeclaredField("mScroller"); - scroller.setAccessible(true); - if (interpolator != null) { - OwnScroller ownScroller = new OwnScroller(getContext(), millis, interpolator); - scroller.set(this, ownScroller); + private boolean resetTouch() { + boolean needsInvalidate; + mActivePointerId = INVALID_POINTER; + endDrag(); + mLeftEdge.onRelease(); + mRightEdge.onRelease(); + needsInvalidate = mLeftEdge.isFinished() || mRightEdge.isFinished(); + return needsInvalidate; + } + + private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + private boolean performDrag(float x) { + boolean needsInvalidate = false; + + final float deltaX = mLastMotionX - x; + mLastMotionX = x; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX + deltaX; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + boolean leftAbsolute = true; + boolean rightAbsolute = true; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftAbsolute = false; + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightAbsolute = false; + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + if (leftAbsolute) { + float over = leftBound - scrollX; + mLeftEdge.onPull(Math.abs(over) / width); + needsInvalidate = true; + } + scrollX = leftBound; + } else if (scrollX > rightBound) { + if (rightAbsolute) { + float over = scrollX - rightBound; + mRightEdge.onPull(Math.abs(over) / width); + needsInvalidate = true; + } + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + return needsInvalidate; + } + + /** + * @return Info about the page at the current scroll position. + * This can be synthetic for a missing middle page; the 'object' field can be null. + */ + private ItemInfo infoForCurrentScrollPosition() { + final int width = getClientWidth(); + final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + int lastPos = -1; + float lastOffset = 0.f; + float lastWidth = 0.f; + boolean first = true; + + ItemInfo lastItem = null; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + float offset; + if (!first && ii.position != lastPos + 1) { + // Create a synthetic item for a missing page. + ii = mTempItem; + ii.offset = lastOffset + lastWidth + marginOffset; + ii.position = lastPos + 1; + ii.widthFactor = mAdapter.getPageWidth(ii.position); + i--; + } + offset = ii.offset; + + final float leftBound = offset; + final float rightBound = offset + ii.widthFactor + marginOffset; + if (first || scrollOffset >= leftBound) { + if (scrollOffset < rightBound || i == mItems.size() - 1) { + return ii; + } } else { - OwnScroller ownScroller = new OwnScroller(getContext(), millis); - scroller.set(this, ownScroller); + return lastItem; } - } catch (Exception e) { - e.printStackTrace(); + first = false; + lastPos = ii.position; + lastOffset = offset; + lastWidth = ii.widthFactor; + lastItem = ii; } + + return lastItem; } - public void setScrollDuration(int millis) { - setScrollDuration(millis, null); + private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { + int targetPage; + if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { + targetPage = velocity > 0 ? currentPage : currentPage + 1; + } else { + final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; + targetPage = currentPage + (int) (pageOffset + truncator); + } + + if (mItems.size() > 0) { + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + + // Only let the user target pages we have items for + targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); + } + + return targetPage; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + boolean needsInvalidate = false; + + final int overScrollMode = getOverScrollMode(); + if (overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS + && mAdapter != null && mAdapter.getCount() > 1)) { + if (!mLeftEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + final int width = getWidth(); + + canvas.rotate(270); + canvas.translate(-height + getPaddingTop(), mFirstOffset * width); + mLeftEdge.setSize(height, width); + needsInvalidate |= mLeftEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mRightEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.rotate(90); + canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); + mRightEdge.setSize(height, width); + needsInvalidate |= mRightEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + } else { + mLeftEdge.finish(); + mRightEdge.finish(); + } + + if (needsInvalidate) { + // Keep animating + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the margin drawable between pages if needed. + if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { + final int scrollX = getScrollX(); + final int width = getWidth(); + + final float marginOffset = (float) mPageMargin / width; + int itemIndex = 0; + ItemInfo ii = mItems.get(0); + float offset = ii.offset; + final int itemCount = mItems.size(); + final int firstPos = ii.position; + final int lastPos = mItems.get(itemCount - 1).position; + for (int pos = firstPos; pos < lastPos; pos++) { + while (pos > ii.position && itemIndex < itemCount) { + ii = mItems.get(++itemIndex); + } + + float drawAt; + if (pos == ii.position) { + drawAt = (ii.offset + ii.widthFactor) * width; + offset = ii.offset + ii.widthFactor + marginOffset; + } else { + float widthFactor = mAdapter.getPageWidth(pos); + drawAt = (offset + widthFactor) * width; + offset += widthFactor + marginOffset; + } + + if (drawAt + mPageMargin > scrollX) { + mMarginDrawable.setBounds(Math.round(drawAt), mTopPageBounds, + Math.round(drawAt + mPageMargin), mBottomPageBounds); + mMarginDrawable.draw(canvas); + } + + if (drawAt > scrollX + width) { + break; // No more visible, no sense in continuing + } + } + } + } + + /** + * Start a fake drag of the pager. + * + *

A fake drag can be useful if you want to synchronize the motion of the SliderPager + * with the touch scrolling of another view, while still letting the SliderPager + * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) + * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call + * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. + * + *

During a fake drag the SliderPager will ignore all touch events. If a real drag + * is already in progress, this method will return false. + * + * @return true if the fake drag began successfully, false if it could not be started. + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean beginFakeDrag() { + if (mIsBeingDragged) { + return false; + } + mFakeDragging = true; + setScrollState(SCROLL_STATE_DRAGGING); + mInitialMotionX = mLastMotionX = 0; + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + mFakeDragBeginTime = time; + return true; + } + + /** + * End a fake drag of the pager. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + */ + public void endFakeDrag() { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + if (mAdapter != null) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int totalDelta = (int) (mLastMotionX - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + } + endDrag(); + + mFakeDragging = false; + } + + /** + * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. + * + * @param xOffset Offset in pixels to drag by. + * @see #beginFakeDrag() + * @see #endFakeDrag() + */ + public void fakeDragBy(float xOffset) { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + if (mAdapter == null) { + return; + } + + mLastMotionX += xOffset; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX - xOffset; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + scrollX = leftBound; + } else if (scrollX > rightBound) { + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + // Synthesize an event for the VelocityTracker. + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, + mLastMotionX, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + } + + /** + * Returns true if a fake drag is in progress. + * + * @return true if currently in a fake drag, false otherwise. + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean isFakeDragging() { + return mFakeDragging; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = ev.getActionIndex(); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = ev.getX(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + mIsUnableToDrag = false; + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void setScrollingCacheEnabled(boolean enabled) { + if (mScrollingCacheEnabled != enabled) { + mScrollingCacheEnabled = enabled; + if (USE_CACHE) { + final int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(enabled); + } + } + } + } + } + + /** + * Check if this SliderPager can be scrolled horizontally in a certain direction. + * + * @param direction Negative to check scrolling left, positive to check scrolling right. + * @return Whether this SliderPager can be scrolled in the specified direction. It will always + * return false if the specified direction is 0. + */ + @Override + public boolean canScrollHorizontally(int direction) { + if (mAdapter == null) { + return false; + } + + final int width = getClientWidth(); + final int scrollX = getScrollX(); + if (direction < 0) { + return (scrollX > (int) (width * mFirstOffset)); + } else if (direction > 0) { + return (scrollX < (int) (width * mLastOffset)); + } else { + return false; + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() + && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() + && canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && v.canScrollHorizontally(-dx); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(@NonNull KeyEvent event) { + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + handled = pageLeft(); + } else { + handled = arrowScroll(FOCUS_LEFT); + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + handled = pageRight(); + } else { + handled = arrowScroll(FOCUS_RIGHT); + } + break; + case KeyEvent.KEYCODE_TAB: + if (event.hasNoModifiers()) { + handled = arrowScroll(FOCUS_FORWARD); + } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { + handled = arrowScroll(FOCUS_BACKWARD); + } + break; + } + } + return handled; + } + + /** + * Handle scrolling in response to a left or right arrow click. + * + * @param direction The direction corresponding to the arrow key that was pressed. It should be + * either {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT}. + * @return Whether the scrolling was handled successfully. + */ + public boolean arrowScroll(int direction) { + View currentFocused = findFocus(); + if (currentFocused == this) { + currentFocused = null; + } else if (currentFocused != null) { + boolean isChild = false; + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + if (parent == this) { + isChild = true; + break; + } + } + if (!isChild) { + // This would cause the focus search down below to fail in fun ways. + final StringBuilder sb = new StringBuilder(); + sb.append(currentFocused.getClass().getSimpleName()); + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + sb.append(" => ").append(parent.getClass().getSimpleName()); + } + Log.e(TAG, "arrowScroll tried to find focus based on non-child " + + "current focused view " + sb.toString()); + currentFocused = null; + } + } + + boolean handled = false; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, + direction); + if (nextFocused != null && nextFocused != currentFocused) { + if (direction == View.FOCUS_LEFT) { + // If there is nothing to the left, or this is causing us to + // jump to the right, then what we really want to do is page left. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft >= currLeft) { + handled = pageLeft(); + } else { + handled = nextFocused.requestFocus(); + } + } else if (direction == View.FOCUS_RIGHT) { + // If there is nothing to the right, or this is causing us to + // jump to the left, then what we really want to do is page right. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft <= currLeft) { + handled = pageRight(); + } else { + handled = nextFocused.requestFocus(); + } + } + } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { + // Trying to move left and nothing there; try to page. + handled = pageLeft(); + } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { + // Trying to move right and nothing there; try to page. + handled = pageRight(); + } + if (handled) { + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + return handled; + } + + private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { + if (outRect == null) { + outRect = new Rect(); + } + if (child == null) { + outRect.set(0, 0, 0, 0); + return outRect; + } + outRect.left = child.getLeft(); + outRect.right = child.getRight(); + outRect.top = child.getTop(); + outRect.bottom = child.getBottom(); + + ViewParent parent = child.getParent(); + while (parent instanceof ViewGroup && parent != this) { + final ViewGroup group = (ViewGroup) parent; + outRect.left += group.getLeft(); + outRect.right += group.getRight(); + outRect.top += group.getTop(); + outRect.bottom += group.getBottom(); + + parent = group.getParent(); + } + return outRect; + } + + boolean pageLeft() { + if (mCurItem > 0) { + setCurrentItem(mCurItem - 1, true); + return true; + } + return false; + } + + boolean pageRight() { + if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) { + setCurrentItem(mCurItem + 1, true); + return true; + } + return false; + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + public void addFocusables(ArrayList views, int direction, int focusableMode) { + final int focusableCount = views.size(); + + final int descendantFocusability = getDescendantFocusability(); + + if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addFocusables(views, direction, focusableMode); + } + } + } + } + + // we add ourselves (if focusable) in all cases except for when we are + // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is + // to avoid the focus search finding layouts when a more precise search + // among the focusable children would be more interesting. + if (descendantFocusability != FOCUS_AFTER_DESCENDANTS + || (focusableCount == views.size())) { // No focusable descendants + // Note that we can't call the superclass here, because it will + // add all views in. So we need to do the same thing View does. + if (!isFocusable()) { + return; + } + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE + && isInTouchMode() && !isFocusableInTouchMode()) { + return; + } + if (views != null) { + views.add(this); + } + } + } + + /** + * We only want the current page that is being shown to be touchable. + */ + @Override + public void addTouchables(ArrayList views) { + // Note that we don't call super.addTouchables(), which means that + // we don't call View.addTouchables(). This is okay because a SliderPager + // is itself not touchable. + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addTouchables(views); + } + } + } + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + int index; + int increment; + int end; + int count = getChildCount(); + if ((direction & FOCUS_FORWARD) != 0) { + index = 0; + increment = 1; + end = count; + } else { + index = count - 1; + increment = -1; + end = -1; + } + for (int i = index; i != end; i += increment) { + View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + } + return false; + } + + @SuppressLint("WrongConstant") + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // Dispatch scroll events from this SliderPager. + if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) { + return super.dispatchPopulateAccessibilityEvent(event); + } + + // Dispatch all other accessibility events from the current page. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + final ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem + && child.dispatchPopulateAccessibilityEvent(event)) { + return true; + } + } + } + + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return generateDefaultLayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + class MyAccessibilityDelegate extends AccessibilityDelegateCompat { + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(SliderPager.class.getName()); + event.setScrollable(canScroll()); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED && mAdapter != null) { + event.setItemCount(mAdapter.getCount()); + event.setFromIndex(mCurItem); + event.setToIndex(mCurItem); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(SliderPager.class.getName()); + info.setScrollable(canScroll()); + if (canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + if (canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + if (canScrollHorizontally(1)) { + setCurrentItem(mCurItem + 1); + return true; + } + } + return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + if (canScrollHorizontally(-1)) { + setCurrentItem(mCurItem - 1); + return true; + } + } + return false; + } + return false; + } + + private boolean canScroll() { + return (mAdapter != null) && (mAdapter.getCount() > 1); + } + } + + private class PagerObserver extends DataSetObserver { + PagerObserver() { + } + + @Override + public void onChanged() { + dataSetChanged(); + } + + @Override + public void onInvalidated() { + dataSetChanged(); + } } class OwnScroller extends Scroller { @@ -62,4 +3125,68 @@ public void startScroll(int startX, int startY, int dx, int dy, int duration) { super.startScroll(startX, startY, dx, dy, durationScrollMillis); } } + + /** + * Layout parameters that should be supplied for views added to a + * SliderPager. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * true if this view is a decoration on the pager itself and not + * a view supplied by the adapter. + */ + public boolean isDecor; + + /** + * Gravity setting for use on decor views only: + * Where to position the view page within the overall SliderPager + * container; constants are defined in {@link Gravity}. + */ + public int gravity; + + /** + * Width as a 0-1 multiplier of the measured pager width + */ + float widthFactor = 0.f; + + /** + * true if this view was added during layout and needs to be measured + * before being positioned. + */ + boolean needsMeasure; + + /** + * Adapter position this view is for if !isDecor + */ + int position; + + /** + * Current child index within the SliderPager that this view occupies + */ + int childIndex; + + public LayoutParams() { + super(MATCH_PARENT, MATCH_PARENT); + } + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + gravity = a.getInteger(0, Gravity.TOP); + a.recycle(); + } + } + + static class ViewPositionComparator implements Comparator { + @Override + public int compare(View lhs, View rhs) { + final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); + final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); + if (llp.isDecor != rlp.isDecor) { + return llp.isDecor ? 1 : -1; + } + return llp.position - rlp.position; + } + } } diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderView.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderView.java index 5e8cec0..d74308f 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderView.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderView.java @@ -1,5 +1,6 @@ package com.smarteist.autoimageslider; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; @@ -8,13 +9,13 @@ import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.animation.Interpolator; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.viewpager.widget.PagerAdapter; -import androidx.viewpager.widget.ViewPager; import com.smarteist.autoimageslider.IndicatorView.PageIndicatorView; import com.smarteist.autoimageslider.IndicatorView.animation.type.AnimationType; @@ -24,6 +25,7 @@ import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; import com.smarteist.autoimageslider.IndicatorView.draw.data.RtlMode; import com.smarteist.autoimageslider.IndicatorView.utils.DensityUtils; +import com.smarteist.autoimageslider.InfiniteAdapter.InfinitePagerAdapter; import com.smarteist.autoimageslider.Transformations.AntiClockSpinTransformation; import com.smarteist.autoimageslider.Transformations.Clock_SpinTransformation; import com.smarteist.autoimageslider.Transformations.CubeInDepthTransformation; @@ -49,7 +51,9 @@ import static com.smarteist.autoimageslider.IndicatorView.draw.controller.AttributeController.getRtlMode; -public class SliderView extends FrameLayout implements Runnable { +public class SliderView extends FrameLayout + implements Runnable, View.OnTouchListener, + SliderViewAdapter.DataSetListener, SliderPager.OnPageChangeListener { public static final int AUTO_CYCLE_DIRECTION_RIGHT = 0; public static final int AUTO_CYCLE_DIRECTION_LEFT = 1; @@ -60,11 +64,13 @@ public class SliderView extends FrameLayout implements Runnable { private boolean mFlagBackAndForth; private boolean mIsAutoCycle; private int mAutoCycleDirection; - private int mScrollTimeInSec; - private CircularSliderHandle mEndlessSliderHandle; + private int mScrollTimeInMillis; private PageIndicatorView mPagerIndicator; - private PagerAdapter mPagerAdapter; + private SliderViewAdapter mPagerAdapter; private SliderPager mSliderPager; + private InfinitePagerAdapter mInfinitePagerAdapter; + private boolean mPausedSliding = false; + private OnSliderPageListener mPageListener; /*Constructor*/ public SliderView(Context context) { @@ -116,7 +122,6 @@ private void setUpAttributes(@NonNull Context context, AttributeSet attrs) { RtlMode rtlMode = getRtlMode(indicatorRtlMode); int sliderAnimationDuration = typedArray.getInt(R.styleable.SliderView_sliderAnimationDuration, SliderPager.DEFAULT_SCROLL_DURATION); int sliderScrollTimeInSec = typedArray.getInt(R.styleable.SliderView_sliderScrollTimeInSec, 2); - boolean sliderCircularHandlerEnabled = typedArray.getBoolean(R.styleable.SliderView_sliderCircularHandlerEnabled, true); boolean sliderAutoCycleEnabled = typedArray.getBoolean(R.styleable.SliderView_sliderAutoCycleEnabled, true); boolean sliderStartAutoCycle = typedArray.getBoolean(R.styleable.SliderView_sliderStartAutoCycle, false); int sliderAutoCycleDirection = typedArray.getInt(R.styleable.SliderView_sliderAutoCycleDirection, AUTO_CYCLE_DIRECTION_RIGHT); @@ -133,12 +138,9 @@ private void setUpAttributes(@NonNull Context context, AttributeSet attrs) { setIndicatorRtlMode(rtlMode); setSliderAnimationDuration(sliderAnimationDuration); setScrollTimeInSec(sliderScrollTimeInSec); - setCircularHandlerEnabled(sliderCircularHandlerEnabled); setAutoCycle(sliderAutoCycleEnabled); setAutoCycleDirection(sliderAutoCycleDirection); - if (sliderStartAutoCycle) { - startAutoCycle(); - } + setAutoCycle(sliderStartAutoCycle); typedArray.recycle(); } @@ -149,6 +151,7 @@ private void setUpAttributes(@NonNull Context context, AttributeSet attrs) { * * @param context its android main context which is needed. */ + @SuppressLint("ClickableViewAccessibility") private void setupSlideView(Context context) { View wrapperView = LayoutInflater @@ -156,12 +159,13 @@ private void setupSlideView(Context context) { .inflate(R.layout.slider_view, this, true); mSliderPager = wrapperView.findViewById(R.id.vp_slider_layout); - mEndlessSliderHandle = new CircularSliderHandle(mSliderPager); - mSliderPager.addOnPageChangeListener(mEndlessSliderHandle); - mSliderPager.setOffscreenPageLimit(3); + mSliderPager.setOnTouchListener(this); + mSliderPager.addOnPageChangeListener(this); mPagerIndicator = wrapperView.findViewById(R.id.pager_indicator); mPagerIndicator.setViewPager(mSliderPager); + + } /** @@ -174,19 +178,21 @@ public void setOnIndicatorClickListener(DrawController.ClickListener listener) { /** * @param listener is a callback of current item in sliderView. */ - public void setCurrentPageListener(CircularSliderHandle.CurrentPageListener listener) { - mEndlessSliderHandle.setCurrentPageListener(listener); + public void setCurrentPageListener(OnSliderPageListener listener) { + this.mPageListener = listener; } /** * @param pagerAdapter Set a SliderAdapter that will supply views * for this slider as needed. */ - public void setSliderAdapter(final PagerAdapter pagerAdapter) { + public void setSliderAdapter(final SliderViewAdapter pagerAdapter) { mPagerAdapter = pagerAdapter; //set slider adapter + mInfinitePagerAdapter = new InfinitePagerAdapter(pagerAdapter); //registerAdapterDataObserver(); - mSliderPager.setAdapter(pagerAdapter); + mSliderPager.setAdapter(mInfinitePagerAdapter); + mPagerAdapter.dataSetChangedListener(this); //setup with indicator mPagerIndicator.setCount(getAdapterItemsCount()); mPagerIndicator.setDynamicCount(true); @@ -215,11 +221,6 @@ public boolean isAutoCycle() { public void setAutoCycle(boolean autoCycle) { this.mIsAutoCycle = autoCycle; - if (mIsAutoCycle) { - startAutoCycle(); - } else { - mHandler.removeCallbacks(this); - } } /** @@ -231,28 +232,26 @@ public void setOffscreenPageLimit(int limit) { mSliderPager.setOffscreenPageLimit(limit); } - /** - * @param enable circular endless scrolling in slider. - */ - public void setCircularHandlerEnabled(boolean enable) { - mSliderPager.clearOnPageChangeListeners(); - if (enable) { - mSliderPager.addOnPageChangeListener(mEndlessSliderHandle); - } - } - /** * @return sliding delay in seconds. */ public int getScrollTimeInSec() { - return mScrollTimeInSec; + return mScrollTimeInMillis * 1000; } /** * @param time of sliding delay in seconds. */ public void setScrollTimeInSec(int time) { - mScrollTimeInSec = time; + mScrollTimeInMillis = time * 1000; + } + + public int getScrollTimeInMillis() { + return mScrollTimeInMillis; + } + + public void setScrollTimeInMillis(int millis) { + this.mScrollTimeInMillis = millis; } /** @@ -331,11 +330,21 @@ public void setSliderTransformAnimation(SliderAnimations animation) { } + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_MOVE) { + mPausedSliding = true; + } else if (event.getAction() == MotionEvent.ACTION_UP) { + mPausedSliding = false; + } + return false; + } + /** * @param animation set slider animation manually . - * it accepts {@link ViewPager.PageTransformer} animation classes. + * it accepts {@link SliderPager.PageTransformer} animation classes. */ - public void setCustomSliderTransformAnimation(ViewPager.PageTransformer animation) { + public void setCustomSliderTransformAnimation(SliderPager.PageTransformer animation) { mSliderPager.setPageTransformer(false, animation); } @@ -347,12 +356,12 @@ public void setSliderAnimationDuration(int duration) { } /** - * @param duration changes slider animation duration. + * @param duration changes slider animation duration. * @param interpolator its animation duration accelerator * An interpolator defines the rate of change of an animation */ public void setSliderAnimationDuration(int duration, Interpolator interpolator) { - mSliderPager.setScrollDuration(duration , interpolator); + mSliderPager.setScrollDuration(duration, interpolator); } /** @@ -374,7 +383,7 @@ public void setCurrentPagePosition(int position) { public int getCurrentPagePosition() { if (getSliderAdapter() != null) { - return mSliderPager.getCurrentItem(); + return getSliderPager().getCurrentItem() % mPagerAdapter.getCount(); } else { throw new NullPointerException("Adapter not set"); } @@ -488,14 +497,22 @@ private int getAdapterItemsCount() { } /** - * This method stars auto cycling + * This method stars the auto cycling */ public void startAutoCycle() { - + //clean previous callbacks mHandler.removeCallbacks(this); //Run the loop for the first time - mHandler.postDelayed(this, mScrollTimeInSec * 1000); + mHandler.postDelayed(this, mScrollTimeInMillis); + } + + /** + * This method cancels the auto cycling + */ + public void stopAutoCycle() { + //clean callback + mHandler.removeCallbacks(this); } /** @@ -582,47 +599,73 @@ public int getIndicatorUnselectedColor() { */ @Override public void run() { - try { - // check is on auto scroll mode - if (!mIsAutoCycle) { - mHandler.removeCallbacks(this); - return; + if (!mPausedSliding) { + // slide to next if not paused + slideToNextPosition(); + } + } finally { + if (mIsAutoCycle) { + // continue the loop + mHandler.postDelayed(this, mScrollTimeInMillis); } + } + } + + public void slideToNextPosition() { + + int currentPosition = mSliderPager.getCurrentItem(); + int adapterItemsCount = getAdapterItemsCount(); - int currentPosition = mSliderPager.getCurrentItem(); - - if (mAutoCycleDirection == AUTO_CYCLE_DIRECTION_BACK_AND_FORTH) { - if (currentPosition == 0) { - mFlagBackAndForth = true; - } - if (currentPosition == getAdapterItemsCount() - 1) { - mFlagBackAndForth = false; - } - if (mFlagBackAndForth) { - mSliderPager.setCurrentItem(++currentPosition, true); - } else { - mSliderPager.setCurrentItem(--currentPosition, true); - } - } else if (mAutoCycleDirection == AUTO_CYCLE_DIRECTION_LEFT) { - if (currentPosition == 0) { - mSliderPager.setCurrentItem(getAdapterItemsCount() - 1, true); - } else { - mSliderPager.setCurrentItem(--currentPosition, true); - } + if (mAutoCycleDirection == AUTO_CYCLE_DIRECTION_BACK_AND_FORTH && adapterItemsCount > 0) { + if (currentPosition % (adapterItemsCount - 1) == 0) { + mFlagBackAndForth = !mFlagBackAndForth; + } + if (mFlagBackAndForth) { + mSliderPager.setCurrentItem(++currentPosition, true); } else { - if (currentPosition == getAdapterItemsCount() - 1) { - // if is last item return to the first position - mSliderPager.setCurrentItem(0, true); - } else { - // continue smooth transition between pager - mSliderPager.setCurrentItem(++currentPosition, true); - } + mSliderPager.setCurrentItem(--currentPosition, true); } + } else if (mAutoCycleDirection == AUTO_CYCLE_DIRECTION_LEFT) { + mSliderPager.setCurrentItem(--currentPosition, true); + } else { + mSliderPager.setCurrentItem(++currentPosition, true); + } + } + //sync infinite pager adapter with real one + @Override + public void dataSetChanged() { + mInfinitePagerAdapter.notifyDataSetChanged(); + mSliderPager.setCurrentItem((getAdapterItemsCount() - 1) * (InfinitePagerAdapter.INFINITE_SCROLL_LIMIT / 2), false); + } - } finally { - mHandler.postDelayed(this, mScrollTimeInSec * 1000); + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // nothing to do + } + + @Override + public void onPageSelected(int position) { + if (mPageListener != null) { + mPageListener.onSliderPageChanged(position); } } + + @Override + public void onPageScrollStateChanged(int state) { + // nothing to do + } + + public interface OnSliderPageListener { + + /** + * This method will be invoked when a new page becomes selected. Animation is not + * necessarily complete. + * + * @param position Position index of the new selected page. + */ + void onSliderPageChanged(int position); + + } } diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderViewAdapter.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderViewAdapter.java index 1d43e2f..37f75bd 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderViewAdapter.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderViewAdapter.java @@ -2,6 +2,7 @@ import androidx.annotation.NonNull; import androidx.viewpager.widget.PagerAdapter; + import android.view.View; import android.view.ViewGroup; @@ -11,6 +12,8 @@ public abstract class SliderViewAdapter extends PagerAdapter { + private DataSetListener dataSetListener; + //Default View holder class public static abstract class ViewHolder { public final View itemView; @@ -56,8 +59,17 @@ public int getItemPosition(Object object) { return POSITION_NONE; } + @Override + public void notifyDataSetChanged() { + super.notifyDataSetChanged(); + if (this.dataSetListener != null) { + dataSetListener.dataSetChanged(); + } + } + /** * Create a new view holder + * * @param parent wrapper view * @return view holder */ @@ -65,10 +77,18 @@ public int getItemPosition(Object object) { /** * Bind data at position into viewHolder + * * @param viewHolder item view holder - * @param position item position + * @param position item position */ public abstract void onBindViewHolder(VH viewHolder, int position); + void dataSetChangedListener(SliderViewAdapter.DataSetListener dataSetListener) { + this.dataSetListener = dataSetListener; + } + + interface DataSetListener { + void dataSetChanged(); + } } diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/AntiClockSpinTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/AntiClockSpinTransformation.java index d50a380..5878244 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/AntiClockSpinTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/AntiClockSpinTransformation.java @@ -1,10 +1,10 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class AntiClockSpinTransformation implements ViewPager.PageTransformer { +public class AntiClockSpinTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/Clock_SpinTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/Clock_SpinTransformation.java index a60bcf8..9c48c76 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/Clock_SpinTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/Clock_SpinTransformation.java @@ -1,10 +1,10 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class Clock_SpinTransformation implements ViewPager.PageTransformer { +public class Clock_SpinTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInDepthTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInDepthTransformation.java index 240383d..27edd34 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInDepthTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInDepthTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class CubeInDepthTransformation implements ViewPager.PageTransformer { +public class CubeInDepthTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { page.setCameraDistance(20000); diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInRotationTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInRotationTransformation.java index 1bf76aa..79743bb 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInRotationTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInRotationTransformation.java @@ -1,10 +1,10 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class CubeInRotationTransformation implements ViewPager.PageTransformer{ +public class CubeInRotationTransformation implements SliderPager.PageTransformer{ @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInScalingTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInScalingTransformation.java index f1b6f77..e40eee9 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInScalingTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInScalingTransformation.java @@ -1,9 +1,10 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; import android.view.View; -public class CubeInScalingTransformation implements ViewPager.PageTransformer { +import com.smarteist.autoimageslider.SliderPager; + +public class CubeInScalingTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { page.setCameraDistance(20000); diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutDepthTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutDepthTransformation.java index bbe276a..db31dd6 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutDepthTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutDepthTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class CubeOutDepthTransformation implements ViewPager.PageTransformer { +public class CubeOutDepthTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutRotationTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutRotationTransformation.java index 8b2f422..05fa622 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutRotationTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutRotationTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class CubeOutRotationTransformation implements ViewPager.PageTransformer { +public class CubeOutRotationTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutScalingTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutScalingTransformation.java index de2e452..69dc536 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutScalingTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutScalingTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class CubeOutScalingTransformation implements ViewPager.PageTransformer{ +public class CubeOutScalingTransformation implements SliderPager.PageTransformer{ @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/DepthTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/DepthTransformation.java index 9ac99fa..eb612ee 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/DepthTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/DepthTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class DepthTransformation implements ViewPager.PageTransformer{ +public class DepthTransformation implements SliderPager.PageTransformer{ @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FadeTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FadeTransformation.java index 5c71c37..e2144dd 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FadeTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FadeTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class FadeTransformation implements ViewPager.PageTransformer{ +public class FadeTransformation implements SliderPager.PageTransformer{ @Override public void transformPage(View view, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FanTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FanTransformation.java index 94645eb..7f2d936 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FanTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FanTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class FanTransformation implements ViewPager.PageTransformer{ +public class FanTransformation implements SliderPager.PageTransformer{ @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FidgetSpinTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FidgetSpinTransformation.java index f813803..057b563 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FidgetSpinTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FidgetSpinTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class FidgetSpinTransformation implements ViewPager.PageTransformer { +public class FidgetSpinTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/GateTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/GateTransformation.java index 4e2a220..e88921b 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/GateTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/GateTransformation.java @@ -1,10 +1,10 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class GateTransformation implements ViewPager.PageTransformer{ +public class GateTransformation implements SliderPager.PageTransformer{ private String TAG = "GateAnimationn"; @Override diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HingeTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HingeTransformation.java index 223dfac..ce9a70a 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HingeTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HingeTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class HingeTransformation implements ViewPager.PageTransformer{ +public class HingeTransformation implements SliderPager.PageTransformer{ @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HorizontalFlipTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HorizontalFlipTransformation.java index e2e71f0..c4d331f 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HorizontalFlipTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HorizontalFlipTransformation.java @@ -1,10 +1,10 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.util.Log; import android.view.View; -public class HorizontalFlipTransformation implements ViewPager.PageTransformer { +public class HorizontalFlipTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/PopTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/PopTransformation.java index 0b6c06c..9f4dba5 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/PopTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/PopTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class PopTransformation implements ViewPager.PageTransformer { +public class PopTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SimpleTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SimpleTransformation.java index 154ce45..2954de2 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SimpleTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SimpleTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class SimpleTransformation implements ViewPager.PageTransformer { +public class SimpleTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SpinnerTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SpinnerTransformation.java index 2828ad1..a232b75 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SpinnerTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SpinnerTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class SpinnerTransformation implements ViewPager.PageTransformer { +public class SpinnerTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/TossTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/TossTransformation.java index 025a35b..08de8eb 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/TossTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/TossTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class TossTransformation implements ViewPager.PageTransformer { +public class TossTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalFlipTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalFlipTransformation.java index bf8d89d..c70d2af 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalFlipTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalFlipTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class VerticalFlipTransformation implements ViewPager.PageTransformer { +public class VerticalFlipTransformation implements SliderPager.PageTransformer { @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalShutTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalShutTransformation.java index 0df52a6..9f5a0e8 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalShutTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalShutTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class VerticalShutTransformation implements ViewPager.PageTransformer{ +public class VerticalShutTransformation implements SliderPager.PageTransformer{ @Override public void transformPage(View page, float position) { diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/ZoomOutTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/ZoomOutTransformation.java index 02e41fc..8c04102 100644 --- a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/ZoomOutTransformation.java +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/ZoomOutTransformation.java @@ -1,9 +1,9 @@ package com.smarteist.autoimageslider.Transformations; -import androidx.viewpager.widget.ViewPager; +import com.smarteist.autoimageslider.SliderPager; import android.view.View; -public class ZoomOutTransformation implements ViewPager.PageTransformer { +public class ZoomOutTransformation implements SliderPager.PageTransformer { private static final float MIN_SCALE = 0.65f; private static final float MIN_ALPHA = 0.3f; From 8f3e775e01263047458fa69e374b64947c7cb518 Mon Sep 17 00:00:00 2001 From: Ali Hn Date: Sun, 16 Feb 2020 11:33:29 +0330 Subject: [PATCH 2/3] Attributes and README.md updated --- README.md | 124 ++++++++++-------- .../autoimageslider/SliderPager.java | 1 - autoimageslider/src/main/res/values/attrs.xml | 2 - 3 files changed, 72 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 6deac20..495f60a 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,28 @@ This is an amazing image slider for the Android . You can easily load images with your custom layout, and there are many kinds of amazing animations you can choose. ```groovy - implementation 'com.github.smarteist:autoimageslider:1.3.3' + implementation 'com.github.smarteist:autoimageslider:1.3.4' ``` -If you are using appcompat libraries +If you are using appcompat libraries use this one, but please migrate to androidx as soon as you can. ```groovy implementation 'com.github.smarteist:autoimageslider:1.3.2-appcompat' ``` -### New Feautures -* Bugs fixed. -* Ability to set an animation interpolator for slider. - -# Demo +### New Feautures +* Infinite adapter implemented +* Auto cycle Bugs fixed. +* Slider API improvements. + +### New Changes +* Circular handle completely replaced with infinite wrapper adapter. +because of that the following interface has been replaced with this one. +`CircularSliderHandle.CurrentPageListener` to=> `SliderView.OnSliderPageListener` +and please change it in `sliderView.setCurrentPageListener(***)` method. +* The slider behaves infinitely permanently, so the following methods have also been deleted. +`sliderView.setCircularHandlerEnabled(boolean enable)` +& its attribute in xml side: +`app:sliderCircularHandlerEnabled="boolean"` +## Demo ![](https://github.com/smarteist/android-image-slider/blob/master/gif/0.gif) ![](https://github.com/smarteist/android-image-slider/blob/master/gif/8.gif) ![](https://github.com/smarteist/android-image-slider/blob/master/gif/4.gif) @@ -39,7 +49,6 @@ First put the slider view in your layout xml : app:sliderAnimationDuration="600" app:sliderAutoCycleDirection="back_and_forth" app:sliderAutoCycleEnabled="true" - app:sliderCircularHandlerEnabled="true" app:sliderIndicatorAnimationDuration="600" app:sliderIndicatorGravity="center_horizontal|bottom" app:sliderIndicatorMargin="15dp" @@ -51,16 +60,16 @@ First put the slider view in your layout xml : app:sliderScrollTimeInSec="1" app:sliderStartAutoCycle="true" /> ``` - + Or you can put it inside the cardView to look more beautiful : - + ```xml - + - + ``` - -# Next step + +# Next step The new version requires an slider adapter plus your custom layout for slider items, Although its very similar to RecyclerView & RecyclerAdapter, and it's familiar and easy to implement this adapter... here is an example for adapter implementation : -```java -public class SliderAdapterExample extends SliderViewAdapter { +```java +public class SliderAdapterExample extends + SliderViewAdapter { private Context context; + private List mSliderItems = new ArrayList<>(); public SliderAdapterExample(Context context) { this.context = context; } + public void renewItems(List sliderItems) { + this.mSliderItems = sliderItems; + notifyDataSetChanged(); + } + + public void deleteItem(int position) { + this.mSliderItems.remove(position); + notifyDataSetChanged(); + } + + public void addItem(SliderItem sliderItem) { + this.mSliderItems.add(sliderItem); + notifyDataSetChanged(); + } + @Override public SliderAdapterVH onCreateViewHolder(ViewGroup parent) { View inflate = LayoutInflater.from(parent.getContext()).inflate(R.layout.image_slider_layout_item, null); @@ -103,64 +128,58 @@ public class SliderAdapterExample extends SliderViewAdapter= 0; i--) { - // TODO: Add versioned support here for transformed views. // This will not work for transformed views in Honeycomb+ final View child = group.getChildAt(i); if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() diff --git a/autoimageslider/src/main/res/values/attrs.xml b/autoimageslider/src/main/res/values/attrs.xml index 2067fce..a152593 100644 --- a/autoimageslider/src/main/res/values/attrs.xml +++ b/autoimageslider/src/main/res/values/attrs.xml @@ -88,8 +88,6 @@ - - From 512c637d9558f541096d42733e74b67301187aa9 Mon Sep 17 00:00:00 2001 From: Ali Hn Date: Sun, 16 Feb 2020 11:48:35 +0330 Subject: [PATCH 3/3] README.md updated --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 495f60a..e959bb0 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ If you are using appcompat libraries use this one, but please migrate to android ### New Changes * Circular handle completely replaced with infinite wrapper adapter. -because of that the following interface has been replaced with this one. -`CircularSliderHandle.CurrentPageListener` to=> `SliderView.OnSliderPageListener` -and please change it in `sliderView.setCurrentPageListener(***)` method. -* The slider behaves infinitely permanently, so the following methods have also been deleted. +because of that the following interface has been replaced with new one. +```CircularSliderHandle.CurrentPageListener``` +changed to => `SliderView.OnSliderPageListener`. +* The slider permanently scrolls infinitely, so the following methods have also been deleted. `sliderView.setCircularHandlerEnabled(boolean enable)` & its attribute in xml side: `app:sliderCircularHandlerEnabled="boolean"` @@ -37,7 +37,7 @@ and please change it in `sliderView.setCurrentPageListener(***)` method. ![](https://github.com/smarteist/android-image-slider/blob/master/gif/4.gif) ![](https://github.com/smarteist/android-image-slider/blob/master/gif/7.gif) -# Integration guide +## Integration guide First put the slider view in your layout xml : @@ -91,7 +91,7 @@ Or you can put it inside the cardView to look more beautiful : ``` -# Next step +## Next step The new version requires an slider adapter plus your custom layout for slider items, Although its very similar to RecyclerView & RecyclerAdapter, and it's familiar and easy to implement this adapter... here is an example for adapter implementation : @@ -172,7 +172,7 @@ public class SliderAdapterExample extends } ``` -# Set the adapter to the Sliderview +## Set the adapter to the Sliderview After the instantiating of the sliderView (inside the activity or fragment with findViewById|BindView blah blah...), set the adapter to the slider. @@ -188,7 +188,7 @@ You can call this method if you want to start flipping automatically and you can sliderView.setSliderTransformAnimation(SliderAnimations.SIMPLETRANSFORMATION); ``` -# Elaborate more? +## Elaborate more? Here is a more realistic and more complete example : @@ -216,12 +216,12 @@ Here is a more realistic and more complete example : } ``` -# Contribute +## Contribute Suggestions and pull requests are always welcome. Special Thanks [Roman Danylyk] (https://github.com/romandanylyk) for nice indicator! -# Licence +## Licence Copyright [2019] [Ali Hosseini]