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