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