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