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