blob: e76302b26d81947a979efa0a8e607621e34c382b [file] [log] [blame]
Alan Viverette75fd8f92015-03-09 15:51:10 -07001/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.widget;
18
19import android.annotation.DrawableRes;
20import android.content.Context;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.database.DataSetObserver;
24import android.graphics.Canvas;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
27import android.os.Bundle;
28import android.os.Parcel;
29import android.os.Parcelable;
Alan Viverette75fd8f92015-03-09 15:51:10 -070030import android.util.AttributeSet;
31import android.util.Log;
Alan Viveretteddf655c2015-04-22 13:43:31 -070032import android.util.MathUtils;
Alan Viverette75fd8f92015-03-09 15:51:10 -070033import android.view.FocusFinder;
34import android.view.Gravity;
35import android.view.KeyEvent;
36import android.view.MotionEvent;
37import android.view.SoundEffectConstants;
38import android.view.VelocityTracker;
39import android.view.View;
40import android.view.ViewConfiguration;
41import android.view.ViewGroup;
42import android.view.ViewParent;
43import android.view.accessibility.AccessibilityEvent;
44import android.view.accessibility.AccessibilityNodeInfo;
Alan Viverette34457f52015-03-25 13:09:20 -070045import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Alan Viverette75fd8f92015-03-09 15:51:10 -070046import android.view.animation.Interpolator;
47import android.widget.EdgeEffect;
48import android.widget.Scroller;
49
Maxim Bogatovac6ffce2015-04-27 13:45:52 -070050import com.android.internal.R;
51
Alan Viverette75fd8f92015-03-09 15:51:10 -070052import java.util.ArrayList;
53import java.util.Collections;
54import java.util.Comparator;
55
56/**
57 * Layout manager that allows the user to flip left and right
58 * through pages of data. You supply an implementation of a
59 * {@link android.support.v4.view.PagerAdapter} to generate the pages that the view shows.
60 *
61 * <p>Note this class is currently under early design and
62 * development. The API will likely change in later updates of
63 * the compatibility library, requiring changes to the source code
64 * of apps when they are compiled against the newer version.</p>
65 *
66 * <p>ViewPager is most often used in conjunction with {@link android.app.Fragment},
67 * which is a convenient way to supply and manage the lifecycle of each page.
68 * There are standard adapters implemented for using fragments with the ViewPager,
69 * which cover the most common use cases. These are
70 * {@link android.support.v4.app.FragmentPagerAdapter} and
71 * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these
72 * classes have simple code showing how to build a full user interface
73 * with them.
74 *
75 * <p>For more information about how to use ViewPager, read <a
76 * href="{@docRoot}training/implementing-navigation/lateral.html">Creating Swipe Views with
77 * Tabs</a>.</p>
78 *
79 * <p>Below is a more complicated example of ViewPager, using it in conjunction
80 * with {@link android.app.ActionBar} tabs. You can find other examples of using
81 * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code.
82 *
83 * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java
84 * complete}
85 */
86public class ViewPager extends ViewGroup {
87 private static final String TAG = "ViewPager";
Alan Viveretteddf655c2015-04-22 13:43:31 -070088 private static final boolean DEBUG = true;
Alan Viverette75fd8f92015-03-09 15:51:10 -070089
Alan Viveretteddf655c2015-04-22 13:43:31 -070090 private static final int MAX_SCROLL_X = 2 << 23;
Alan Viverette75fd8f92015-03-09 15:51:10 -070091 private static final boolean USE_CACHE = false;
92
93 private static final int DEFAULT_OFFSCREEN_PAGES = 1;
94 private static final int MAX_SETTLE_DURATION = 600; // ms
95 private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
96
97 private static final int DEFAULT_GUTTER_SIZE = 16; // dips
98
99 private static final int MIN_FLING_VELOCITY = 400; // dips
100
101 private static final int[] LAYOUT_ATTRS = new int[] {
102 com.android.internal.R.attr.layout_gravity
103 };
104
105 /**
106 * Used to track what the expected number of items in the adapter should be.
107 * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
108 */
109 private int mExpectedAdapterCount;
110
111 static class ItemInfo {
112 Object object;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700113 boolean scrolling;
114 float widthFactor;
Alan Viveretteddf655c2015-04-22 13:43:31 -0700115
116 /** Logical position of the item within the pager adapter. */
117 int position;
118
119 /** Offset between the starting edges of the item and its container. */
Alan Viverette75fd8f92015-03-09 15:51:10 -0700120 float offset;
121 }
122
123 private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){
124 @Override
125 public int compare(ItemInfo lhs, ItemInfo rhs) {
126 return lhs.position - rhs.position;
127 }
128 };
129
130 private static final Interpolator sInterpolator = new Interpolator() {
131 public float getInterpolation(float t) {
132 t -= 1.0f;
133 return t * t * t * t * t + 1.0f;
134 }
135 };
136
137 private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
138 private final ItemInfo mTempItem = new ItemInfo();
139
140 private final Rect mTempRect = new Rect();
141
142 private PagerAdapter mAdapter;
143 private int mCurItem; // Index of currently displayed page.
144 private int mRestoredCurItem = -1;
145 private Parcelable mRestoredAdapterState = null;
146 private ClassLoader mRestoredClassLoader = null;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700147 private final Scroller mScroller;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700148 private PagerObserver mObserver;
149
150 private int mPageMargin;
151 private Drawable mMarginDrawable;
152 private int mTopPageBounds;
153 private int mBottomPageBounds;
154
Alan Viveretteddf655c2015-04-22 13:43:31 -0700155 /**
156 * The increment used to move in the "left" direction. Dependent on layout
157 * direction.
158 */
159 private int mLeftIncr = -1;
160
Alan Viverette75fd8f92015-03-09 15:51:10 -0700161 // Offsets of the first and last items, if known.
162 // Set during population, used to determine if we are at the beginning
163 // or end of the pager data set during touch scrolling.
164 private float mFirstOffset = -Float.MAX_VALUE;
165 private float mLastOffset = Float.MAX_VALUE;
166
167 private int mChildWidthMeasureSpec;
168 private int mChildHeightMeasureSpec;
169 private boolean mInLayout;
170
171 private boolean mScrollingCacheEnabled;
172
173 private boolean mPopulatePending;
174 private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
175
176 private boolean mIsBeingDragged;
177 private boolean mIsUnableToDrag;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700178 private final int mDefaultGutterSize;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700179 private int mGutterSize;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700180 private final int mTouchSlop;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700181 /**
182 * Position of the last motion event.
183 */
184 private float mLastMotionX;
185 private float mLastMotionY;
186 private float mInitialMotionX;
187 private float mInitialMotionY;
188 /**
189 * ID of the active pointer. This is used to retain consistency during
190 * drags/flings if multiple pointers are used.
191 */
192 private int mActivePointerId = INVALID_POINTER;
193 /**
194 * Sentinel value for no current active pointer.
195 * Used by {@link #mActivePointerId}.
196 */
197 private static final int INVALID_POINTER = -1;
198
199 /**
200 * Determines speed during touch scrolling
201 */
202 private VelocityTracker mVelocityTracker;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700203 private final int mMinimumVelocity;
204 private final int mMaximumVelocity;
205 private final int mFlingDistance;
206 private final int mCloseEnough;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700207
208 // If the pager is at least this close to its final position, complete the scroll
209 // on touch down and let the user interact with the content inside instead of
210 // "catching" the flinging pager.
211 private static final int CLOSE_ENOUGH = 2; // dp
212
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700213 private final EdgeEffect mLeftEdge;
214 private final EdgeEffect mRightEdge;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700215
216 private boolean mFirstLayout = true;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700217 private boolean mCalledSuper;
218 private int mDecorChildCount;
219
220 private OnPageChangeListener mOnPageChangeListener;
221 private OnPageChangeListener mInternalPageChangeListener;
222 private OnAdapterChangeListener mAdapterChangeListener;
223 private PageTransformer mPageTransformer;
224
225 private static final int DRAW_ORDER_DEFAULT = 0;
226 private static final int DRAW_ORDER_FORWARD = 1;
227 private static final int DRAW_ORDER_REVERSE = 2;
228 private int mDrawingOrder;
229 private ArrayList<View> mDrawingOrderedChildren;
230 private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();
231
232 /**
233 * Indicates that the pager is in an idle, settled state. The current page
234 * is fully in view and no animation is in progress.
235 */
236 public static final int SCROLL_STATE_IDLE = 0;
237
238 /**
239 * Indicates that the pager is currently being dragged by the user.
240 */
241 public static final int SCROLL_STATE_DRAGGING = 1;
242
243 /**
244 * Indicates that the pager is in the process of settling to a final position.
245 */
246 public static final int SCROLL_STATE_SETTLING = 2;
247
248 private final Runnable mEndScrollRunnable = new Runnable() {
249 public void run() {
250 setScrollState(SCROLL_STATE_IDLE);
251 populate();
252 }
253 };
254
255 private int mScrollState = SCROLL_STATE_IDLE;
256
257 /**
258 * Callback interface for responding to changing state of the selected page.
259 */
260 public interface OnPageChangeListener {
261
262 /**
263 * This method will be invoked when the current page is scrolled, either as part
264 * of a programmatically initiated smooth scroll or a user initiated touch scroll.
265 *
266 * @param position Position index of the first page currently being displayed.
267 * Page position+1 will be visible if positionOffset is nonzero.
268 * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
269 * @param positionOffsetPixels Value in pixels indicating the offset from position.
270 */
271 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
272
273 /**
274 * This method will be invoked when a new page becomes selected. Animation is not
275 * necessarily complete.
276 *
277 * @param position Position index of the new selected page.
278 */
279 public void onPageSelected(int position);
280
281 /**
282 * Called when the scroll state changes. Useful for discovering when the user
283 * begins dragging, when the pager is automatically settling to the current page,
284 * or when it is fully stopped/idle.
285 *
286 * @param state The new scroll state.
287 * @see com.android.internal.widget.ViewPager#SCROLL_STATE_IDLE
288 * @see com.android.internal.widget.ViewPager#SCROLL_STATE_DRAGGING
289 * @see com.android.internal.widget.ViewPager#SCROLL_STATE_SETTLING
290 */
291 public void onPageScrollStateChanged(int state);
292 }
293
294 /**
295 * Simple implementation of the {@link OnPageChangeListener} interface with stub
296 * implementations of each method. Extend this if you do not intend to override
297 * every method of {@link OnPageChangeListener}.
298 */
299 public static class SimpleOnPageChangeListener implements OnPageChangeListener {
300 @Override
301 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
302 // This space for rent
303 }
304
305 @Override
306 public void onPageSelected(int position) {
307 // This space for rent
308 }
309
310 @Override
311 public void onPageScrollStateChanged(int state) {
312 // This space for rent
313 }
314 }
315
316 /**
317 * A PageTransformer is invoked whenever a visible/attached page is scrolled.
318 * This offers an opportunity for the application to apply a custom transformation
319 * to the page views using animation properties.
320 *
321 * <p>As property animation is only supported as of Android 3.0 and forward,
322 * setting a PageTransformer on a ViewPager on earlier platform versions will
323 * be ignored.</p>
324 */
325 public interface PageTransformer {
326 /**
327 * Apply a property transformation to the given page.
328 *
329 * @param page Apply the transformation to this page
330 * @param position Position of page relative to the current front-and-center
331 * position of the pager. 0 is front and center. 1 is one full
332 * page position to the right, and -1 is one page position to the left.
333 */
334 public void transformPage(View page, float position);
335 }
336
337 /**
338 * Used internally to monitor when adapters are switched.
339 */
340 interface OnAdapterChangeListener {
341 public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter);
342 }
343
344 /**
345 * Used internally to tag special types of child views that should be added as
346 * pager decorations by default.
347 */
348 interface Decor {}
349
350 public ViewPager(Context context) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700351 this(context, null);
Alan Viverette75fd8f92015-03-09 15:51:10 -0700352 }
353
354 public ViewPager(Context context, AttributeSet attrs) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700355 this(context, attrs, 0);
Alan Viverette75fd8f92015-03-09 15:51:10 -0700356 }
357
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700358 public ViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
359 this(context, attrs, defStyleAttr, 0);
360 }
361
362 public ViewPager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
363 super(context, attrs, defStyleAttr, defStyleRes);
364
Alan Viverette75fd8f92015-03-09 15:51:10 -0700365 setWillNotDraw(false);
366 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
367 setFocusable(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700368
Alan Viverette75fd8f92015-03-09 15:51:10 -0700369 mScroller = new Scroller(context, sInterpolator);
370 final ViewConfiguration configuration = ViewConfiguration.get(context);
371 final float density = context.getResources().getDisplayMetrics().density;
372
373 mTouchSlop = configuration.getScaledPagingTouchSlop();
374 mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
375 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
376 mLeftEdge = new EdgeEffect(context);
377 mRightEdge = new EdgeEffect(context);
378
379 mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
380 mCloseEnough = (int) (CLOSE_ENOUGH * density);
381 mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);
382
Alan Viverette75fd8f92015-03-09 15:51:10 -0700383 if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
384 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
385 }
386 }
387
388 @Override
389 protected void onDetachedFromWindow() {
390 removeCallbacks(mEndScrollRunnable);
391 super.onDetachedFromWindow();
392 }
393
394 private void setScrollState(int newState) {
395 if (mScrollState == newState) {
396 return;
397 }
398
399 mScrollState = newState;
400 if (mPageTransformer != null) {
401 // PageTransformers can do complex things that benefit from hardware layers.
402 enableLayers(newState != SCROLL_STATE_IDLE);
403 }
404 if (mOnPageChangeListener != null) {
405 mOnPageChangeListener.onPageScrollStateChanged(newState);
406 }
407 }
408
409 /**
410 * Set a PagerAdapter that will supply views for this pager as needed.
411 *
412 * @param adapter Adapter to use
413 */
414 public void setAdapter(PagerAdapter adapter) {
415 if (mAdapter != null) {
416 mAdapter.unregisterDataSetObserver(mObserver);
417 mAdapter.startUpdate(this);
418 for (int i = 0; i < mItems.size(); i++) {
419 final ItemInfo ii = mItems.get(i);
420 mAdapter.destroyItem(this, ii.position, ii.object);
421 }
422 mAdapter.finishUpdate(this);
423 mItems.clear();
424 removeNonDecorViews();
425 mCurItem = 0;
426 scrollTo(0, 0);
427 }
428
429 final PagerAdapter oldAdapter = mAdapter;
430 mAdapter = adapter;
431 mExpectedAdapterCount = 0;
432
433 if (mAdapter != null) {
434 if (mObserver == null) {
435 mObserver = new PagerObserver();
436 }
437 mAdapter.registerDataSetObserver(mObserver);
438 mPopulatePending = false;
439 final boolean wasFirstLayout = mFirstLayout;
440 mFirstLayout = true;
441 mExpectedAdapterCount = mAdapter.getCount();
442 if (mRestoredCurItem >= 0) {
443 mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
444 setCurrentItemInternal(mRestoredCurItem, false, true);
445 mRestoredCurItem = -1;
446 mRestoredAdapterState = null;
447 mRestoredClassLoader = null;
448 } else if (!wasFirstLayout) {
449 populate();
450 } else {
451 requestLayout();
452 }
453 }
454
455 if (mAdapterChangeListener != null && oldAdapter != adapter) {
456 mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
457 }
458 }
459
460 private void removeNonDecorViews() {
461 for (int i = 0; i < getChildCount(); i++) {
462 final View child = getChildAt(i);
463 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
464 if (!lp.isDecor) {
465 removeViewAt(i);
466 i--;
467 }
468 }
469 }
470
471 /**
472 * Retrieve the current adapter supplying pages.
473 *
474 * @return The currently registered PagerAdapter
475 */
476 public PagerAdapter getAdapter() {
477 return mAdapter;
478 }
479
480 void setOnAdapterChangeListener(OnAdapterChangeListener listener) {
481 mAdapterChangeListener = listener;
482 }
483
Alan Viveretteddf655c2015-04-22 13:43:31 -0700484 private int getPaddedWidth() {
Alan Viverette75fd8f92015-03-09 15:51:10 -0700485 return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
486 }
487
488 /**
489 * Set the currently selected page. If the ViewPager has already been through its first
490 * layout with its current adapter there will be a smooth animated transition between
491 * the current item and the specified item.
492 *
493 * @param item Item index to select
494 */
495 public void setCurrentItem(int item) {
496 mPopulatePending = false;
497 setCurrentItemInternal(item, !mFirstLayout, false);
498 }
499
500 /**
501 * Set the currently selected page.
502 *
503 * @param item Item index to select
504 * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
505 */
506 public void setCurrentItem(int item, boolean smoothScroll) {
507 mPopulatePending = false;
508 setCurrentItemInternal(item, smoothScroll, false);
509 }
510
511 public int getCurrentItem() {
512 return mCurItem;
513 }
514
Alan Viveretteddf655c2015-04-22 13:43:31 -0700515 boolean setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
516 return setCurrentItemInternal(item, smoothScroll, always, 0);
Alan Viverette75fd8f92015-03-09 15:51:10 -0700517 }
518
Alan Viveretteddf655c2015-04-22 13:43:31 -0700519 boolean setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
Alan Viverette75fd8f92015-03-09 15:51:10 -0700520 if (mAdapter == null || mAdapter.getCount() <= 0) {
521 setScrollingCacheEnabled(false);
Alan Viveretteddf655c2015-04-22 13:43:31 -0700522 return false;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700523 }
524
Alan Viveretteddf655c2015-04-22 13:43:31 -0700525 item = MathUtils.constrain(item, 0, mAdapter.getCount() - 1);
526 if (!always && mCurItem == item && mItems.size() != 0) {
527 setScrollingCacheEnabled(false);
528 return false;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700529 }
Alan Viveretteddf655c2015-04-22 13:43:31 -0700530
Alan Viverette75fd8f92015-03-09 15:51:10 -0700531 final int pageLimit = mOffscreenPageLimit;
532 if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
533 // We are doing a jump by more than one page. To avoid
534 // glitches, we want to keep all current pages in the view
535 // until the scroll ends.
Alan Viveretteddf655c2015-04-22 13:43:31 -0700536 for (int i = 0; i < mItems.size(); i++) {
Alan Viverette75fd8f92015-03-09 15:51:10 -0700537 mItems.get(i).scrolling = true;
538 }
539 }
Alan Viverette75fd8f92015-03-09 15:51:10 -0700540
Alan Viveretteddf655c2015-04-22 13:43:31 -0700541 final boolean dispatchSelected = mCurItem != item;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700542 if (mFirstLayout) {
543 // We don't have any idea how big we are yet and shouldn't have any pages either.
544 // Just set things up and let the pending layout handle things.
545 mCurItem = item;
546 if (dispatchSelected && mOnPageChangeListener != null) {
547 mOnPageChangeListener.onPageSelected(item);
548 }
549 if (dispatchSelected && mInternalPageChangeListener != null) {
550 mInternalPageChangeListener.onPageSelected(item);
551 }
552 requestLayout();
553 } else {
554 populate(item);
555 scrollToItem(item, smoothScroll, velocity, dispatchSelected);
556 }
Alan Viveretteddf655c2015-04-22 13:43:31 -0700557
558 return true;
Alan Viverette75fd8f92015-03-09 15:51:10 -0700559 }
560
Alan Viveretteddf655c2015-04-22 13:43:31 -0700561 private void scrollToItem(int position, boolean smoothScroll, int velocity,
Alan Viverette75fd8f92015-03-09 15:51:10 -0700562 boolean dispatchSelected) {
Alan Viveretteddf655c2015-04-22 13:43:31 -0700563 final int destX = getLeftEdgeForItem(position);
564
Alan Viverette75fd8f92015-03-09 15:51:10 -0700565 if (smoothScroll) {
566 smoothScrollTo(destX, 0, velocity);
Alan Viveretteddf655c2015-04-22 13:43:31 -0700567
Alan Viverette75fd8f92015-03-09 15:51:10 -0700568 if (dispatchSelected && mOnPageChangeListener != null) {
Alan Viveretteddf655c2015-04-22 13:43:31 -0700569 mOnPageChangeListener.onPageSelected(position);
Alan Viverette75fd8f92015-03-09 15:51:10 -0700570 }
571 if (dispatchSelected && mInternalPageChangeListener != null) {
Alan Viveretteddf655c2015-04-22 13:43:31 -0700572 mInternalPageChangeListener.onPageSelected(position);
Alan Viverette75fd8f92015-03-09 15:51:10 -0700573 }
574 } else {
575 if (dispatchSelected && mOnPageChangeListener != null) {
Alan Viveretteddf655c2015-04-22 13:43:31 -0700576 mOnPageChangeListener.onPageSelected(position);
Alan Viverette75fd8f92015-03-09 15:51:10 -0700577 }
578 if (dispatchSelected && mInternalPageChangeListener != null) {
Alan Viveretteddf655c2015-04-22 13:43:31 -0700579 mInternalPageChangeListener.onPageSelected(position);
Alan Viverette75fd8f92015-03-09 15:51:10 -0700580 }
Alan Viveretteddf655c2015-04-22 13:43:31 -0700581
Alan Viverette75fd8f92015-03-09 15:51:10 -0700582 completeScroll(false);
583 scrollTo(destX, 0);
584 pageScrolled(destX);
585 }
586 }
587
Alan Viveretteddf655c2015-04-22 13:43:31 -0700588 private int getLeftEdgeForItem(int position) {
589 final ItemInfo info = infoForPosition(position);
590 if (info == null) {
591 return 0;
592 }
593
594 final int width = getPaddedWidth();
595 final int scaledOffset = (int) (width * MathUtils.constrain(
596 info.offset, mFirstOffset, mLastOffset));
597
598 if (isLayoutRtl()) {
599 final int itemWidth = (int) (width * info.widthFactor + 0.5f);
600 return MAX_SCROLL_X - itemWidth - scaledOffset;
601 } else {
602 return scaledOffset;
603 }
604 }
605
Alan Viverette75fd8f92015-03-09 15:51:10 -0700606 /**
607 * Set a listener that will be invoked whenever the page changes or is incrementally
608 * scrolled. See {@link OnPageChangeListener}.
609 *
610 * @param listener Listener to set
611 */
612 public void setOnPageChangeListener(OnPageChangeListener listener) {
613 mOnPageChangeListener = listener;
614 }
615
616 /**
617 * Set a {@link PageTransformer} that will be called for each attached page whenever
618 * the scroll position is changed. This allows the application to apply custom property
619 * transformations to each page, overriding the default sliding look and feel.
620 *
621 * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.
622 * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p>
623 *
624 * @param reverseDrawingOrder true if the supplied PageTransformer requires page views
625 * to be drawn from last to first instead of first to last.
626 * @param transformer PageTransformer that will modify each page's animation properties
627 */
628 public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) {
629 final boolean hasTransformer = transformer != null;
630 final boolean needsPopulate = hasTransformer != (mPageTransformer != null);
631 mPageTransformer = transformer;
632 setChildrenDrawingOrderEnabled(hasTransformer);
633 if (hasTransformer) {
634 mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;
635 } else {
636 mDrawingOrder = DRAW_ORDER_DEFAULT;
637 }
638 if (needsPopulate) populate();
639 }
640
641 @Override
642 protected int getChildDrawingOrder(int childCount, int i) {
643 final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i;
644 final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex;
645 return result;
646 }
647
648 /**
649 * Set a separate OnPageChangeListener for internal use by the support library.
650 *
651 * @param listener Listener to set
652 * @return The old listener that was set, if any.
653 */
654 OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) {
655 OnPageChangeListener oldListener = mInternalPageChangeListener;
656 mInternalPageChangeListener = listener;
657 return oldListener;
658 }
659
660 /**
661 * Returns the number of pages that will be retained to either side of the
662 * current page in the view hierarchy in an idle state. Defaults to 1.
663 *
664 * @return How many pages will be kept offscreen on either side
665 * @see #setOffscreenPageLimit(int)
666 */
667 public int getOffscreenPageLimit() {
668 return mOffscreenPageLimit;
669 }
670
671 /**
672 * Set the number of pages that should be retained to either side of the
673 * current page in the view hierarchy in an idle state. Pages beyond this
674 * limit will be recreated from the adapter when needed.
675 *
676 * <p>This is offered as an optimization. If you know in advance the number
677 * of pages you will need to support or have lazy-loading mechanisms in place
678 * on your pages, tweaking this setting can have benefits in perceived smoothness
679 * of paging animations and interaction. If you have a small number of pages (3-4)
680 * that you can keep active all at once, less time will be spent in layout for
681 * newly created view subtrees as the user pages back and forth.</p>
682 *
683 * <p>You should keep this limit low, especially if your pages have complex layouts.
684 * This setting defaults to 1.</p>
685 *
686 * @param limit How many pages will be kept offscreen in an idle state.
687 */
688 public void setOffscreenPageLimit(int limit) {
689 if (limit < DEFAULT_OFFSCREEN_PAGES) {
690 Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
691 DEFAULT_OFFSCREEN_PAGES);
692 limit = DEFAULT_OFFSCREEN_PAGES;
693 }
694 if (limit != mOffscreenPageLimit) {
695 mOffscreenPageLimit = limit;
696 populate();
697 }
698 }
699
700 /**
701 * Set the margin between pages.
702 *
703 * @param marginPixels Distance between adjacent pages in pixels
704 * @see #getPageMargin()
705 * @see #setPageMarginDrawable(android.graphics.drawable.Drawable)
706 * @see #setPageMarginDrawable(int)
707 */
708 public void setPageMargin(int marginPixels) {
709 final int oldMargin = mPageMargin;
710 mPageMargin = marginPixels;
711
712 final int width = getWidth();
713 recomputeScrollPosition(width, width, marginPixels, oldMargin);
714
715 requestLayout();
716 }
717
718 /**
719 * Return the margin between pages.
720 *
721 * @return The size of the margin in pixels
722 */
723 public int getPageMargin() {
724 return mPageMargin;
725 }
726
727 /**
728 * Set a drawable that will be used to fill the margin between pages.
729 *
730 * @param d Drawable to display between pages
731 */
732 public void setPageMarginDrawable(Drawable d) {
733 mMarginDrawable = d;
734 if (d != null) refreshDrawableState();
735 setWillNotDraw(d == null);
736 invalidate();
737 }
738
739 /**
740 * Set a drawable that will be used to fill the margin between pages.
741 *
742 * @param resId Resource ID of a drawable to display between pages
743 */
744 public void setPageMarginDrawable(@DrawableRes int resId) {
745 setPageMarginDrawable(getContext().getDrawable(resId));
746 }
747
748 @Override
749 protected boolean verifyDrawable(Drawable who) {
750 return super.verifyDrawable(who) || who == mMarginDrawable;
751 }
752
753 @Override
754 protected void drawableStateChanged() {
755 super.drawableStateChanged();
756 final Drawable d = mMarginDrawable;
757 if (d != null && d.isStateful()) {
758 d.setState(getDrawableState());
759 }
760 }
761
762 // We want the duration of the page snap animation to be influenced by the distance that
763 // the screen has to travel, however, we don't want this duration to be effected in a
764 // purely linear fashion. Instead, we use this method to moderate the effect that the distance
765 // of travel has on the overall snap duration.
766 float distanceInfluenceForSnapDuration(float f) {
767 f -= 0.5f; // center the values about 0.
768 f *= 0.3f * Math.PI / 2.0f;
769 return (float) Math.sin(f);
770 }
771
772 /**
773 * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
774 *
775 * @param x the number of pixels to scroll by on the X axis
776 * @param y the number of pixels to scroll by on the Y axis
777 */
778 void smoothScrollTo(int x, int y) {
779 smoothScrollTo(x, y, 0);
780 }
781
782 /**
783 * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
784 *
785 * @param x the number of pixels to scroll by on the X axis
786 * @param y the number of pixels to scroll by on the Y axis
787 * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
788 */
789 void smoothScrollTo(int x, int y, int velocity) {
790 if (getChildCount() == 0) {
791 // Nothing to do.
792 setScrollingCacheEnabled(false);
793 return;
794 }
795 int sx = getScrollX();
796 int sy = getScrollY();
797 int dx = x - sx;
798 int dy = y - sy;
799 if (dx == 0 && dy == 0) {
800 completeScroll(false);
801 populate();
802 setScrollState(SCROLL_STATE_IDLE);
803 return;
804 }
805
806 setScrollingCacheEnabled(true);
807 setScrollState(SCROLL_STATE_SETTLING);
808
Alan Viveretteddf655c2015-04-22 13:43:31 -0700809 final int width = getPaddedWidth();
Alan Viverette75fd8f92015-03-09 15:51:10 -0700810 final int halfWidth = width / 2;
811 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
812 final float distance = halfWidth + halfWidth *
813 distanceInfluenceForSnapDuration(distanceRatio);
814
815 int duration = 0;
816 velocity = Math.abs(velocity);
817 if (velocity > 0) {
818 duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
819 } else {
820 final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
821 final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
822 duration = (int) ((pageDelta + 1) * 100);
823 }
824 duration = Math.min(duration, MAX_SETTLE_DURATION);
825
826 mScroller.startScroll(sx, sy, dx, dy, duration);
827 postInvalidateOnAnimation();
828 }
829
830 ItemInfo addNewItem(int position, int index) {
831 ItemInfo ii = new ItemInfo();
832 ii.position = position;
833 ii.object = mAdapter.instantiateItem(this, position);
834 ii.widthFactor = mAdapter.getPageWidth(position);
835 if (index < 0 || index >= mItems.size()) {
836 mItems.add(ii);
837 } else {
838 mItems.add(index, ii);
839 }
840 return ii;
841 }
842
843 void dataSetChanged() {
844 // This method only gets called if our observer is attached, so mAdapter is non-null.
845
846 final int adapterCount = mAdapter.getCount();
847 mExpectedAdapterCount = adapterCount;
848 boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
849 mItems.size() < adapterCount;
850 int newCurrItem = mCurItem;
851
852 boolean isUpdating = false;
853 for (int i = 0; i < mItems.size(); i++) {
854 final ItemInfo ii = mItems.get(i);
855 final int newPos = mAdapter.getItemPosition(ii.object);
856
857 if (newPos == PagerAdapter.POSITION_UNCHANGED) {
858 continue;
859 }
860
861 if (newPos == PagerAdapter.POSITION_NONE) {
862 mItems.remove(i);
863 i--;
864
865 if (!isUpdating) {
866 mAdapter.startUpdate(this);
867 isUpdating = true;
868 }
869
870 mAdapter.destroyItem(this, ii.position, ii.object);
871 needPopulate = true;
872
873 if (mCurItem == ii.position) {
874 // Keep the current item in the valid range
875 newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
876 needPopulate = true;
877 }
878 continue;
879 }
880
881 if (ii.position != newPos) {
882 if (ii.position == mCurItem) {
883 // Our current item changed position. Follow it.
884 newCurrItem = newPos;
885 }
886
887 ii.position = newPos;
888 needPopulate = true;
889 }
890 }
891
892 if (isUpdating) {
893 mAdapter.finishUpdate(this);
894 }
895
896 Collections.sort(mItems, COMPARATOR);
897
898 if (needPopulate) {
899 // Reset our known page widths; populate will recompute them.
900 final int childCount = getChildCount();
901 for (int i = 0; i < childCount; i++) {
902 final View child = getChildAt(i);
903 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
904 if (!lp.isDecor) {
905 lp.widthFactor = 0.f;
906 }
907 }
908
909 setCurrentItemInternal(newCurrItem, false, true);
910 requestLayout();
911 }
912 }
913
Alan Viverette816aa142015-04-10 15:41:10 -0700914 public void populate() {
Alan Viverette75fd8f92015-03-09 15:51:10 -0700915 populate(mCurItem);
916 }
917
918 void populate(int newCurrentItem) {
919 ItemInfo oldCurInfo = null;
920 int focusDirection = View.FOCUS_FORWARD;
921 if (mCurItem != newCurrentItem) {
922 focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
923 oldCurInfo = infoForPosition(mCurItem);
924 mCurItem = newCurrentItem;
925 }
926
927 if (mAdapter == null) {
928 sortChildDrawingOrder();
929 return;
930 }
931
932 // Bail now if we are waiting to populate. This is to hold off
933 // on creating views from the time the user releases their finger to
934 // fling to a new position until we have finished the scroll to
935 // that position, avoiding glitches from happening at that point.
936 if (mPopulatePending) {
937 if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
938 sortChildDrawingOrder();
939 return;
940 }
941
942 // Also, don't populate until we are attached to a window. This is to
943 // avoid trying to populate before we have restored our view hierarchy
944 // state and conflicting with what is restored.
945 if (getWindowToken() == null) {
946 return;
947 }
948
949 mAdapter.startUpdate(this);
950
951 final int pageLimit = mOffscreenPageLimit;
952 final int startPos = Math.max(0, mCurItem - pageLimit);
953 final int N = mAdapter.getCount();
954 final int endPos = Math.min(N-1, mCurItem + pageLimit);
955
956 if (N != mExpectedAdapterCount) {
957 String resName;
958 try {
959 resName = getResources().getResourceName(getId());
960 } catch (Resources.NotFoundException e) {
961 resName = Integer.toHexString(getId());
962 }
963 throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
964 " contents without calling PagerAdapter#notifyDataSetChanged!" +
965 " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
966 " Pager id: " + resName +
967 " Pager class: " + getClass() +
968 " Problematic adapter: " + mAdapter.getClass());
969 }
970
971 // Locate the currently focused item or add it if needed.
972 int curIndex = -1;
973 ItemInfo curItem = null;
974 for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
975 final ItemInfo ii = mItems.get(curIndex);
976 if (ii.position >= mCurItem) {
977 if (ii.position == mCurItem) curItem = ii;
978 break;
979 }
980 }
981
982 if (curItem == null && N > 0) {
983 curItem = addNewItem(mCurItem, curIndex);
984 }
985
986 // Fill 3x the available width or up to the number of offscreen
987 // pages requested to either side, whichever is larger.
988 // If we have no current item we have no work to do.
989 if (curItem != null) {
990 float extraWidthLeft = 0.f;
991 int itemIndex = curIndex - 1;
992 ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
Alan Viveretteddf655c2015-04-22 13:43:31 -0700993 final int clientWidth = getPaddedWidth();
Alan Viverette75fd8f92015-03-09 15:51:10 -0700994 final float leftWidthNeeded = clientWidth <= 0 ? 0 :
995 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
996 for (int pos = mCurItem - 1; pos >= 0; pos--) {
997 if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
998 if (ii == null) {
999 break;
1000 }
1001 if (pos == ii.position && !ii.scrolling) {
1002 mItems.remove(itemIndex);
1003 mAdapter.destroyItem(this, pos, ii.object);
1004 if (DEBUG) {
1005 Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
Alan Viveretteddf655c2015-04-22 13:43:31 -07001006 " view: " + ii.object);
Alan Viverette75fd8f92015-03-09 15:51:10 -07001007 }
1008 itemIndex--;
1009 curIndex--;
1010 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
1011 }
1012 } else if (ii != null && pos == ii.position) {
1013 extraWidthLeft += ii.widthFactor;
1014 itemIndex--;
1015 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
1016 } else {
1017 ii = addNewItem(pos, itemIndex + 1);
1018 extraWidthLeft += ii.widthFactor;
1019 curIndex++;
1020 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
1021 }
1022 }
1023
1024 float extraWidthRight = curItem.widthFactor;
1025 itemIndex = curIndex + 1;
1026 if (extraWidthRight < 2.f) {
1027 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
1028 final float rightWidthNeeded = clientWidth <= 0 ? 0 :
1029 (float) getPaddingRight() / (float) clientWidth + 2.f;
1030 for (int pos = mCurItem + 1; pos < N; pos++) {
1031 if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
1032 if (ii == null) {
1033 break;
1034 }
1035 if (pos == ii.position && !ii.scrolling) {
1036 mItems.remove(itemIndex);
1037 mAdapter.destroyItem(this, pos, ii.object);
1038 if (DEBUG) {
1039 Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
Alan Viveretteddf655c2015-04-22 13:43:31 -07001040 " view: " + ii.object);
Alan Viverette75fd8f92015-03-09 15:51:10 -07001041 }
1042 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
1043 }
1044 } else if (ii != null && pos == ii.position) {
1045 extraWidthRight += ii.widthFactor;
1046 itemIndex++;
1047 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
1048 } else {
1049 ii = addNewItem(pos, itemIndex);
1050 itemIndex++;
1051 extraWidthRight += ii.widthFactor;
1052 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
1053 }
1054 }
1055 }
1056
1057 calculatePageOffsets(curItem, curIndex, oldCurInfo);
1058 }
1059
1060 if (DEBUG) {
1061 Log.i(TAG, "Current page list:");
1062 for (int i=0; i<mItems.size(); i++) {
1063 Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
1064 }
1065 }
1066
1067 mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
1068
1069 mAdapter.finishUpdate(this);
1070
1071 // Check width measurement of current pages and drawing sort order.
1072 // Update LayoutParams as needed.
1073 final int childCount = getChildCount();
1074 for (int i = 0; i < childCount; i++) {
1075 final View child = getChildAt(i);
1076 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1077 lp.childIndex = i;
1078 if (!lp.isDecor && lp.widthFactor == 0.f) {
1079 // 0 means requery the adapter for this, it doesn't have a valid width.
1080 final ItemInfo ii = infoForChild(child);
1081 if (ii != null) {
1082 lp.widthFactor = ii.widthFactor;
1083 lp.position = ii.position;
1084 }
1085 }
1086 }
1087 sortChildDrawingOrder();
1088
1089 if (hasFocus()) {
1090 View currentFocused = findFocus();
1091 ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
1092 if (ii == null || ii.position != mCurItem) {
1093 for (int i=0; i<getChildCount(); i++) {
1094 View child = getChildAt(i);
1095 ii = infoForChild(child);
1096 if (ii != null && ii.position == mCurItem) {
1097 if (child.requestFocus(focusDirection)) {
1098 break;
1099 }
1100 }
1101 }
1102 }
1103 }
1104 }
1105
1106 private void sortChildDrawingOrder() {
1107 if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
1108 if (mDrawingOrderedChildren == null) {
1109 mDrawingOrderedChildren = new ArrayList<View>();
1110 } else {
1111 mDrawingOrderedChildren.clear();
1112 }
1113 final int childCount = getChildCount();
1114 for (int i = 0; i < childCount; i++) {
1115 final View child = getChildAt(i);
1116 mDrawingOrderedChildren.add(child);
1117 }
1118 Collections.sort(mDrawingOrderedChildren, sPositionComparator);
1119 }
1120 }
1121
1122 private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
1123 final int N = mAdapter.getCount();
Alan Viveretteddf655c2015-04-22 13:43:31 -07001124 final int width = getPaddedWidth();
Alan Viverette75fd8f92015-03-09 15:51:10 -07001125 final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
Alan Viveretteddf655c2015-04-22 13:43:31 -07001126
Alan Viverette75fd8f92015-03-09 15:51:10 -07001127 // Fix up offsets for later layout.
1128 if (oldCurInfo != null) {
1129 final int oldCurPosition = oldCurInfo.position;
Alan Viveretteddf655c2015-04-22 13:43:31 -07001130
Alan Viverette75fd8f92015-03-09 15:51:10 -07001131 // Base offsets off of oldCurInfo.
1132 if (oldCurPosition < curItem.position) {
1133 int itemIndex = 0;
Alan Viverette75fd8f92015-03-09 15:51:10 -07001134 float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
Alan Viveretteddf655c2015-04-22 13:43:31 -07001135 for (int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < mItems.size(); pos++) {
1136 ItemInfo ii = mItems.get(itemIndex);
Alan Viverette75fd8f92015-03-09 15:51:10 -07001137 while (pos > ii.position && itemIndex < mItems.size() - 1) {
1138 itemIndex++;
1139 ii = mItems.get(itemIndex);
1140 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07001141
Alan Viverette75fd8f92015-03-09 15:51:10 -07001142 while (pos < ii.position) {
1143 // We don't have an item populated for this,
1144 // ask the adapter for an offset.
1145 offset += mAdapter.getPageWidth(pos) + marginOffset;
1146 pos++;
1147 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07001148
Alan Viverette75fd8f92015-03-09 15:51:10 -07001149 ii.offset = offset;
1150 offset += ii.widthFactor + marginOffset;
1151 }
1152 } else if (oldCurPosition > curItem.position) {
1153 int itemIndex = mItems.size() - 1;
Alan Viverette75fd8f92015-03-09 15:51:10 -07001154 float offset = oldCurInfo.offset;
Alan Viveretteddf655c2015-04-22 13:43:31 -07001155 for (int pos = oldCurPosition - 1; pos >= curItem.position && itemIndex >= 0; pos--) {
1156 ItemInfo ii = mItems.get(itemIndex);
Alan Viverette75fd8f92015-03-09 15:51:10 -07001157 while (pos < ii.position && itemIndex > 0) {
1158 itemIndex--;
1159 ii = mItems.get(itemIndex);
1160 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07001161
Alan Viverette75fd8f92015-03-09 15:51:10 -07001162 while (pos > ii.position) {
1163 // We don't have an item populated for this,
1164 // ask the adapter for an offset.
1165 offset -= mAdapter.getPageWidth(pos) + marginOffset;
1166 pos--;
1167 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07001168
Alan Viverette75fd8f92015-03-09 15:51:10 -07001169 offset -= ii.widthFactor + marginOffset;
1170 ii.offset = offset;
1171 }
1172 }
1173 }
1174
1175 // Base all offsets off of curItem.
1176 final int itemCount = mItems.size();
1177 float offset = curItem.offset;
1178 int pos = curItem.position - 1;
1179 mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
1180 mLastOffset = curItem.position == N - 1 ?
1181 curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
Alan Viveretteddf655c2015-04-22 13:43:31 -07001182
Alan Viverette75fd8f92015-03-09 15:51:10 -07001183 // Previous pages
1184 for (int i = curIndex - 1; i >= 0; i--, pos--) {
1185 final ItemInfo ii = mItems.get(i);
1186 while (pos > ii.position) {
1187 offset -= mAdapter.getPageWidth(pos--) + marginOffset;
1188 }
1189 offset -= ii.widthFactor + marginOffset;
1190 ii.offset = offset;
1191 if (ii.position == 0) mFirstOffset = offset;
1192 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07001193
Alan Viverette75fd8f92015-03-09 15:51:10 -07001194 offset = curItem.offset + curItem.widthFactor + marginOffset;
1195 pos = curItem.position + 1;
Alan Viveretteddf655c2015-04-22 13:43:31 -07001196
Alan Viverette75fd8f92015-03-09 15:51:10 -07001197 // Next pages
1198 for (int i = curIndex + 1; i < itemCount; i++, pos++) {
1199 final ItemInfo ii = mItems.get(i);
1200 while (pos < ii.position) {
1201 offset += mAdapter.getPageWidth(pos++) + marginOffset;
1202 }
1203 if (ii.position == N - 1) {
1204 mLastOffset = offset + ii.widthFactor - 1;
1205 }
1206 ii.offset = offset;
1207 offset += ii.widthFactor + marginOffset;
1208 }
Alan Viverette75fd8f92015-03-09 15:51:10 -07001209 }
1210
1211 /**
1212 * This is the persistent state that is saved by ViewPager. Only needed
1213 * if you are creating a sublass of ViewPager that must save its own
1214 * state, in which case it should implement a subclass of this which
1215 * contains that state.
1216 */
1217 public static class SavedState extends BaseSavedState {
1218 int position;
1219 Parcelable adapterState;
1220 ClassLoader loader;
1221
1222 public SavedState(Parcel source) {
1223 super(source);
1224 }
1225
1226 public SavedState(Parcelable superState) {
1227 super(superState);
1228 }
1229
1230 @Override
1231 public void writeToParcel(Parcel out, int flags) {
1232 super.writeToParcel(out, flags);
1233 out.writeInt(position);
1234 out.writeParcelable(adapterState, flags);
1235 }
1236
1237 @Override
1238 public String toString() {
1239 return "FragmentPager.SavedState{"
1240 + Integer.toHexString(System.identityHashCode(this))
1241 + " position=" + position + "}";
1242 }
1243
1244 public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
1245 @Override
1246 public SavedState createFromParcel(Parcel in) {
1247 return new SavedState(in);
1248 }
1249 @Override
1250 public SavedState[] newArray(int size) {
1251 return new SavedState[size];
1252 }
1253 };
1254
1255 SavedState(Parcel in, ClassLoader loader) {
1256 super(in);
1257 if (loader == null) {
1258 loader = getClass().getClassLoader();
1259 }
1260 position = in.readInt();
1261 adapterState = in.readParcelable(loader);
1262 this.loader = loader;
1263 }
1264 }
1265
1266 @Override
1267 public Parcelable onSaveInstanceState() {
1268 Parcelable superState = super.onSaveInstanceState();
1269 SavedState ss = new SavedState(superState);
1270 ss.position = mCurItem;
1271 if (mAdapter != null) {
1272 ss.adapterState = mAdapter.saveState();
1273 }
1274 return ss;
1275 }
1276
1277 @Override
1278 public void onRestoreInstanceState(Parcelable state) {
1279 if (!(state instanceof SavedState)) {
1280 super.onRestoreInstanceState(state);
1281 return;
1282 }
1283
1284 SavedState ss = (SavedState)state;
1285 super.onRestoreInstanceState(ss.getSuperState());
1286
1287 if (mAdapter != null) {
1288 mAdapter.restoreState(ss.adapterState, ss.loader);
1289 setCurrentItemInternal(ss.position, false, true);
1290 } else {
1291 mRestoredCurItem = ss.position;
1292 mRestoredAdapterState = ss.adapterState;
1293 mRestoredClassLoader = ss.loader;
1294 }
1295 }
1296
1297 @Override
1298 public void addView(View child, int index, ViewGroup.LayoutParams params) {
1299 if (!checkLayoutParams(params)) {
1300 params = generateLayoutParams(params);
1301 }
1302 final LayoutParams lp = (LayoutParams) params;
1303 lp.isDecor |= child instanceof Decor;
1304 if (mInLayout) {
1305 if (lp != null && lp.isDecor) {
1306 throw new IllegalStateException("Cannot add pager decor view during layout");
1307 }
1308 lp.needsMeasure = true;
1309 addViewInLayout(child, index, params);
1310 } else {
1311 super.addView(child, index, params);
1312 }
1313
1314 if (USE_CACHE) {
1315 if (child.getVisibility() != GONE) {
1316 child.setDrawingCacheEnabled(mScrollingCacheEnabled);
1317 } else {
1318 child.setDrawingCacheEnabled(false);
1319 }
1320 }
1321 }
1322
1323 @Override
1324 public void removeView(View view) {
1325 if (mInLayout) {
1326 removeViewInLayout(view);
1327 } else {
1328 super.removeView(view);
1329 }
1330 }
1331
1332 ItemInfo infoForChild(View child) {
1333 for (int i=0; i<mItems.size(); i++) {
1334 ItemInfo ii = mItems.get(i);
1335 if (mAdapter.isViewFromObject(child, ii.object)) {
1336 return ii;
1337 }
1338 }
1339 return null;
1340 }
1341
1342 ItemInfo infoForAnyChild(View child) {
1343 ViewParent parent;
1344 while ((parent=child.getParent()) != this) {
1345 if (parent == null || !(parent instanceof View)) {
1346 return null;
1347 }
1348 child = (View)parent;
1349 }
1350 return infoForChild(child);
1351 }
1352
1353 ItemInfo infoForPosition(int position) {
1354 for (int i = 0; i < mItems.size(); i++) {
1355 ItemInfo ii = mItems.get(i);
1356 if (ii.position == position) {
1357 return ii;
1358 }
1359 }
1360 return null;
1361 }
1362
1363 @Override
1364 protected void onAttachedToWindow() {
1365 super.onAttachedToWindow();
1366 mFirstLayout = true;
1367 }
1368
1369 @Override
1370 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1371 // For simple implementation, our internal size is always 0.
1372 // We depend on the container to specify the layout size of
1373 // our view. We can't really know what it is since we will be
1374 // adding and removing different arbitrary views and do not
1375 // want the layout to change as this happens.
1376 setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
1377 getDefaultSize(0, heightMeasureSpec));
1378
1379 final int measuredWidth = getMeasuredWidth();
1380 final int maxGutterSize = measuredWidth / 10;
1381 mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
1382
1383 // Children are just made to fill our space.
1384 int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
1385 int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
1386
1387 /*
1388 * Make sure all children have been properly measured. Decor views first.
1389 * Right now we cheat and make this less complicated by assuming decor
1390 * views won't intersect. We will pin to edges based on gravity.
1391 */
1392 int size = getChildCount();
1393 for (int i = 0; i < size; ++i) {
1394 final View child = getChildAt(i);
1395 if (child.getVisibility() != GONE) {
1396 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1397 if (lp != null && lp.isDecor) {
1398 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1399 final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
1400 int widthMode = MeasureSpec.AT_MOST;
1401 int heightMode = MeasureSpec.AT_MOST;
1402 boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
1403 boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
1404
1405 if (consumeVertical) {
1406 widthMode = MeasureSpec.EXACTLY;
1407 } else if (consumeHorizontal) {
1408 heightMode = MeasureSpec.EXACTLY;
1409 }
1410
1411 int widthSize = childWidthSize;
1412 int heightSize = childHeightSize;
1413 if (lp.width != LayoutParams.WRAP_CONTENT) {
1414 widthMode = MeasureSpec.EXACTLY;
1415 if (lp.width != LayoutParams.FILL_PARENT) {
1416 widthSize = lp.width;
1417 }
1418 }
1419 if (lp.height != LayoutParams.WRAP_CONTENT) {
1420 heightMode = MeasureSpec.EXACTLY;
1421 if (lp.height != LayoutParams.FILL_PARENT) {
1422 heightSize = lp.height;
1423 }
1424 }
1425 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
1426 final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
1427 child.measure(widthSpec, heightSpec);
1428
1429 if (consumeVertical) {
1430 childHeightSize -= child.getMeasuredHeight();
1431 } else if (consumeHorizontal) {
1432 childWidthSize -= child.getMeasuredWidth();
1433 }
1434 }
1435 }
1436 }
1437
1438 mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
1439 mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
1440
1441 // Make sure we have created all fragments that we need to have shown.
1442 mInLayout = true;
1443 populate();
1444 mInLayout = false;
1445
1446 // Page views next.
1447 size = getChildCount();
1448 for (int i = 0; i < size; ++i) {
1449 final View child = getChildAt(i);
1450 if (child.getVisibility() != GONE) {
1451 if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
1452 + ": " + mChildWidthMeasureSpec);
1453
1454 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1455 if (lp == null || !lp.isDecor) {
1456 final int widthSpec = MeasureSpec.makeMeasureSpec(
1457 (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
1458 child.measure(widthSpec, mChildHeightMeasureSpec);
1459 }
1460 }
1461 }
1462 }
1463
1464 @Override
1465 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1466 super.onSizeChanged(w, h, oldw, oldh);
1467
1468 // Make sure scroll position is set correctly.
1469 if (w != oldw) {
1470 recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
1471 }
1472 }
1473
1474 private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) {
1475 if (oldWidth > 0 && !mItems.isEmpty()) {
1476 final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin;
1477 final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight()
1478 + oldMargin;
1479 final int xpos = getScrollX();
1480 final float pageOffset = (float) xpos / oldWidthWithMargin;
1481 final int newOffsetPixels = (int) (pageOffset * widthWithMargin);
1482
1483 scrollTo(newOffsetPixels, getScrollY());
1484 if (!mScroller.isFinished()) {
1485 // We now return to your regularly scheduled scroll, already in progress.
1486 final int newDuration = mScroller.getDuration() - mScroller.timePassed();
1487 ItemInfo targetInfo = infoForPosition(mCurItem);
1488 mScroller.startScroll(newOffsetPixels, 0,
1489 (int) (targetInfo.offset * width), 0, newDuration);
1490 }
1491 } else {
1492 final ItemInfo ii = infoForPosition(mCurItem);
1493 final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
1494 final int scrollPos = (int) (scrollOffset *
1495 (width - getPaddingLeft() - getPaddingRight()));
1496 if (scrollPos != getScrollX()) {
1497 completeScroll(false);
1498 scrollTo(scrollPos, getScrollY());
1499 }
1500 }
1501 }
1502
1503 @Override
1504 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1505 final int count = getChildCount();
1506 int width = r - l;
1507 int height = b - t;
1508 int paddingLeft = getPaddingLeft();
1509 int paddingTop = getPaddingTop();
1510 int paddingRight = getPaddingRight();
1511 int paddingBottom = getPaddingBottom();
1512 final int scrollX = getScrollX();
1513
1514 int decorCount = 0;
1515
1516 // First pass - decor views. We need to do this in two passes so that
1517 // we have the proper offsets for non-decor views later.
1518 for (int i = 0; i < count; i++) {
1519 final View child = getChildAt(i);
1520 if (child.getVisibility() != GONE) {
1521 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1522 int childLeft = 0;
1523 int childTop = 0;
1524 if (lp.isDecor) {
1525 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1526 final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
1527 switch (hgrav) {
1528 default:
1529 childLeft = paddingLeft;
1530 break;
1531 case Gravity.LEFT:
1532 childLeft = paddingLeft;
1533 paddingLeft += child.getMeasuredWidth();
1534 break;
1535 case Gravity.CENTER_HORIZONTAL:
1536 childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
1537 paddingLeft);
1538 break;
1539 case Gravity.RIGHT:
1540 childLeft = width - paddingRight - child.getMeasuredWidth();
1541 paddingRight += child.getMeasuredWidth();
1542 break;
1543 }
1544 switch (vgrav) {
1545 default:
1546 childTop = paddingTop;
1547 break;
1548 case Gravity.TOP:
1549 childTop = paddingTop;
1550 paddingTop += child.getMeasuredHeight();
1551 break;
1552 case Gravity.CENTER_VERTICAL:
1553 childTop = Math.max((height - child.getMeasuredHeight()) / 2,
1554 paddingTop);
1555 break;
1556 case Gravity.BOTTOM:
1557 childTop = height - paddingBottom - child.getMeasuredHeight();
1558 paddingBottom += child.getMeasuredHeight();
1559 break;
1560 }
1561 childLeft += scrollX;
1562 child.layout(childLeft, childTop,
1563 childLeft + child.getMeasuredWidth(),
1564 childTop + child.getMeasuredHeight());
1565 decorCount++;
1566 }
1567 }
1568 }
1569
1570 final int childWidth = width - paddingLeft - paddingRight;
1571 // Page views. Do this once we have the right padding offsets from above.
1572 for (int i = 0; i < count; i++) {
1573 final View child = getChildAt(i);
Alan Viveretteddf655c2015-04-22 13:43:31 -07001574 if (child.getVisibility() == GONE) {
1575 continue;
Alan Viverette75fd8f92015-03-09 15:51:10 -07001576 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07001577
1578 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1579 if (lp.isDecor) {
1580 continue;
1581 }
1582
1583 final ItemInfo ii = infoForChild(child);
1584 if (ii == null) {
1585 continue;
1586 }
1587
1588 if (lp.needsMeasure) {
1589 // This was added during layout and needs measurement.
1590 // Do it now that we know what we're working with.
1591 lp.needsMeasure = false;
1592 final int widthSpec = MeasureSpec.makeMeasureSpec(
1593 (int) (childWidth * lp.widthFactor),
1594 MeasureSpec.EXACTLY);
1595 final int heightSpec = MeasureSpec.makeMeasureSpec(
1596 (int) (height - paddingTop - paddingBottom),
1597 MeasureSpec.EXACTLY);
1598 child.measure(widthSpec, heightSpec);
1599 }
1600
1601 final int childMeasuredWidth = child.getMeasuredWidth();
1602 final int startOffset = (int) (childWidth * ii.offset);
1603 final int childLeft;
1604 if (isLayoutRtl()) {
1605 childLeft = MAX_SCROLL_X - paddingRight - startOffset - childMeasuredWidth;
1606 } else {
1607 childLeft = paddingLeft + startOffset;
1608 }
1609
1610 final int childTop = paddingTop;
1611 child.layout(childLeft, childTop, childLeft + childMeasuredWidth,
1612 childTop + child.getMeasuredHeight());
Alan Viverette75fd8f92015-03-09 15:51:10 -07001613 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07001614
Alan Viverette75fd8f92015-03-09 15:51:10 -07001615 mTopPageBounds = paddingTop;
1616 mBottomPageBounds = height - paddingBottom;
1617 mDecorChildCount = decorCount;
1618
1619 if (mFirstLayout) {
1620 scrollToItem(mCurItem, false, 0, false);
1621 }
1622 mFirstLayout = false;
1623 }
1624
1625 @Override
1626 public void computeScroll() {
1627 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
Alan Viveretteddf655c2015-04-22 13:43:31 -07001628 final int oldX = getScrollX();
1629 final int oldY = getScrollY();
1630 final int x = mScroller.getCurrX();
1631 final int y = mScroller.getCurrY();
Alan Viverette75fd8f92015-03-09 15:51:10 -07001632
1633 if (oldX != x || oldY != y) {
1634 scrollTo(x, y);
Alan Viveretteddf655c2015-04-22 13:43:31 -07001635
Alan Viverette75fd8f92015-03-09 15:51:10 -07001636 if (!pageScrolled(x)) {
1637 mScroller.abortAnimation();
1638 scrollTo(0, y);
1639 }
1640 }
1641
1642 // Keep on drawing until the animation has finished.
1643 postInvalidateOnAnimation();
1644 return;
1645 }
1646
1647 // Done with scroll, clean up state.
1648 completeScroll(true);
1649 }
1650
Alan Viveretteddf655c2015-04-22 13:43:31 -07001651 private boolean pageScrolled(int scrollX) {
Alan Viverette75fd8f92015-03-09 15:51:10 -07001652 if (mItems.size() == 0) {
1653 mCalledSuper = false;
1654 onPageScrolled(0, 0, 0);
1655 if (!mCalledSuper) {
1656 throw new IllegalStateException(
1657 "onPageScrolled did not call superclass implementation");
1658 }
1659 return false;
1660 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07001661
1662 // Translate to scrollX to scrollStart for RTL.
1663 final int scrollStart;
1664 if (isLayoutRtl()) {
1665 scrollStart = MAX_SCROLL_X - scrollX;
1666 } else {
1667 scrollStart = scrollX;
1668 }
1669
1670 final ItemInfo ii = infoForFirstVisiblePage();
1671 final int width = getPaddedWidth();
Alan Viverette75fd8f92015-03-09 15:51:10 -07001672 final int widthWithMargin = width + mPageMargin;
1673 final float marginOffset = (float) mPageMargin / width;
1674 final int currentPage = ii.position;
Alan Viveretteddf655c2015-04-22 13:43:31 -07001675 final float pageOffset = (((float) scrollStart / width) - ii.offset) /
Alan Viverette75fd8f92015-03-09 15:51:10 -07001676 (ii.widthFactor + marginOffset);
1677 final int offsetPixels = (int) (pageOffset * widthWithMargin);
1678
1679 mCalledSuper = false;
1680 onPageScrolled(currentPage, pageOffset, offsetPixels);
1681 if (!mCalledSuper) {
1682 throw new IllegalStateException(
1683 "onPageScrolled did not call superclass implementation");
1684 }
1685 return true;
1686 }
1687
1688 /**
1689 * This method will be invoked when the current page is scrolled, either as part
1690 * of a programmatically initiated smooth scroll or a user initiated touch scroll.
1691 * If you override this method you must call through to the superclass implementation
1692 * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
1693 * returns.
1694 *
1695 * @param position Position index of the first page currently being displayed.
1696 * Page position+1 will be visible if positionOffset is nonzero.
1697 * @param offset Value from [0, 1) indicating the offset from the page at position.
1698 * @param offsetPixels Value in pixels indicating the offset from position.
1699 */
1700 protected void onPageScrolled(int position, float offset, int offsetPixels) {
1701 // Offset any decor views if needed - keep them on-screen at all times.
1702 if (mDecorChildCount > 0) {
1703 final int scrollX = getScrollX();
1704 int paddingLeft = getPaddingLeft();
1705 int paddingRight = getPaddingRight();
1706 final int width = getWidth();
1707 final int childCount = getChildCount();
1708 for (int i = 0; i < childCount; i++) {
1709 final View child = getChildAt(i);
1710 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1711 if (!lp.isDecor) continue;
1712
1713 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1714 int childLeft = 0;
1715 switch (hgrav) {
1716 default:
1717 childLeft = paddingLeft;
1718 break;
1719 case Gravity.LEFT:
1720 childLeft = paddingLeft;
1721 paddingLeft += child.getWidth();
1722 break;
1723 case Gravity.CENTER_HORIZONTAL:
1724 childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
1725 paddingLeft);
1726 break;
1727 case Gravity.RIGHT:
1728 childLeft = width - paddingRight - child.getMeasuredWidth();
1729 paddingRight += child.getMeasuredWidth();
1730 break;
1731 }
1732 childLeft += scrollX;
1733
1734 final int childOffset = childLeft - child.getLeft();
1735 if (childOffset != 0) {
1736 child.offsetLeftAndRight(childOffset);
1737 }
1738 }
1739 }
1740
1741 if (mOnPageChangeListener != null) {
1742 mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
1743 }
1744 if (mInternalPageChangeListener != null) {
1745 mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
1746 }
1747
1748 if (mPageTransformer != null) {
1749 final int scrollX = getScrollX();
1750 final int childCount = getChildCount();
1751 for (int i = 0; i < childCount; i++) {
1752 final View child = getChildAt(i);
1753 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1754
1755 if (lp.isDecor) continue;
1756
Alan Viveretteddf655c2015-04-22 13:43:31 -07001757 final float transformPos = (float) (child.getLeft() - scrollX) / getPaddedWidth();
Alan Viverette75fd8f92015-03-09 15:51:10 -07001758 mPageTransformer.transformPage(child, transformPos);
1759 }
1760 }
1761
1762 mCalledSuper = true;
1763 }
1764
1765 private void completeScroll(boolean postEvents) {
1766 boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
1767 if (needPopulate) {
1768 // Done with scroll, no longer want to cache view drawing.
1769 setScrollingCacheEnabled(false);
1770 mScroller.abortAnimation();
1771 int oldX = getScrollX();
1772 int oldY = getScrollY();
1773 int x = mScroller.getCurrX();
1774 int y = mScroller.getCurrY();
1775 if (oldX != x || oldY != y) {
1776 scrollTo(x, y);
1777 }
1778 }
1779 mPopulatePending = false;
1780 for (int i=0; i<mItems.size(); i++) {
1781 ItemInfo ii = mItems.get(i);
1782 if (ii.scrolling) {
1783 needPopulate = true;
1784 ii.scrolling = false;
1785 }
1786 }
1787 if (needPopulate) {
1788 if (postEvents) {
1789 postOnAnimation(mEndScrollRunnable);
1790 } else {
1791 mEndScrollRunnable.run();
1792 }
1793 }
1794 }
1795
1796 private boolean isGutterDrag(float x, float dx) {
1797 return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0);
1798 }
1799
1800 private void enableLayers(boolean enable) {
1801 final int childCount = getChildCount();
1802 for (int i = 0; i < childCount; i++) {
1803 final int layerType = enable ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE;
1804 getChildAt(i).setLayerType(layerType, null);
1805 }
1806 }
1807
1808 @Override
1809 public boolean onInterceptTouchEvent(MotionEvent ev) {
1810 /*
1811 * This method JUST determines whether we want to intercept the motion.
1812 * If we return true, onMotionEvent will be called and we do the actual
1813 * scrolling there.
1814 */
1815
1816 final int action = ev.getAction() & MotionEvent.ACTION_MASK;
1817
1818 // Always take care of the touch gesture being complete.
1819 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
1820 // Release the drag.
1821 if (DEBUG) Log.v(TAG, "Intercept done!");
1822 mIsBeingDragged = false;
1823 mIsUnableToDrag = false;
1824 mActivePointerId = INVALID_POINTER;
1825 if (mVelocityTracker != null) {
1826 mVelocityTracker.recycle();
1827 mVelocityTracker = null;
1828 }
1829 return false;
1830 }
1831
1832 // Nothing more to do here if we have decided whether or not we
1833 // are dragging.
1834 if (action != MotionEvent.ACTION_DOWN) {
1835 if (mIsBeingDragged) {
Alan Viveretteddf655c2015-04-22 13:43:31 -07001836 if (DEBUG) Log.v(TAG, "Being dragged, intercept returning true!");
Alan Viverette75fd8f92015-03-09 15:51:10 -07001837 return true;
1838 }
1839 if (mIsUnableToDrag) {
Alan Viveretteddf655c2015-04-22 13:43:31 -07001840 if (DEBUG) Log.v(TAG, "Unable to drag, intercept returning false!");
Alan Viverette75fd8f92015-03-09 15:51:10 -07001841 return false;
1842 }
1843 }
1844
1845 switch (action) {
1846 case MotionEvent.ACTION_MOVE: {
1847 /*
1848 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
1849 * whether the user has moved far enough from his original down touch.
1850 */
1851
1852 /*
1853 * Locally do absolute value. mLastMotionY is set to the y value
1854 * of the down event.
1855 */
1856 final int activePointerId = mActivePointerId;
1857 if (activePointerId == INVALID_POINTER) {
1858 // If we don't have a valid id, the touch down wasn't on content.
1859 break;
1860 }
1861
1862 final int pointerIndex = ev.findPointerIndex(activePointerId);
1863 final float x = ev.getX(pointerIndex);
1864 final float dx = x - mLastMotionX;
1865 final float xDiff = Math.abs(dx);
1866 final float y = ev.getY(pointerIndex);
1867 final float yDiff = Math.abs(y - mInitialMotionY);
1868 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
1869
1870 if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
1871 canScroll(this, false, (int) dx, (int) x, (int) y)) {
1872 // Nested view has scrollable area under this point. Let it be handled there.
1873 mLastMotionX = x;
1874 mLastMotionY = y;
1875 mIsUnableToDrag = true;
1876 return false;
1877 }
1878 if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
1879 if (DEBUG) Log.v(TAG, "Starting drag!");
1880 mIsBeingDragged = true;
1881 requestParentDisallowInterceptTouchEvent(true);
1882 setScrollState(SCROLL_STATE_DRAGGING);
1883 mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
1884 mInitialMotionX - mTouchSlop;
1885 mLastMotionY = y;
1886 setScrollingCacheEnabled(true);
1887 } else if (yDiff > mTouchSlop) {
1888 // The finger has moved enough in the vertical
1889 // direction to be counted as a drag... abort
1890 // any attempt to drag horizontally, to work correctly
1891 // with children that have scrolling containers.
1892 if (DEBUG) Log.v(TAG, "Starting unable to drag!");
1893 mIsUnableToDrag = true;
1894 }
1895 if (mIsBeingDragged) {
1896 // Scroll to follow the motion event
1897 if (performDrag(x)) {
1898 postInvalidateOnAnimation();
1899 }
1900 }
1901 break;
1902 }
1903
1904 case MotionEvent.ACTION_DOWN: {
1905 /*
1906 * Remember location of down touch.
1907 * ACTION_DOWN always refers to pointer index 0.
1908 */
1909 mLastMotionX = mInitialMotionX = ev.getX();
1910 mLastMotionY = mInitialMotionY = ev.getY();
1911 mActivePointerId = ev.getPointerId(0);
1912 mIsUnableToDrag = false;
1913
1914 mScroller.computeScrollOffset();
1915 if (mScrollState == SCROLL_STATE_SETTLING &&
1916 Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
1917 // Let the user 'catch' the pager as it animates.
1918 mScroller.abortAnimation();
1919 mPopulatePending = false;
1920 populate();
1921 mIsBeingDragged = true;
1922 requestParentDisallowInterceptTouchEvent(true);
1923 setScrollState(SCROLL_STATE_DRAGGING);
1924 } else {
1925 completeScroll(false);
1926 mIsBeingDragged = false;
1927 }
1928
1929 if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
1930 + " mIsBeingDragged=" + mIsBeingDragged
1931 + "mIsUnableToDrag=" + mIsUnableToDrag);
1932 break;
1933 }
1934
1935 case MotionEvent.ACTION_POINTER_UP:
1936 onSecondaryPointerUp(ev);
1937 break;
1938 }
1939
1940 if (mVelocityTracker == null) {
1941 mVelocityTracker = VelocityTracker.obtain();
1942 }
1943 mVelocityTracker.addMovement(ev);
1944
1945 /*
1946 * The only time we want to intercept motion events is if we are in the
1947 * drag mode.
1948 */
1949 return mIsBeingDragged;
1950 }
1951
1952 @Override
1953 public boolean onTouchEvent(MotionEvent ev) {
Alan Viverette75fd8f92015-03-09 15:51:10 -07001954 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
1955 // Don't handle edge touches immediately -- they may actually belong to one of our
1956 // descendants.
1957 return false;
1958 }
1959
1960 if (mAdapter == null || mAdapter.getCount() == 0) {
1961 // Nothing to present or scroll; nothing to touch.
1962 return false;
1963 }
1964
1965 if (mVelocityTracker == null) {
1966 mVelocityTracker = VelocityTracker.obtain();
1967 }
1968 mVelocityTracker.addMovement(ev);
1969
1970 final int action = ev.getAction();
1971 boolean needsInvalidate = false;
1972
1973 switch (action & MotionEvent.ACTION_MASK) {
1974 case MotionEvent.ACTION_DOWN: {
1975 mScroller.abortAnimation();
1976 mPopulatePending = false;
1977 populate();
1978
1979 // Remember where the motion event started
1980 mLastMotionX = mInitialMotionX = ev.getX();
1981 mLastMotionY = mInitialMotionY = ev.getY();
1982 mActivePointerId = ev.getPointerId(0);
1983 break;
1984 }
1985 case MotionEvent.ACTION_MOVE:
1986 if (!mIsBeingDragged) {
1987 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
1988 final float x = ev.getX(pointerIndex);
1989 final float xDiff = Math.abs(x - mLastMotionX);
1990 final float y = ev.getY(pointerIndex);
1991 final float yDiff = Math.abs(y - mLastMotionY);
1992 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
1993 if (xDiff > mTouchSlop && xDiff > yDiff) {
1994 if (DEBUG) Log.v(TAG, "Starting drag!");
1995 mIsBeingDragged = true;
1996 requestParentDisallowInterceptTouchEvent(true);
1997 mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
1998 mInitialMotionX - mTouchSlop;
1999 mLastMotionY = y;
2000 setScrollState(SCROLL_STATE_DRAGGING);
2001 setScrollingCacheEnabled(true);
2002
2003 // Disallow Parent Intercept, just in case
2004 ViewParent parent = getParent();
2005 if (parent != null) {
2006 parent.requestDisallowInterceptTouchEvent(true);
2007 }
2008 }
2009 }
2010 // Not else! Note that mIsBeingDragged can be set above.
2011 if (mIsBeingDragged) {
2012 // Scroll to follow the motion event
2013 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
2014 final float x = ev.getX(activePointerIndex);
2015 needsInvalidate |= performDrag(x);
2016 }
2017 break;
2018 case MotionEvent.ACTION_UP:
2019 if (mIsBeingDragged) {
2020 final VelocityTracker velocityTracker = mVelocityTracker;
2021 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
Alan Viveretteddf655c2015-04-22 13:43:31 -07002022 final int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
2023
Alan Viverette75fd8f92015-03-09 15:51:10 -07002024 mPopulatePending = true;
Alan Viveretteddf655c2015-04-22 13:43:31 -07002025
2026 final float scrollStart = getScrollStart();
2027 final float scrolledPages = scrollStart / getPaddedWidth();
2028 final ItemInfo ii = infoForFirstVisiblePage();
Alan Viverette75fd8f92015-03-09 15:51:10 -07002029 final int currentPage = ii.position;
Alan Viveretteddf655c2015-04-22 13:43:31 -07002030 final float nextPageOffset;
2031 if (isLayoutRtl()) {
2032 nextPageOffset = (ii.offset - scrolledPages) / ii.widthFactor;
2033 } else {
2034 nextPageOffset = (scrolledPages - ii.offset) / ii.widthFactor;
2035 }
2036
2037 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002038 final float x = ev.getX(activePointerIndex);
2039 final int totalDelta = (int) (x - mInitialMotionX);
Alan Viveretteddf655c2015-04-22 13:43:31 -07002040 final int nextPage = determineTargetPage(
2041 currentPage, nextPageOffset, initialVelocity, totalDelta);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002042 setCurrentItemInternal(nextPage, true, true, initialVelocity);
2043
2044 mActivePointerId = INVALID_POINTER;
2045 endDrag();
2046 mLeftEdge.onRelease();
2047 mRightEdge.onRelease();
2048 needsInvalidate = true;
2049 }
2050 break;
2051 case MotionEvent.ACTION_CANCEL:
2052 if (mIsBeingDragged) {
2053 scrollToItem(mCurItem, true, 0, false);
2054 mActivePointerId = INVALID_POINTER;
2055 endDrag();
2056 mLeftEdge.onRelease();
2057 mRightEdge.onRelease();
2058 needsInvalidate = true;
2059 }
2060 break;
2061 case MotionEvent.ACTION_POINTER_DOWN: {
2062 final int index = ev.getActionIndex();
2063 final float x = ev.getX(index);
2064 mLastMotionX = x;
2065 mActivePointerId = ev.getPointerId(index);
2066 break;
2067 }
2068 case MotionEvent.ACTION_POINTER_UP:
2069 onSecondaryPointerUp(ev);
2070 mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
2071 break;
2072 }
2073 if (needsInvalidate) {
2074 postInvalidateOnAnimation();
2075 }
2076 return true;
2077 }
2078
2079 private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
2080 final ViewParent parent = getParent();
2081 if (parent != null) {
2082 parent.requestDisallowInterceptTouchEvent(disallowIntercept);
2083 }
2084 }
2085
2086 private boolean performDrag(float x) {
2087 boolean needsInvalidate = false;
2088
Alan Viveretteddf655c2015-04-22 13:43:31 -07002089 final int width = getPaddedWidth();
Alan Viverette75fd8f92015-03-09 15:51:10 -07002090 final float deltaX = mLastMotionX - x;
2091 mLastMotionX = x;
2092
Alan Viveretteddf655c2015-04-22 13:43:31 -07002093 final EdgeEffect startEdge;
2094 final EdgeEffect endEdge;
2095 if (isLayoutRtl()) {
2096 startEdge = mRightEdge;
2097 endEdge = mLeftEdge;
2098 } else {
2099 startEdge = mLeftEdge;
2100 endEdge = mRightEdge;
Alan Viverette75fd8f92015-03-09 15:51:10 -07002101 }
2102
Alan Viveretteddf655c2015-04-22 13:43:31 -07002103 // Translate scroll to relative coordinates.
2104 final float nextScrollX = getScrollX() + deltaX;
2105 final float scrollStart;
2106 if (isLayoutRtl()) {
2107 scrollStart = MAX_SCROLL_X - nextScrollX;
2108 } else {
2109 scrollStart = nextScrollX;
2110 }
2111
2112 final float startBound;
2113 final ItemInfo startItem = mItems.get(0);
2114 final boolean startAbsolute = startItem.position == 0;
2115 if (startAbsolute) {
2116 startBound = startItem.offset * width;
2117 } else {
2118 startBound = width * mFirstOffset;
2119 }
2120
2121 final float endBound;
2122 final ItemInfo endItem = mItems.get(mItems.size() - 1);
2123 final boolean endAbsolute = endItem.position == mAdapter.getCount() - 1;
2124 if (endAbsolute) {
2125 endBound = endItem.offset * width;
2126 } else {
2127 endBound = width * mLastOffset;
2128 }
2129
2130 final float clampedScrollStart;
2131 if (scrollStart < startBound) {
2132 if (startAbsolute) {
2133 final float over = startBound - scrollStart;
2134 startEdge.onPull(Math.abs(over) / width);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002135 needsInvalidate = true;
2136 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07002137 clampedScrollStart = startBound;
2138 } else if (scrollStart > endBound) {
2139 if (endAbsolute) {
2140 final float over = scrollStart - endBound;
2141 endEdge.onPull(Math.abs(over) / width);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002142 needsInvalidate = true;
2143 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07002144 clampedScrollStart = endBound;
2145 } else {
2146 clampedScrollStart = scrollStart;
Alan Viverette75fd8f92015-03-09 15:51:10 -07002147 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07002148
2149 // Translate back to absolute coordinates.
2150 final float targetScrollX;
2151 if (isLayoutRtl()) {
2152 targetScrollX = MAX_SCROLL_X - clampedScrollStart;
2153 } else {
2154 targetScrollX = clampedScrollStart;
2155 }
2156
2157 // Don't lose the rounded component.
2158 mLastMotionX += targetScrollX - (int) targetScrollX;
2159
2160 scrollTo((int) targetScrollX, getScrollY());
2161 pageScrolled((int) targetScrollX);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002162
2163 return needsInvalidate;
2164 }
2165
2166 /**
2167 * @return Info about the page at the current scroll position.
2168 * This can be synthetic for a missing middle page; the 'object' field can be null.
2169 */
Alan Viveretteddf655c2015-04-22 13:43:31 -07002170 private ItemInfo infoForFirstVisiblePage() {
2171 final int startOffset = getScrollStart();
2172 final int width = getPaddedWidth();
2173 final float scrollOffset = width > 0 ? (float) startOffset / width : 0;
Alan Viverette75fd8f92015-03-09 15:51:10 -07002174 final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
Alan Viveretteddf655c2015-04-22 13:43:31 -07002175
Alan Viverette75fd8f92015-03-09 15:51:10 -07002176 int lastPos = -1;
2177 float lastOffset = 0.f;
2178 float lastWidth = 0.f;
2179 boolean first = true;
Alan Viverette75fd8f92015-03-09 15:51:10 -07002180 ItemInfo lastItem = null;
Alan Viveretteddf655c2015-04-22 13:43:31 -07002181
2182 final int N = mItems.size();
2183 for (int i = 0; i < N; i++) {
Alan Viverette75fd8f92015-03-09 15:51:10 -07002184 ItemInfo ii = mItems.get(i);
Alan Viveretteddf655c2015-04-22 13:43:31 -07002185
2186 // Seek to position.
Alan Viverette75fd8f92015-03-09 15:51:10 -07002187 if (!first && ii.position != lastPos + 1) {
2188 // Create a synthetic item for a missing page.
2189 ii = mTempItem;
2190 ii.offset = lastOffset + lastWidth + marginOffset;
2191 ii.position = lastPos + 1;
2192 ii.widthFactor = mAdapter.getPageWidth(ii.position);
2193 i--;
2194 }
Alan Viverette75fd8f92015-03-09 15:51:10 -07002195
Alan Viveretteddf655c2015-04-22 13:43:31 -07002196 final float offset = ii.offset;
2197 final float startBound = offset;
2198 if (first || scrollOffset >= startBound) {
2199 final float endBound = offset + ii.widthFactor + marginOffset;
2200 if (scrollOffset < endBound || i == mItems.size() - 1) {
Alan Viverette75fd8f92015-03-09 15:51:10 -07002201 return ii;
2202 }
2203 } else {
2204 return lastItem;
2205 }
Alan Viveretteddf655c2015-04-22 13:43:31 -07002206
Alan Viverette75fd8f92015-03-09 15:51:10 -07002207 first = false;
2208 lastPos = ii.position;
2209 lastOffset = offset;
2210 lastWidth = ii.widthFactor;
2211 lastItem = ii;
2212 }
2213
2214 return lastItem;
2215 }
2216
Alan Viveretteddf655c2015-04-22 13:43:31 -07002217 private int getScrollStart() {
2218 if (isLayoutRtl()) {
2219 return MAX_SCROLL_X - getScrollX();
2220 } else {
2221 return getScrollX();
2222 }
2223 }
2224
2225 /**
2226 * @param currentPage the position of the page with the first visible starting edge
2227 * @param pageOffset the fraction of the right-hand page that's visible
2228 * @param velocity the velocity of the touch event stream
2229 * @param deltaX the distance of the touch event stream
2230 * @return the position of the target page
2231 */
Alan Viverette75fd8f92015-03-09 15:51:10 -07002232 private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
2233 int targetPage;
2234 if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
Alan Viveretteddf655c2015-04-22 13:43:31 -07002235 targetPage = currentPage - (velocity < 0 ? mLeftIncr : 0);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002236 } else {
2237 final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
Alan Viveretteddf655c2015-04-22 13:43:31 -07002238 targetPage = (int) (currentPage - mLeftIncr * (pageOffset + truncator));
Alan Viverette75fd8f92015-03-09 15:51:10 -07002239 }
2240
2241 if (mItems.size() > 0) {
2242 final ItemInfo firstItem = mItems.get(0);
2243 final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2244
2245 // Only let the user target pages we have items for
Alan Viveretteddf655c2015-04-22 13:43:31 -07002246 targetPage = MathUtils.constrain(targetPage, firstItem.position, lastItem.position);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002247 }
2248
2249 return targetPage;
2250 }
2251
2252 @Override
2253 public void draw(Canvas canvas) {
2254 super.draw(canvas);
2255 boolean needsInvalidate = false;
2256
2257 final int overScrollMode = getOverScrollMode();
2258 if (overScrollMode == View.OVER_SCROLL_ALWAYS ||
2259 (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS &&
2260 mAdapter != null && mAdapter.getCount() > 1)) {
2261 if (!mLeftEdge.isFinished()) {
2262 final int restoreCount = canvas.save();
2263 final int height = getHeight() - getPaddingTop() - getPaddingBottom();
2264 final int width = getWidth();
2265
2266 canvas.rotate(270);
2267 canvas.translate(-height + getPaddingTop(), mFirstOffset * width);
2268 mLeftEdge.setSize(height, width);
2269 needsInvalidate |= mLeftEdge.draw(canvas);
2270 canvas.restoreToCount(restoreCount);
2271 }
2272 if (!mRightEdge.isFinished()) {
2273 final int restoreCount = canvas.save();
2274 final int width = getWidth();
2275 final int height = getHeight() - getPaddingTop() - getPaddingBottom();
2276
2277 canvas.rotate(90);
2278 canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width);
2279 mRightEdge.setSize(height, width);
2280 needsInvalidate |= mRightEdge.draw(canvas);
2281 canvas.restoreToCount(restoreCount);
2282 }
2283 } else {
2284 mLeftEdge.finish();
2285 mRightEdge.finish();
2286 }
2287
2288 if (needsInvalidate) {
2289 // Keep animating
2290 postInvalidateOnAnimation();
2291 }
2292 }
2293
2294 @Override
2295 protected void onDraw(Canvas canvas) {
2296 super.onDraw(canvas);
2297
2298 // Draw the margin drawable between pages if needed.
2299 if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
2300 final int scrollX = getScrollX();
2301 final int width = getWidth();
2302
2303 final float marginOffset = (float) mPageMargin / width;
2304 int itemIndex = 0;
2305 ItemInfo ii = mItems.get(0);
2306 float offset = ii.offset;
Alan Viveretteddf655c2015-04-22 13:43:31 -07002307
Alan Viverette75fd8f92015-03-09 15:51:10 -07002308 final int itemCount = mItems.size();
2309 final int firstPos = ii.position;
2310 final int lastPos = mItems.get(itemCount - 1).position;
2311 for (int pos = firstPos; pos < lastPos; pos++) {
2312 while (pos > ii.position && itemIndex < itemCount) {
2313 ii = mItems.get(++itemIndex);
2314 }
2315
Alan Viveretteddf655c2015-04-22 13:43:31 -07002316 final float itemOffset;
2317 final float widthFactor;
Alan Viverette75fd8f92015-03-09 15:51:10 -07002318 if (pos == ii.position) {
Alan Viveretteddf655c2015-04-22 13:43:31 -07002319 itemOffset = ii.offset;
2320 widthFactor = ii.widthFactor;
Alan Viverette75fd8f92015-03-09 15:51:10 -07002321 } else {
Alan Viveretteddf655c2015-04-22 13:43:31 -07002322 itemOffset = offset;
2323 widthFactor = mAdapter.getPageWidth(pos);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002324 }
2325
Alan Viveretteddf655c2015-04-22 13:43:31 -07002326 final float left;
2327 final float scaledOffset = itemOffset * width;
2328 if (isLayoutRtl()) {
2329 left = MAX_SCROLL_X - scaledOffset;
2330 } else {
2331 left = scaledOffset + widthFactor * width;
2332 }
2333
2334 offset = itemOffset + widthFactor + marginOffset;
2335
2336 if (left + mPageMargin > scrollX) {
2337 mMarginDrawable.setBounds((int) left, mTopPageBounds,
2338 (int) (left + mPageMargin + 0.5f), mBottomPageBounds);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002339 mMarginDrawable.draw(canvas);
2340 }
2341
Alan Viveretteddf655c2015-04-22 13:43:31 -07002342 if (left > scrollX + width) {
Alan Viverette75fd8f92015-03-09 15:51:10 -07002343 break; // No more visible, no sense in continuing
2344 }
2345 }
2346 }
2347 }
2348
Alan Viverette75fd8f92015-03-09 15:51:10 -07002349 private void onSecondaryPointerUp(MotionEvent ev) {
2350 final int pointerIndex = ev.getActionIndex();
2351 final int pointerId = ev.getPointerId(pointerIndex);
2352 if (pointerId == mActivePointerId) {
2353 // This was our active pointer going up. Choose a new
2354 // active pointer and adjust accordingly.
2355 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
2356 mLastMotionX = ev.getX(newPointerIndex);
2357 mActivePointerId = ev.getPointerId(newPointerIndex);
2358 if (mVelocityTracker != null) {
2359 mVelocityTracker.clear();
2360 }
2361 }
2362 }
2363
2364 private void endDrag() {
2365 mIsBeingDragged = false;
2366 mIsUnableToDrag = false;
2367
2368 if (mVelocityTracker != null) {
2369 mVelocityTracker.recycle();
2370 mVelocityTracker = null;
2371 }
2372 }
2373
2374 private void setScrollingCacheEnabled(boolean enabled) {
2375 if (mScrollingCacheEnabled != enabled) {
2376 mScrollingCacheEnabled = enabled;
2377 if (USE_CACHE) {
2378 final int size = getChildCount();
2379 for (int i = 0; i < size; ++i) {
2380 final View child = getChildAt(i);
2381 if (child.getVisibility() != GONE) {
2382 child.setDrawingCacheEnabled(enabled);
2383 }
2384 }
2385 }
2386 }
2387 }
2388
2389 public boolean canScrollHorizontally(int direction) {
2390 if (mAdapter == null) {
2391 return false;
2392 }
2393
Alan Viveretteddf655c2015-04-22 13:43:31 -07002394 final int width = getPaddedWidth();
Alan Viverette75fd8f92015-03-09 15:51:10 -07002395 final int scrollX = getScrollX();
2396 if (direction < 0) {
2397 return (scrollX > (int) (width * mFirstOffset));
2398 } else if (direction > 0) {
2399 return (scrollX < (int) (width * mLastOffset));
2400 } else {
2401 return false;
2402 }
2403 }
2404
2405 /**
2406 * Tests scrollability within child views of v given a delta of dx.
2407 *
2408 * @param v View to test for horizontal scrollability
2409 * @param checkV Whether the view v passed should itself be checked for scrollability (true),
2410 * or just its children (false).
2411 * @param dx Delta scrolled in pixels
2412 * @param x X coordinate of the active touch point
2413 * @param y Y coordinate of the active touch point
2414 * @return true if child views of v can be scrolled by delta of dx.
2415 */
2416 protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
2417 if (v instanceof ViewGroup) {
2418 final ViewGroup group = (ViewGroup) v;
2419 final int scrollX = v.getScrollX();
2420 final int scrollY = v.getScrollY();
2421 final int count = group.getChildCount();
2422 // Count backwards - let topmost views consume scroll distance first.
2423 for (int i = count - 1; i >= 0; i--) {
Alan Viveretteddf655c2015-04-22 13:43:31 -07002424 // TODO: Add support for transformed views.
Alan Viverette75fd8f92015-03-09 15:51:10 -07002425 final View child = group.getChildAt(i);
Alan Viveretteddf655c2015-04-22 13:43:31 -07002426 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
2427 && y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
2428 && canScroll(child, true, dx, x + scrollX - child.getLeft(),
Alan Viverette75fd8f92015-03-09 15:51:10 -07002429 y + scrollY - child.getTop())) {
2430 return true;
2431 }
2432 }
2433 }
2434
2435 return checkV && v.canScrollHorizontally(-dx);
2436 }
2437
2438 @Override
2439 public boolean dispatchKeyEvent(KeyEvent event) {
2440 // Let the focused view and/or our descendants get the key first
2441 return super.dispatchKeyEvent(event) || executeKeyEvent(event);
2442 }
2443
2444 /**
2445 * You can call this function yourself to have the scroll view perform
2446 * scrolling from a key event, just as if the event had been dispatched to
2447 * it by the view hierarchy.
2448 *
2449 * @param event The key event to execute.
2450 * @return Return true if the event was handled, else false.
2451 */
2452 public boolean executeKeyEvent(KeyEvent event) {
2453 boolean handled = false;
2454 if (event.getAction() == KeyEvent.ACTION_DOWN) {
2455 switch (event.getKeyCode()) {
2456 case KeyEvent.KEYCODE_DPAD_LEFT:
2457 handled = arrowScroll(FOCUS_LEFT);
2458 break;
2459 case KeyEvent.KEYCODE_DPAD_RIGHT:
2460 handled = arrowScroll(FOCUS_RIGHT);
2461 break;
2462 case KeyEvent.KEYCODE_TAB:
2463 if (event.hasNoModifiers()) {
2464 handled = arrowScroll(FOCUS_FORWARD);
2465 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
2466 handled = arrowScroll(FOCUS_BACKWARD);
2467 }
2468 break;
2469 }
2470 }
2471 return handled;
2472 }
2473
2474 public boolean arrowScroll(int direction) {
2475 View currentFocused = findFocus();
2476 if (currentFocused == this) {
2477 currentFocused = null;
2478 } else if (currentFocused != null) {
2479 boolean isChild = false;
2480 for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2481 parent = parent.getParent()) {
2482 if (parent == this) {
2483 isChild = true;
2484 break;
2485 }
2486 }
2487 if (!isChild) {
2488 // This would cause the focus search down below to fail in fun ways.
2489 final StringBuilder sb = new StringBuilder();
2490 sb.append(currentFocused.getClass().getSimpleName());
2491 for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2492 parent = parent.getParent()) {
2493 sb.append(" => ").append(parent.getClass().getSimpleName());
2494 }
2495 Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
2496 "current focused view " + sb.toString());
2497 currentFocused = null;
2498 }
2499 }
2500
2501 boolean handled = false;
2502
2503 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
2504 direction);
2505 if (nextFocused != null && nextFocused != currentFocused) {
2506 if (direction == View.FOCUS_LEFT) {
2507 // If there is nothing to the left, or this is causing us to
2508 // jump to the right, then what we really want to do is page left.
2509 final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
2510 final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
2511 if (currentFocused != null && nextLeft >= currLeft) {
2512 handled = pageLeft();
2513 } else {
2514 handled = nextFocused.requestFocus();
2515 }
2516 } else if (direction == View.FOCUS_RIGHT) {
2517 // If there is nothing to the right, or this is causing us to
2518 // jump to the left, then what we really want to do is page right.
2519 final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
2520 final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
2521 if (currentFocused != null && nextLeft <= currLeft) {
2522 handled = pageRight();
2523 } else {
2524 handled = nextFocused.requestFocus();
2525 }
2526 }
2527 } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
2528 // Trying to move left and nothing there; try to page.
2529 handled = pageLeft();
2530 } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
2531 // Trying to move right and nothing there; try to page.
2532 handled = pageRight();
2533 }
2534 if (handled) {
2535 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
2536 }
2537 return handled;
2538 }
2539
2540 private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
2541 if (outRect == null) {
2542 outRect = new Rect();
2543 }
2544 if (child == null) {
2545 outRect.set(0, 0, 0, 0);
2546 return outRect;
2547 }
2548 outRect.left = child.getLeft();
2549 outRect.right = child.getRight();
2550 outRect.top = child.getTop();
2551 outRect.bottom = child.getBottom();
2552
2553 ViewParent parent = child.getParent();
2554 while (parent instanceof ViewGroup && parent != this) {
2555 final ViewGroup group = (ViewGroup) parent;
2556 outRect.left += group.getLeft();
2557 outRect.right += group.getRight();
2558 outRect.top += group.getTop();
2559 outRect.bottom += group.getBottom();
2560
2561 parent = group.getParent();
2562 }
2563 return outRect;
2564 }
2565
2566 boolean pageLeft() {
Alan Viveretteddf655c2015-04-22 13:43:31 -07002567 return setCurrentItemInternal(mCurItem + mLeftIncr, true, false);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002568 }
2569
2570 boolean pageRight() {
Alan Viveretteddf655c2015-04-22 13:43:31 -07002571 return setCurrentItemInternal(mCurItem - mLeftIncr, true, false);
2572 }
2573
2574 @Override
2575 public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
2576 super.onRtlPropertiesChanged(layoutDirection);
2577
2578 if (layoutDirection == LAYOUT_DIRECTION_LTR) {
2579 mLeftIncr = -1;
2580 } else {
2581 mLeftIncr = 1;
Alan Viverette75fd8f92015-03-09 15:51:10 -07002582 }
Alan Viverette75fd8f92015-03-09 15:51:10 -07002583 }
2584
2585 /**
2586 * We only want the current page that is being shown to be focusable.
2587 */
2588 @Override
2589 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
2590 final int focusableCount = views.size();
2591
2592 final int descendantFocusability = getDescendantFocusability();
2593
2594 if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
2595 for (int i = 0; i < getChildCount(); i++) {
2596 final View child = getChildAt(i);
2597 if (child.getVisibility() == VISIBLE) {
2598 ItemInfo ii = infoForChild(child);
2599 if (ii != null && ii.position == mCurItem) {
2600 child.addFocusables(views, direction, focusableMode);
2601 }
2602 }
2603 }
2604 }
2605
2606 // we add ourselves (if focusable) in all cases except for when we are
2607 // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is
2608 // to avoid the focus search finding layouts when a more precise search
2609 // among the focusable children would be more interesting.
2610 if (
2611 descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
2612 // No focusable descendants
2613 (focusableCount == views.size())) {
2614 // Note that we can't call the superclass here, because it will
2615 // add all views in. So we need to do the same thing View does.
2616 if (!isFocusable()) {
2617 return;
2618 }
2619 if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&
2620 isInTouchMode() && !isFocusableInTouchMode()) {
2621 return;
2622 }
2623 if (views != null) {
2624 views.add(this);
2625 }
2626 }
2627 }
2628
2629 /**
2630 * We only want the current page that is being shown to be touchable.
2631 */
2632 @Override
2633 public void addTouchables(ArrayList<View> views) {
2634 // Note that we don't call super.addTouchables(), which means that
2635 // we don't call View.addTouchables(). This is okay because a ViewPager
2636 // is itself not touchable.
2637 for (int i = 0; i < getChildCount(); i++) {
2638 final View child = getChildAt(i);
2639 if (child.getVisibility() == VISIBLE) {
2640 ItemInfo ii = infoForChild(child);
2641 if (ii != null && ii.position == mCurItem) {
2642 child.addTouchables(views);
2643 }
2644 }
2645 }
2646 }
2647
2648 /**
2649 * We only want the current page that is being shown to be focusable.
2650 */
2651 @Override
2652 protected boolean onRequestFocusInDescendants(int direction,
2653 Rect previouslyFocusedRect) {
2654 int index;
2655 int increment;
2656 int end;
2657 int count = getChildCount();
2658 if ((direction & FOCUS_FORWARD) != 0) {
2659 index = 0;
2660 increment = 1;
2661 end = count;
2662 } else {
2663 index = count - 1;
2664 increment = -1;
2665 end = -1;
2666 }
2667 for (int i = index; i != end; i += increment) {
2668 View child = getChildAt(i);
2669 if (child.getVisibility() == VISIBLE) {
2670 ItemInfo ii = infoForChild(child);
2671 if (ii != null && ii.position == mCurItem) {
2672 if (child.requestFocus(direction, previouslyFocusedRect)) {
2673 return true;
2674 }
2675 }
2676 }
2677 }
2678 return false;
2679 }
2680
2681 @Override
Alan Viverette75fd8f92015-03-09 15:51:10 -07002682 protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
2683 return new LayoutParams();
2684 }
2685
2686 @Override
2687 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
2688 return generateDefaultLayoutParams();
2689 }
2690
2691 @Override
2692 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
2693 return p instanceof LayoutParams && super.checkLayoutParams(p);
2694 }
2695
2696 @Override
2697 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
2698 return new LayoutParams(getContext(), attrs);
2699 }
2700
Alan Viverette75fd8f92015-03-09 15:51:10 -07002701
Alan Viverette34457f52015-03-25 13:09:20 -07002702 @Override
2703 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
2704 super.onInitializeAccessibilityEvent(event);
2705
2706 event.setClassName(ViewPager.class.getName());
2707 event.setScrollable(canScroll());
2708
2709 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED && mAdapter != null) {
2710 event.setItemCount(mAdapter.getCount());
2711 event.setFromIndex(mCurItem);
2712 event.setToIndex(mCurItem);
2713 }
2714 }
2715
2716 @Override
2717 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
2718 super.onInitializeAccessibilityNodeInfo(info);
2719
2720 info.setClassName(ViewPager.class.getName());
2721 info.setScrollable(canScroll());
2722
2723 if (canScrollHorizontally(1)) {
2724 info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
Maxim Bogatovac6ffce2015-04-27 13:45:52 -07002725 info.addAction(AccessibilityAction.ACTION_SCROLL_RIGHT);
Alan Viverette75fd8f92015-03-09 15:51:10 -07002726 }
2727
Alan Viverette34457f52015-03-25 13:09:20 -07002728 if (canScrollHorizontally(-1)) {
2729 info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD);
Maxim Bogatovac6ffce2015-04-27 13:45:52 -07002730 info.addAction(AccessibilityAction.ACTION_SCROLL_LEFT);
Alan Viverette34457f52015-03-25 13:09:20 -07002731 }
2732 }
2733
2734 @Override
2735 public boolean performAccessibilityAction(int action, Bundle args) {
2736 if (super.performAccessibilityAction(action, args)) {
2737 return true;
Alan Viverette75fd8f92015-03-09 15:51:10 -07002738 }
2739
Alan Viverette34457f52015-03-25 13:09:20 -07002740 switch (action) {
2741 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
Maxim Bogatovac6ffce2015-04-27 13:45:52 -07002742 case R.id.accessibilityActionScrollRight:
Alan Viverette34457f52015-03-25 13:09:20 -07002743 if (canScrollHorizontally(1)) {
2744 setCurrentItem(mCurItem + 1);
2745 return true;
2746 }
2747 return false;
2748 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
Maxim Bogatovac6ffce2015-04-27 13:45:52 -07002749 case R.id.accessibilityActionScrollLeft:
Alan Viverette34457f52015-03-25 13:09:20 -07002750 if (canScrollHorizontally(-1)) {
2751 setCurrentItem(mCurItem - 1);
2752 return true;
2753 }
2754 return false;
Alan Viverette75fd8f92015-03-09 15:51:10 -07002755 }
2756
Alan Viverette34457f52015-03-25 13:09:20 -07002757 return false;
2758 }
2759
2760 private boolean canScroll() {
2761 return mAdapter != null && mAdapter.getCount() > 1;
Alan Viverette75fd8f92015-03-09 15:51:10 -07002762 }
2763
2764 private class PagerObserver extends DataSetObserver {
2765 @Override
2766 public void onChanged() {
2767 dataSetChanged();
2768 }
2769 @Override
2770 public void onInvalidated() {
2771 dataSetChanged();
2772 }
2773 }
2774
2775 /**
2776 * Layout parameters that should be supplied for views added to a
2777 * ViewPager.
2778 */
2779 public static class LayoutParams extends ViewGroup.LayoutParams {
2780 /**
2781 * true if this view is a decoration on the pager itself and not
2782 * a view supplied by the adapter.
2783 */
2784 public boolean isDecor;
2785
2786 /**
2787 * Gravity setting for use on decor views only:
2788 * Where to position the view page within the overall ViewPager
2789 * container; constants are defined in {@link android.view.Gravity}.
2790 */
2791 public int gravity;
2792
2793 /**
2794 * Width as a 0-1 multiplier of the measured pager width
2795 */
2796 float widthFactor = 0.f;
2797
2798 /**
2799 * true if this view was added during layout and needs to be measured
2800 * before being positioned.
2801 */
2802 boolean needsMeasure;
2803
2804 /**
2805 * Adapter position this view is for if !isDecor
2806 */
2807 int position;
2808
2809 /**
2810 * Current child index within the ViewPager that this view occupies
2811 */
2812 int childIndex;
2813
2814 public LayoutParams() {
2815 super(FILL_PARENT, FILL_PARENT);
2816 }
2817
2818 public LayoutParams(Context context, AttributeSet attrs) {
2819 super(context, attrs);
2820
2821 final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
2822 gravity = a.getInteger(0, Gravity.TOP);
2823 a.recycle();
2824 }
2825 }
2826
2827 static class ViewPositionComparator implements Comparator<View> {
2828 @Override
2829 public int compare(View lhs, View rhs) {
2830 final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
2831 final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
2832 if (llp.isDecor != rlp.isDecor) {
2833 return llp.isDecor ? 1 : -1;
2834 }
2835 return llp.position - rlp.position;
2836 }
2837 }
2838}