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