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