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