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