--- /dev/null
+package com.folioreader.view;
+
+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.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.v4.os.ParcelableCompat;
+import android.support.v4.os.ParcelableCompatCreatorCallbacks;
+import android.support.v4.view.AccessibilityDelegateCompat;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.VelocityTrackerCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewConfigurationCompat;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.support.v4.view.ViewPager.PageTransformer;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;
+import android.support.v4.widget.EdgeEffectCompat;
+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.Interpolator;
+import android.widget.Scroller;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * Created by castorflex on 12/29/13.
+ * Just a copy of the original ViewPager modified to support vertical Scrolling
+ */
+public class VerticalViewPager extends ViewGroup {
+ private static final String TAG = "ViewPager";
+ 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
+
+ private 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 heightFactor;
+
+ float offset;
+ }
+
+ private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>() {
+ @Override
+ public int compare(ItemInfo lhs, ItemInfo rhs) {
+ return lhs.position - rhs.position;
+ }
+ };
+
+ private static final Interpolator sInterpolator = new Interpolator() {
+ public float getInterpolation(float t) {
+ t -= 1.0f;
+ return t * t * t * t * t + 1.0f;
+ }
+ };
+
+ private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
+ private final ItemInfo mTempItem = new ItemInfo();
+
+ private final Rect mTempRect = new Rect();
+
+ private PagerAdapter mAdapter;
+ private int mCurItem; // Index of currently displayed page.
+ private int mRestoredCurItem = -1;
+ private Parcelable mRestoredAdapterState = null;
+ private ClassLoader mRestoredClassLoader = null;
+ private Scroller mScroller;
+ private PagerObserver mObserver;
+
+ private int mPageMargin;
+ private Drawable mMarginDrawable;
+ private int mLeftPageBounds;
+ private int mRightPageBounds;
+
+ // 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 boolean mIgnoreGutter;
+ 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 EdgeEffectCompat mTopEdge;
+ private EdgeEffectCompat mBottomEdge;
+ private boolean mFirstLayout = true;
+ private boolean mNeedCalculatePageOffsets = false;
+ private boolean mCalledSuper;
+ private int mDecorChildCount;
+
+ private ViewPager.OnPageChangeListener mOnPageChangeListener;
+ private ViewPager.OnPageChangeListener mInternalPageChangeListener;
+ private OnAdapterChangeListener mAdapterChangeListener;
+ private ViewPager.PageTransformer mPageTransformer;
+ private Method mSetChildrenDrawingOrderEnabled;
+
+ 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<View> 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() {
+ public void run() {
+ setScrollState(SCROLL_STATE_IDLE);
+ populate();
+ }
+ };
+
+ private int mScrollState = SCROLL_STATE_IDLE;
+
+ // private ScrollerCustomDuration mScrollerCustomDuration = null;
+
+ /**
+ * Used internally to monitor when adapters are switched.
+ */
+ interface OnAdapterChangeListener {
+ public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter);
+ }
+
+ /**
+ * Used internally to tag special types of child views that should be added as
+ * pager decorations by default.
+ */
+ interface Decor {
+ }
+
+ public VerticalViewPager(Context context) {
+ super(context);
+ initViewPager();
+ //postInitViewPager();
+ }
+
+ public VerticalViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initViewPager();
+ // postInitViewPager();
+ }
+
+ void initViewPager() {
+ 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 = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
+ mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mTopEdge = new EdgeEffectCompat(context);
+ mBottomEdge = new EdgeEffectCompat(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);
+ }
+ //postInitViewPager();
+ }
+
+
+ /* private void postInitViewPager() {
+ try {
+ Field scroller = ViewPager.class.getDeclaredField("mScroller");
+ scroller.setAccessible(true);
+ Field interpolator = ViewPager.class.getDeclaredField("sInterpolator");
+ interpolator.setAccessible(true);
+
+ mScrollerCustomDuration = new ScrollerCustomDuration(getContext(),
+ (Interpolator) interpolator.get(null));
+ scroller.set(this, mScrollerCustomDuration);
+ } catch (Exception e) {
+ }
+ }
+
+ public void setScrollDurationFactor(double scrollFactor) {
+ mScrollerCustomDuration.setScrollDurationFactor(scrollFactor);
+ }*/
+
+ @Override
+ protected void onDetachedFromWindow() {
+ removeCallbacks(mEndScrollRunnable);
+ super.onDetachedFromWindow();
+ }
+
+ private 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);
+ }
+ if (mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageScrollStateChanged(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) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ 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();
+ }
+ 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();
+ }
+ }
+
+ if (mAdapterChangeListener != null && oldAdapter != adapter) {
+ mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
+ }
+ }
+
+ 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
+ */
+ public PagerAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ void setOnAdapterChangeListener(OnAdapterChangeListener listener) {
+ mAdapterChangeListener = listener;
+ }
+
+// private int getClientWidth() {
+// return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+// }
+
+ private int getClientHeight() {
+ return getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+ }
+
+
+ /**
+ * Set the currently selected page. If the ViewPager 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;
+ if (dispatchSelected && mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageSelected(item);
+ }
+ if (dispatchSelected && mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageSelected(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 destY = 0;
+ if (curInfo != null) {
+ final int height = getClientHeight();
+ destY = (int) (height * Math.max(mFirstOffset,
+ Math.min(curInfo.offset, mLastOffset)));
+ }
+ if (smoothScroll) {
+ smoothScrollTo(0, destY, velocity);
+ if (dispatchSelected && mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageSelected(item);
+ }
+ if (dispatchSelected && mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageSelected(item);
+ }
+ } else {
+ if (dispatchSelected && mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageSelected(item);
+ }
+ if (dispatchSelected && mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageSelected(item);
+ }
+ completeScroll(false);
+ scrollTo(0, destY);
+ pageScrolled(destY);
+ }
+ }
+
+ /**
+ * Set a listener that will be invoked whenever the page changes or is incrementally
+ * scrolled. See {@link ViewPager.OnPageChangeListener}.
+ *
+ * @param listener Listener to set
+ */
+ public void setOnPageChangeListener(OnPageChangeListener listener) {
+ mOnPageChangeListener = listener;
+ }
+
+ /**
+ * Set a {@link ViewPager.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 look and feel.
+ * <p/>
+ * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.
+ * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no
+ * effect.</p>
+ *
+ * @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, PageTransformer transformer) {
+ if (Build.VERSION.SDK_INT >= 11) {
+ final boolean hasTransformer = transformer != null;
+ final boolean needsPopulate = hasTransformer != (mPageTransformer != null);
+ mPageTransformer = transformer;
+ setChildrenDrawingOrderEnabledCompat(hasTransformer);
+ if (hasTransformer) {
+ mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;
+ } else {
+ mDrawingOrder = DRAW_ORDER_DEFAULT;
+ }
+ if (needsPopulate) populate();
+ }
+ }
+
+ void setChildrenDrawingOrderEnabledCompat(boolean enable) {
+ if (Build.VERSION.SDK_INT >= 7) {
+ if (mSetChildrenDrawingOrderEnabled == null) {
+ try {
+ mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod(
+ "setChildrenDrawingOrderEnabled", new Class[]{Boolean.TYPE});
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e);
+ }
+ }
+ try {
+ mSetChildrenDrawingOrderEnabled.invoke(this, enable);
+ } catch (Exception e) {
+ Log.e(TAG, "Error changing children drawing order", e);
+ }
+ }
+ }
+
+ @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.
+ * <p/>
+ * <p>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.</p>
+ * <p/>
+ * <p>You should keep this limit low, especially if your pages have complex layouts.
+ * This setting defaults to 1.</p>
+ *
+ * @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 height = getHeight();
+ recomputeScrollPosition(height, height, 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(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(int resId) {
+ setPageMarginDrawable(getContext().getResources().getDrawable(resId));
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || who == mMarginDrawable;
+ }
+
+ @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 * 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 = 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 height = getClientHeight();
+ final int halfHeight = height / 2;
+ final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / height);
+ final float distance = halfHeight + halfHeight *
+ distanceInfluenceForSnapDuration(distanceRatio);
+
+ int duration = 0;
+ velocity = Math.abs(velocity);
+ if (velocity > 0) {
+ duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+ } else {
+ final float pageHeight = height * mAdapter.getPageWidth(mCurItem);
+ final float pageDelta = (float) Math.abs(dx) / (pageHeight + mPageMargin);
+ duration = (int) ((pageDelta + 1) * 100);
+ }
+ duration = Math.min(duration, MAX_SETTLE_DURATION);
+
+ 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.heightFactor = 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.heightFactor = 0.f;
+ }
+ }
+
+ setCurrentItemInternal(newCurrItem, false, true);
+ requestLayout();
+ }
+ }
+
+ void populate() {
+ populate(mCurItem);
+ }
+
+ void populate(int newCurrentItem) {
+ ItemInfo oldCurInfo = null;
+ int focusDirection = View.FOCUS_FORWARD;
+ if (mCurItem != newCurrentItem) {
+ focusDirection = mCurItem < newCurrentItem ? View.FOCUS_DOWN : View.FOCUS_UP;
+ 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 extraHeightTop = 0.f;
+ int itemIndex = curIndex - 1;
+ ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ final int clientHeight = getClientHeight();
+ final float topHeightNeeded = clientHeight <= 0 ? 0 :
+ 2.f - curItem.heightFactor + (float) getPaddingLeft() / (float) clientHeight;
+ for (int pos = mCurItem - 1; pos >= 0; pos--) {
+ if (extraHeightTop >= topHeightNeeded && 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) {
+ extraHeightTop += ii.heightFactor;
+ itemIndex--;
+ ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ } else {
+ ii = addNewItem(pos, itemIndex + 1);
+ extraHeightTop += ii.heightFactor;
+ curIndex++;
+ ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ }
+ }
+
+ float extraHeightBottom = curItem.heightFactor;
+ itemIndex = curIndex + 1;
+ if (extraHeightBottom < 2.f) {
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ final float bottomHeightNeeded = clientHeight <= 0 ? 0 :
+ (float) getPaddingRight() / (float) clientHeight + 2.f;
+ for (int pos = mCurItem + 1; pos < N; pos++) {
+ if (extraHeightBottom >= bottomHeightNeeded && 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) {
+ extraHeightBottom += ii.heightFactor;
+ itemIndex++;
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ } else {
+ ii = addNewItem(pos, itemIndex);
+ itemIndex++;
+ extraHeightBottom += ii.heightFactor;
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ }
+ }
+ }
+
+ calculatePageOffsets(curItem, curIndex, oldCurInfo);
+ }
+
+ 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.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
+
+ 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.heightFactor == 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.heightFactor = ii.heightFactor;
+ 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
+ && child.requestFocus(focusDirection)) {
+// if (child.requestFocus(focusDirection)) {
+ break;
+ // }
+ }
+ }
+ }
+ }
+ }
+
+ private void sortChildDrawingOrder() {
+ if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
+ if (mDrawingOrderedChildren == null) {
+ mDrawingOrderedChildren = new ArrayList<View>();
+ } 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 calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
+ final int N = mAdapter.getCount();
+ final int height = getClientHeight();
+ final float marginOffset = height > 0 ? (float) mPageMargin / height : 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.heightFactor + 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.heightFactor + 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.heightFactor + 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.heightFactor - 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.heightFactor + marginOffset;
+ ii.offset = offset;
+ if (ii.position == 0) mFirstOffset = offset;
+ }
+ offset = curItem.offset + curItem.heightFactor + 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.heightFactor - 1;
+ }
+ ii.offset = offset;
+ offset += ii.heightFactor + marginOffset;
+ }
+
+ mNeedCalculatePageOffsets = false;
+ }
+
+ /**
+ * This is the persistent state that is saved by ViewPager. Only needed
+ * if you are creating a sublass of ViewPager 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 BaseSavedState {
+ int position;
+ Parcelable adapterState;
+ ClassLoader loader;
+
+ public SavedState(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<SavedState> CREATOR =
+ ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in, ClassLoader loader) {
+ return new SavedState(in, loader);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ });
+
+ SavedState(Parcel in, ClassLoader loader) {
+ super(in);
+ 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;
+ lp.isDecor |= child instanceof Decor;
+ 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);
+ }
+ }
+ }
+
+ @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 measuredHeight = getMeasuredHeight();
+ final int maxGutterSize = measuredHeight / 10;
+ mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
+
+ // Children are just made to fill our space.
+ int childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+ int childHeightSize = measuredHeight - 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.FILL_PARENT) {
+ widthSize = lp.width;
+ }
+ }
+ if (lp.height != LayoutParams.WRAP_CONTENT) {
+ heightMode = MeasureSpec.EXACTLY;
+ if (lp.height != LayoutParams.FILL_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 heightSpec = MeasureSpec.makeMeasureSpec(
+ (int) (childHeightSize * lp.heightFactor), MeasureSpec.EXACTLY);
+ child.measure(mChildWidthMeasureSpec, heightSpec);
+ }
+ }
+ }
+ }
+
+ @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 (h != oldh) {
+ recomputeScrollPosition(h, oldh, mPageMargin, mPageMargin);
+ }
+ }
+
+ private void recomputeScrollPosition(int height, int oldHeight, int margin, int oldMargin) {
+ if (oldHeight > 0 && !mItems.isEmpty()) {
+ final int heightWithMargin = height - getPaddingTop() - getPaddingBottom() + margin;
+ final int oldHeightWithMargin = oldHeight - getPaddingTop() - getPaddingBottom()
+ + oldMargin;
+ final int ypos = getScrollY();
+ final float pageOffset = (float) ypos / oldHeightWithMargin;
+ final int newOffsetPixels = (int) (pageOffset * heightWithMargin);
+
+ scrollTo(getScrollX(), newOffsetPixels);
+ if (!mScroller.isFinished()) {
+ // We now return to your regularly scheduled scroll, already in progress.
+ final int newDuration = mScroller.getDuration() - mScroller.timePassed();
+ ItemInfo targetInfo = infoForPosition(mCurItem);
+ mScroller.startScroll(0, newOffsetPixels,
+ 0, (int) (targetInfo.offset * height), newDuration);
+ }
+ } else {
+ final ItemInfo ii = infoForPosition(mCurItem);
+ final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
+ final int scrollPos = (int) (scrollOffset *
+ (height - getPaddingTop() - getPaddingBottom()));
+ if (scrollPos != getScrollY()) {
+ completeScroll(false);
+ scrollTo(getScrollX(), scrollPos);
+ }
+ }
+ }
+
+ @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 scrollY = getScrollY();
+
+ 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;
+ }
+ childTop += scrollY;
+ child.layout(childLeft, childTop,
+ childLeft + child.getMeasuredWidth(),
+ childTop + child.getMeasuredHeight());
+ decorCount++;
+ }
+ }
+ }
+
+ final int childHeight = height - paddingTop - paddingBottom;
+ // 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 toff = (int) (childHeight * ii.offset);
+ int childLeft = paddingLeft;
+ int childTop = paddingTop + toff;
+ 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) (width - paddingLeft - paddingRight),
+ MeasureSpec.EXACTLY);
+ final int heightSpec = MeasureSpec.makeMeasureSpec(
+ (int) (childHeight * lp.heightFactor),
+ 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());
+ }
+ }
+ }
+ mLeftPageBounds = paddingLeft;
+ mRightPageBounds = width - paddingRight;
+ mDecorChildCount = decorCount;
+
+ if (mFirstLayout) {
+ scrollToItem(mCurItem, false, 0, false);
+ }
+ mFirstLayout = false;
+ }
+
+ @Override
+ public void computeScroll() {
+ 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(y)) {
+ mScroller.abortAnimation();
+ scrollTo(x, 0);
+ }
+ }
+
+ // Keep on drawing until the animation has finished.
+ ViewCompat.postInvalidateOnAnimation(this);
+ return;
+ }
+
+ // Done with scroll, clean up state.
+ completeScroll(true);
+ }
+
+ private boolean pageScrolled(int ypos) {
+ if (mItems.size() == 0) {
+ 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 height = getClientHeight();
+ final int heightWithMargin = height + mPageMargin;
+ final float marginOffset = (float) mPageMargin / height;
+ final int currentPage = ii.position;
+ final float pageOffset = (((float) ypos / height) - ii.offset) /
+ (ii.heightFactor + marginOffset);
+ final int offsetPixels = (int) (pageOffset * heightWithMargin);
+
+ 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.
+ */
+ 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 scrollY = getScrollY();
+ int paddingTop = getPaddingTop();
+ int paddingBottom = getPaddingBottom();
+ final int height = getHeight();
+ 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 vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ int childTop = 0;
+ switch (vgrav) {
+ default:
+ childTop = paddingTop;
+ break;
+ case Gravity.TOP:
+ childTop = paddingTop;
+ paddingTop += child.getHeight();
+ 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;
+ }
+ childTop += scrollY;
+
+ final int childOffset = childTop - child.getTop();
+ if (childOffset != 0) {
+ child.offsetTopAndBottom(childOffset);
+ }
+ }
+ }
+
+ if (mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
+ }
+ if (mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
+ }
+
+ if (mPageTransformer != null) {
+ final int scrollY = getScrollY();
+ 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.getTop() - scrollY) / getClientHeight();
+ mPageTransformer.transformPage(child, transformPos);
+ }
+ }
+
+ mCalledSuper = true;
+ }
+
+ 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);
+ mScroller.abortAnimation();
+ int oldX = getScrollX();
+ int oldY = getScrollY();
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+ if (oldX != x || oldY != y) {
+ scrollTo(x, y);
+ }
+ }
+ 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();
+ }
+ }
+ }
+
+ private boolean isGutterDrag(float y, float dy) {
+ return (y < mGutterSize && dy > 0) || (y > getHeight() - mGutterSize && dy < 0);
+ }
+
+ private void enableLayers(boolean enable) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final int layerType = enable ?
+ ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE;
+ ViewCompat.setLayerType(getChildAt(i), layerType, null);
+ }
+ }
+
+ @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.
+ */
+
+ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+
+ // 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!");
+ mIsBeingDragged = false;
+ mIsUnableToDrag = false;
+ mActivePointerId = INVALID_POINTER;
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ 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 = MotionEventCompat.findPointerIndex(ev, activePointerId);
+ final float y = MotionEventCompat.getY(ev, pointerIndex);
+ final float dy = y - mLastMotionY;
+ final float yDiff = Math.abs(dy);
+ final float x = MotionEventCompat.getX(ev, pointerIndex);
+ final float xDiff = Math.abs(x - mInitialMotionX);
+ if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
+
+ if (dy != 0 && !isGutterDrag(mLastMotionY, dy) &&
+ canScroll(this, false, (int) dy, (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 (yDiff > mTouchSlop && yDiff * 0.5f > xDiff) {
+ if (DEBUG) Log.v(TAG, "Starting drag!");
+ mIsBeingDragged = true;
+ requestParentDisallowInterceptTouchEvent(true);
+ setScrollState(SCROLL_STATE_DRAGGING);
+ mLastMotionY = dy > 0 ? mInitialMotionY + mTouchSlop :
+ mInitialMotionY - mTouchSlop;
+ mLastMotionX = x;
+ setScrollingCacheEnabled(true);
+ } else if (xDiff > 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 && performDrag(y)) {
+ // Scroll to follow the motion event
+ 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 = MotionEventCompat.getPointerId(ev, 0);
+ mIsUnableToDrag = false;
+
+ mScroller.computeScrollOffset();
+ if (mScrollState == SCROLL_STATE_SETTLING &&
+ Math.abs(mScroller.getFinalY() - mScroller.getCurrY()) > 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 MotionEventCompat.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;
+ }
+
+ @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 & MotionEventCompat.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 = MotionEventCompat.getPointerId(ev, 0);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE:
+ if (!mIsBeingDragged) {
+ final int pointerIndex =
+ MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ final float y = MotionEventCompat.getY(ev, pointerIndex);
+ final float yDiff = Math.abs(y - mLastMotionY);
+ final float x = MotionEventCompat.getX(ev, pointerIndex);
+ final float xDiff = Math.abs(x - mLastMotionX);
+ if (DEBUG)
+ Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
+ if (yDiff > mTouchSlop && yDiff > xDiff) {
+ if (DEBUG) Log.v(TAG, "Starting drag!");
+ mIsBeingDragged = true;
+ requestParentDisallowInterceptTouchEvent(true);
+ mLastMotionY = y - mInitialMotionY > 0 ? mInitialMotionY + mTouchSlop :
+ mInitialMotionY - mTouchSlop;
+ mLastMotionX = x;
+ 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 = MotionEventCompat.findPointerIndex(
+ ev, mActivePointerId);
+ final float y = MotionEventCompat.getY(ev, activePointerIndex);
+ needsInvalidate |= performDrag(y);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mIsBeingDragged) {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
+ velocityTracker, mActivePointerId);
+ mPopulatePending = true;
+ final int height = getClientHeight();
+ final int scrollY = getScrollY();
+ final ItemInfo ii = infoForCurrentScrollPosition();
+ final int currentPage = ii.position;
+ final float pageOffset =
+ (((float) scrollY / height) - ii.offset) / ii.heightFactor;
+ final int activePointerIndex =
+ MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ final float y = MotionEventCompat.getY(ev, activePointerIndex);
+ final int totalDelta = (int) (y - mInitialMotionY);
+ int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
+ totalDelta);
+ setCurrentItemInternal(nextPage, true, true, initialVelocity);
+
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease();
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (mIsBeingDragged) {
+ scrollToItem(mCurItem, true, 0, false);
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease();
+ }
+ break;
+ case MotionEventCompat.ACTION_POINTER_DOWN: {
+ final int index = MotionEventCompat.getActionIndex(ev);
+ final float y = MotionEventCompat.getY(ev, index);
+ mLastMotionY = y;
+ mActivePointerId = MotionEventCompat.getPointerId(ev, index);
+ break;
+ }
+ case MotionEventCompat.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ mLastMotionY = MotionEventCompat.getY(ev,
+ MotionEventCompat.findPointerIndex(ev, mActivePointerId));
+ break;
+ }
+ if (needsInvalidate) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ return true;
+ }
+
+ private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ }
+
+ private boolean performDrag(float y) {
+ boolean needsInvalidate = false;
+
+ final float deltaY = mLastMotionY - y;
+ mLastMotionY = y;
+
+ float oldScrollY = getScrollY();
+ float scrollY = oldScrollY + deltaY;
+ final int height = getClientHeight();
+
+ float topBound = height * mFirstOffset;
+ float bottomBound = height * mLastOffset;
+ boolean topAbsolute = true;
+ boolean bottomAbsolute = true;
+
+ final ItemInfo firstItem = mItems.get(0);
+ final ItemInfo lastItem = mItems.get(mItems.size() - 1);
+ if (firstItem.position != 0) {
+ topAbsolute = false;
+ topBound = firstItem.offset * height;
+ }
+ if (lastItem.position != mAdapter.getCount() - 1) {
+ bottomAbsolute = false;
+ bottomBound = lastItem.offset * height;
+ }
+
+ if (scrollY < topBound) {
+ if (topAbsolute) {
+ float over = topBound - scrollY;
+ needsInvalidate = mTopEdge.onPull(Math.abs(over) / height);
+ }
+ scrollY = topBound;
+ } else if (scrollY > bottomBound) {
+ if (bottomAbsolute) {
+ float over = scrollY - bottomBound;
+ needsInvalidate = mBottomEdge.onPull(Math.abs(over) / height);
+ }
+ scrollY = bottomBound;
+ }
+ // Don't lose the rounded component
+ mLastMotionX += scrollY - (int) scrollY;
+ scrollTo(getScrollX(), (int) scrollY);
+ pageScrolled((int) scrollY);
+
+ 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 height = getClientHeight();
+ final float scrollOffset = height > 0 ? (float) getScrollY() / height : 0;
+ final float marginOffset = height > 0 ? (float) mPageMargin / height : 0;
+ int lastPos = -1;
+ float lastOffset = 0.f;
+ float lastHeight = 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 + lastHeight + marginOffset;
+ ii.position = lastPos + 1;
+ ii.heightFactor = mAdapter.getPageWidth(ii.position);
+ i--;
+ }
+ offset = ii.offset;
+
+ final float topBound = offset;
+ final float bottomBound = offset + ii.heightFactor + marginOffset;
+ if (first || scrollOffset >= topBound) {
+ if (scrollOffset < bottomBound || i == mItems.size() - 1) {
+ return ii;
+ }
+ } else {
+ return lastItem;
+ }
+ first = false;
+ lastPos = ii.position;
+ lastOffset = offset;
+ lastHeight = ii.heightFactor;
+ lastItem = ii;
+ }
+
+ return lastItem;
+ }
+
+ private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaY) {
+ int targetPage;
+ if (Math.abs(deltaY) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
+ targetPage = velocity > 0 ? currentPage : currentPage + 1;
+ } else {
+ final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
+ targetPage = (int) (currentPage + 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 = ViewCompat.getOverScrollMode(this);
+ if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
+ mAdapter != null && mAdapter.getCount() > 1)) {
+ if (!mTopEdge.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int height = getHeight();
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+
+ canvas.translate(getPaddingLeft(), mFirstOffset * height);
+ mTopEdge.setSize(width, height);
+ needsInvalidate |= mTopEdge.draw(canvas);
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mBottomEdge.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int height = getHeight();
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+
+ canvas.rotate(180);
+ canvas.translate(-width - getPaddingLeft(), -(mLastOffset + 1) * height);
+ mBottomEdge.setSize(width, height);
+ needsInvalidate |= mBottomEdge.draw(canvas);
+ canvas.restoreToCount(restoreCount);
+ }
+ } else {
+ mTopEdge.finish();
+ mBottomEdge.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 scrollY = getScrollY();
+ final int height = getHeight();
+
+ final float marginOffset = (float) mPageMargin / height;
+ 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.heightFactor) * height;
+ offset = ii.offset + ii.heightFactor + marginOffset;
+ } else {
+ float heightFactor = mAdapter.getPageWidth(pos);
+ drawAt = (offset + heightFactor) * height;
+ offset += heightFactor + marginOffset;
+ }
+
+ if (drawAt + mPageMargin > scrollY) {
+ mMarginDrawable.setBounds(mLeftPageBounds, (int) drawAt,
+ mRightPageBounds, (int) (drawAt + mPageMargin + 0.5f));
+ mMarginDrawable.draw(canvas);
+ }
+
+ if (drawAt > scrollY + height) {
+ break; // No more visible, no sense in continuing
+ }
+ }
+ }
+ }
+
+ /**
+ * Start a fake drag of the pager.
+ * <p>
+ * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager
+ * with the touch scrolling of another view, while still letting the ViewPager
+ * 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.
+ * <p>
+ * <p>During a fake drag the ViewPager 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);
+ mInitialMotionY = mLastMotionY = 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.");
+ }
+
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
+ velocityTracker, mActivePointerId);
+ mPopulatePending = true;
+ final int height = getClientHeight();
+ final int scrollY = getScrollY();
+ final ItemInfo ii = infoForCurrentScrollPosition();
+ final int currentPage = ii.position;
+ final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor;
+ final int totalDelta = (int) (mLastMotionY - mInitialMotionY);
+ 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 yOffset Offset in pixels to drag by.
+ * @see #beginFakeDrag()
+ * @see #endFakeDrag()
+ */
+ public void fakeDragBy(float yOffset) {
+ if (!mFakeDragging) {
+ throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
+ }
+
+
+ mLastMotionY += yOffset;
+
+ float oldScrollY = getScrollY();
+ float scrollY = oldScrollY - yOffset;
+ final int height = getClientHeight();
+
+ float topBound = height * mFirstOffset;
+ float bottomBound = height * mLastOffset;
+
+ final ItemInfo firstItem = mItems.get(0);
+ final ItemInfo lastItem = mItems.get(mItems.size() - 1);
+ if (firstItem.position != 0) {
+ topBound = firstItem.offset * height;
+ }
+ if (lastItem.position != mAdapter.getCount() - 1) {
+ bottomBound = lastItem.offset * height;
+ }
+
+ if (scrollY < topBound) {
+ scrollY = topBound;
+ } else if (scrollY > bottomBound) {
+ scrollY = bottomBound;
+ }
+ // Don't lose the rounded component
+ mLastMotionY += scrollY - (int) scrollY;
+ scrollTo(getScrollX(), (int) scrollY);
+ pageScrolled((int) scrollY);
+
+ // Synthesize an event for the VelocityTracker.
+ final long time = SystemClock.uptimeMillis();
+ final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
+ 0, mLastMotionY, 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 = MotionEventCompat.getActionIndex(ev);
+ final int pointerId = MotionEventCompat.getPointerId(ev, 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;
+ mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 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);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean internalCanScrollVertically(int direction) {
+ if (mAdapter == null) {
+ return false;
+ }
+
+ final int height = getClientHeight();
+ final int scrollY = getScrollY();
+ if (direction < 0) {
+ return (scrollY > (int) (height * mFirstOffset));
+ } else if (direction > 0) {
+ return (scrollY < (int) (height * 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 dy 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 dy, 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 (y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
+ x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
+ canScroll(child, true, dy, x + scrollX - child.getLeft(),
+ y + scrollY - child.getTop())) {
+ return true;
+ }
+ }
+
+ }
+
+ return checkV && ViewCompat.canScrollVertically(v, -dy);
+ }
+
+ @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(KeyEvent event) {
+ boolean handled = false;
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ handled = arrowScroll(FOCUS_LEFT);
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ handled = arrowScroll(FOCUS_RIGHT);
+ break;
+ case KeyEvent.KEYCODE_TAB:
+ if (Build.VERSION.SDK_INT >= 11) {
+ // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
+ // before Android 3.0. Ignore the tab key on those devices.
+ if (KeyEvent.metaStateHasNoModifiers(event.getMetaState())) {
+ handled = arrowScroll(FOCUS_FORWARD);
+ } else if (KeyEvent.metaStateHasNoModifiers(event.getMetaState())) {
+ handled = arrowScroll(FOCUS_BACKWARD);
+ }
+ }
+ break;
+ }
+ }
+ return handled;
+ }
+
+ 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_UP) {
+ // 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 nextTop = getChildRectInPagerCoordinates(mTempRect, nextFocused).top;
+ final int currTop = getChildRectInPagerCoordinates(mTempRect, currentFocused).top;
+ if (currentFocused != null && nextTop >= currTop) {
+ handled = pageUp();
+ } else {
+ handled = nextFocused.requestFocus();
+ }
+ } else if (direction == View.FOCUS_DOWN) {
+ final int nextDown =
+ getChildRectInPagerCoordinates(mTempRect, nextFocused).bottom;
+ final int currDown =
+ getChildRectInPagerCoordinates(mTempRect, currentFocused).bottom;
+ if (currentFocused != null && nextDown <= currDown) {
+ handled = pageDown();
+ } else {
+ handled = nextFocused.requestFocus();
+ }
+ }
+ } else if (direction == FOCUS_UP || direction == FOCUS_BACKWARD) {
+ // Trying to move left and nothing there; try to page.
+ handled = pageUp();
+ } else if (direction == FOCUS_DOWN || direction == FOCUS_FORWARD) {
+ // Trying to move right and nothing there; try to page.
+ handled = pageDown();
+ }
+ 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 pageUp() {
+ if (mCurItem > 0) {
+ setCurrentItem(mCurItem - 1, true);
+ return true;
+ }
+ return false;
+ }
+
+ boolean pageDown() {
+ 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<View> 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 ||
+ // No focusable descendants
+ (focusableCount == views.size())) {
+ // 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<View> views) {
+ // Note that we don't call super.addTouchables(), which means that
+ // we don't call View.addTouchables(). This is okay because a ViewPager
+ // 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 &&
+ child.requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ // Dispatch scroll events from this ViewPager.
+ 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(ViewPager.class.getName());
+ final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain();
+ recordCompat.setScrollable(canScroll());
+ if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED
+ && mAdapter != null) {
+ recordCompat.setItemCount(mAdapter.getCount());
+ recordCompat.setFromIndex(mCurItem);
+ recordCompat.setToIndex(mCurItem);
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setClassName(ViewPager.class.getName());
+ info.setScrollable(canScroll());
+ if (internalCanScrollVertically(1)) {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
+ }
+ if (internalCanScrollVertically(-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 (internalCanScrollVertically(1)) {
+ setCurrentItem(mCurItem + 1);
+ return true;
+ }
+ }
+ return false;
+ case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
+ if (internalCanScrollVertically(-1)) {
+ setCurrentItem(mCurItem - 1);
+ return true;
+ }
+ }
+ return false;
+ }
+ return false;
+ }
+
+ private boolean canScroll() {
+ return (mAdapter != null) && (mAdapter.getCount() > 1);
+ }
+ }
+
+ private class PagerObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ dataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ dataSetChanged();
+ }
+ }
+
+ /**
+ * Layout parameters that should be supplied for views added to a
+ * ViewPager.
+ */
+ 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 ViewPager
+ * container; constants are defined in {@link Gravity}.
+ */
+ public int gravity;
+
+ /**
+ * Width as a 0-1 multiplier of the measured pager width
+ */
+ float heightFactor = 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 ViewPager that this view occupies
+ */
+ int childIndex;
+
+ public LayoutParams() {
+ super(FILL_PARENT, FILL_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<View> {
+ @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;
+ }
+ }
+
+ public class ScrollerCustomDuration extends Scroller {
+
+ private double mScrollFactor = 1;
+
+ public ScrollerCustomDuration(Context context) {
+ super(context);
+ }
+
+ public ScrollerCustomDuration(Context context,
+ Interpolator interpolator) {
+ super(context, interpolator);
+ }
+
+ @SuppressLint("NewApi")
+ public ScrollerCustomDuration(Context context,
+ Interpolator interpolator, boolean flywheel) {
+ super(context, interpolator, flywheel);
+ }
+
+ /**
+ * Set the factor by which the duration will change
+ */
+ public void setScrollDurationFactor(double scrollFactor) {
+ mScrollFactor = scrollFactor;
+ }
+
+ @Override
+ public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+ super.startScroll(startX, startY, dx, dy, (int) (duration * mScrollFactor));
+ }
+
+ }
+}