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