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