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