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