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