ViewPager.java revision 91eec7fd0b6ffdbb44cd13777950552f74f654e6
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 mFirstLayout = true; 331 if (mRestoredCurItem >= 0) { 332 mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); 333 setCurrentItemInternal(mRestoredCurItem, false, true); 334 mRestoredCurItem = -1; 335 mRestoredAdapterState = null; 336 mRestoredClassLoader = null; 337 } else { 338 populate(); 339 } 340 } 341 342 if (mAdapterChangeListener != null && oldAdapter != adapter) { 343 mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); 344 } 345 } 346 347 private void removeNonDecorViews() { 348 for (int i = 0; i < getChildCount(); i++) { 349 final View child = getChildAt(i); 350 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 351 if (!lp.isDecor) { 352 removeViewAt(i); 353 i--; 354 } 355 } 356 } 357 358 /** 359 * Retrieve the current adapter supplying pages. 360 * 361 * @return The currently registered PagerAdapter 362 */ 363 public PagerAdapter getAdapter() { 364 return mAdapter; 365 } 366 367 void setOnAdapterChangeListener(OnAdapterChangeListener listener) { 368 mAdapterChangeListener = listener; 369 } 370 371 /** 372 * Set the currently selected page. If the ViewPager has already been through its first 373 * layout with its current adapter there will be a smooth animated transition between 374 * the current item and the specified item. 375 * 376 * @param item Item index to select 377 */ 378 public void setCurrentItem(int item) { 379 mPopulatePending = false; 380 setCurrentItemInternal(item, !mFirstLayout, false); 381 } 382 383 /** 384 * Set the currently selected page. 385 * 386 * @param item Item index to select 387 * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately 388 */ 389 public void setCurrentItem(int item, boolean smoothScroll) { 390 mPopulatePending = false; 391 setCurrentItemInternal(item, smoothScroll, false); 392 } 393 394 public int getCurrentItem() { 395 return mCurItem; 396 } 397 398 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { 399 setCurrentItemInternal(item, smoothScroll, always, 0); 400 } 401 402 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { 403 if (mAdapter == null || mAdapter.getCount() <= 0) { 404 setScrollingCacheEnabled(false); 405 return; 406 } 407 if (!always && mCurItem == item && mItems.size() != 0) { 408 setScrollingCacheEnabled(false); 409 return; 410 } 411 if (item < 0) { 412 item = 0; 413 } else if (item >= mAdapter.getCount()) { 414 item = mAdapter.getCount() - 1; 415 } 416 final int pageLimit = mOffscreenPageLimit; 417 if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { 418 // We are doing a jump by more than one page. To avoid 419 // glitches, we want to keep all current pages in the view 420 // until the scroll ends. 421 for (int i=0; i<mItems.size(); i++) { 422 mItems.get(i).scrolling = true; 423 } 424 } 425 final boolean dispatchSelected = mCurItem != item; 426 populate(item); 427 final ItemInfo curInfo = infoForPosition(item); 428 final int destX = curInfo != null ? (int) (getWidth() * curInfo.offset) : 0; 429 if (smoothScroll) { 430 smoothScrollTo(destX, 0, velocity); 431 if (dispatchSelected && mOnPageChangeListener != null) { 432 mOnPageChangeListener.onPageSelected(item); 433 } 434 if (dispatchSelected && mInternalPageChangeListener != null) { 435 mInternalPageChangeListener.onPageSelected(item); 436 } 437 } else { 438 if (dispatchSelected && mOnPageChangeListener != null) { 439 mOnPageChangeListener.onPageSelected(item); 440 } 441 if (dispatchSelected && mInternalPageChangeListener != null) { 442 mInternalPageChangeListener.onPageSelected(item); 443 } 444 completeScroll(); 445 scrollTo(destX, 0); 446 } 447 } 448 449 /** 450 * Set a listener that will be invoked whenever the page changes or is incrementally 451 * scrolled. See {@link OnPageChangeListener}. 452 * 453 * @param listener Listener to set 454 */ 455 public void setOnPageChangeListener(OnPageChangeListener listener) { 456 mOnPageChangeListener = listener; 457 } 458 459 /** 460 * Set a separate OnPageChangeListener for internal use by the support library. 461 * 462 * @param listener Listener to set 463 * @return The old listener that was set, if any. 464 */ 465 OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { 466 OnPageChangeListener oldListener = mInternalPageChangeListener; 467 mInternalPageChangeListener = listener; 468 return oldListener; 469 } 470 471 /** 472 * Returns the number of pages that will be retained to either side of the 473 * current page in the view hierarchy in an idle state. Defaults to 1. 474 * 475 * @return How many pages will be kept offscreen on either side 476 * @see #setOffscreenPageLimit(int) 477 */ 478 public int getOffscreenPageLimit() { 479 return mOffscreenPageLimit; 480 } 481 482 /** 483 * Set the number of pages that should be retained to either side of the 484 * current page in the view hierarchy in an idle state. Pages beyond this 485 * limit will be recreated from the adapter when needed. 486 * 487 * <p>This is offered as an optimization. If you know in advance the number 488 * of pages you will need to support or have lazy-loading mechanisms in place 489 * on your pages, tweaking this setting can have benefits in perceived smoothness 490 * of paging animations and interaction. If you have a small number of pages (3-4) 491 * that you can keep active all at once, less time will be spent in layout for 492 * newly created view subtrees as the user pages back and forth.</p> 493 * 494 * <p>You should keep this limit low, especially if your pages have complex layouts. 495 * This setting defaults to 1.</p> 496 * 497 * @param limit How many pages will be kept offscreen in an idle state. 498 */ 499 public void setOffscreenPageLimit(int limit) { 500 if (limit < DEFAULT_OFFSCREEN_PAGES) { 501 Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + 502 DEFAULT_OFFSCREEN_PAGES); 503 limit = DEFAULT_OFFSCREEN_PAGES; 504 } 505 if (limit != mOffscreenPageLimit) { 506 mOffscreenPageLimit = limit; 507 populate(); 508 } 509 } 510 511 /** 512 * Set the margin between pages. 513 * 514 * @param marginPixels Distance between adjacent pages in pixels 515 * @see #getPageMargin() 516 * @see #setPageMarginDrawable(Drawable) 517 * @see #setPageMarginDrawable(int) 518 */ 519 public void setPageMargin(int marginPixels) { 520 final int oldMargin = mPageMargin; 521 mPageMargin = marginPixels; 522 523 final int width = getWidth(); 524 recomputeScrollPosition(width, width, marginPixels, oldMargin); 525 526 requestLayout(); 527 } 528 529 /** 530 * Return the margin between pages. 531 * 532 * @return The size of the margin in pixels 533 */ 534 public int getPageMargin() { 535 return mPageMargin; 536 } 537 538 /** 539 * Set a drawable that will be used to fill the margin between pages. 540 * 541 * @param d Drawable to display between pages 542 */ 543 public void setPageMarginDrawable(Drawable d) { 544 mMarginDrawable = d; 545 if (d != null) refreshDrawableState(); 546 setWillNotDraw(d == null); 547 invalidate(); 548 } 549 550 /** 551 * Set a drawable that will be used to fill the margin between pages. 552 * 553 * @param resId Resource ID of a drawable to display between pages 554 */ 555 public void setPageMarginDrawable(int resId) { 556 setPageMarginDrawable(getContext().getResources().getDrawable(resId)); 557 } 558 559 @Override 560 protected boolean verifyDrawable(Drawable who) { 561 return super.verifyDrawable(who) || who == mMarginDrawable; 562 } 563 564 @Override 565 protected void drawableStateChanged() { 566 super.drawableStateChanged(); 567 final Drawable d = mMarginDrawable; 568 if (d != null && d.isStateful()) { 569 d.setState(getDrawableState()); 570 } 571 } 572 573 // We want the duration of the page snap animation to be influenced by the distance that 574 // the screen has to travel, however, we don't want this duration to be effected in a 575 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 576 // of travel has on the overall snap duration. 577 float distanceInfluenceForSnapDuration(float f) { 578 f -= 0.5f; // center the values about 0. 579 f *= 0.3f * Math.PI / 2.0f; 580 return (float) Math.sin(f); 581 } 582 583 /** 584 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 585 * 586 * @param x the number of pixels to scroll by on the X axis 587 * @param y the number of pixels to scroll by on the Y axis 588 */ 589 void smoothScrollTo(int x, int y) { 590 smoothScrollTo(x, y, 0); 591 } 592 593 /** 594 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 595 * 596 * @param x the number of pixels to scroll by on the X axis 597 * @param y the number of pixels to scroll by on the Y axis 598 * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) 599 */ 600 void smoothScrollTo(int x, int y, int velocity) { 601 if (getChildCount() == 0) { 602 // Nothing to do. 603 setScrollingCacheEnabled(false); 604 return; 605 } 606 int sx = getScrollX(); 607 int sy = getScrollY(); 608 int dx = x - sx; 609 int dy = y - sy; 610 if (dx == 0 && dy == 0) { 611 completeScroll(); 612 setScrollState(SCROLL_STATE_IDLE); 613 return; 614 } 615 616 setScrollingCacheEnabled(true); 617 mScrolling = true; 618 setScrollState(SCROLL_STATE_SETTLING); 619 620 final int width = getWidth(); 621 final int halfWidth = width / 2; 622 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); 623 final float distance = halfWidth + halfWidth * 624 distanceInfluenceForSnapDuration(distanceRatio); 625 626 int duration = 0; 627 velocity = Math.abs(velocity); 628 if (velocity > 0) { 629 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 630 } else { 631 final float pageWidth = width * mAdapter.getPageWidth(mCurItem); 632 final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); 633 duration = (int) ((pageDelta + 1) * 100); 634 } 635 duration = Math.min(duration, MAX_SETTLE_DURATION); 636 637 mScroller.startScroll(sx, sy, dx, dy, duration); 638 invalidate(); 639 } 640 641 ItemInfo addNewItem(int position, int index) { 642 ItemInfo ii = new ItemInfo(); 643 ii.position = position; 644 ii.object = mAdapter.instantiateItem(this, position); 645 ii.widthFactor = mAdapter.getPageWidth(position); 646 if (index < 0 || index >= mItems.size()) { 647 mItems.add(ii); 648 } else { 649 mItems.add(index, ii); 650 } 651 return ii; 652 } 653 654 void dataSetChanged() { 655 // This method only gets called if our observer is attached, so mAdapter is non-null. 656 657 boolean needPopulate = mItems.size() < 3 && mItems.size() < mAdapter.getCount(); 658 int newCurrItem = -1; 659 660 boolean isUpdating = false; 661 for (int i = 0; i < mItems.size(); i++) { 662 final ItemInfo ii = mItems.get(i); 663 final int newPos = mAdapter.getItemPosition(ii.object); 664 665 if (newPos == PagerAdapter.POSITION_UNCHANGED) { 666 continue; 667 } 668 669 if (newPos == PagerAdapter.POSITION_NONE) { 670 mItems.remove(i); 671 i--; 672 673 if (!isUpdating) { 674 mAdapter.startUpdate(this); 675 isUpdating = true; 676 } 677 678 mAdapter.destroyItem(this, ii.position, ii.object); 679 needPopulate = true; 680 681 if (mCurItem == ii.position) { 682 // Keep the current item in the valid range 683 newCurrItem = Math.max(0, Math.min(mCurItem, mAdapter.getCount() - 1)); 684 } 685 continue; 686 } 687 688 if (ii.position != newPos) { 689 if (ii.position == mCurItem) { 690 // Our current item changed position. Follow it. 691 newCurrItem = newPos; 692 } 693 694 ii.position = newPos; 695 needPopulate = true; 696 } 697 } 698 699 if (isUpdating) { 700 mAdapter.finishUpdate(this); 701 } 702 703 Collections.sort(mItems, COMPARATOR); 704 705 if (needPopulate) { 706 // Reset our known page widths; populate will recompute them. 707 final int childCount = getChildCount(); 708 for (int i = 0; i < childCount; i++) { 709 final View child = getChildAt(i); 710 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 711 if (!lp.isDecor) { 712 lp.widthFactor = 0.f; 713 } 714 } 715 } 716 717 if (newCurrItem >= 0) { 718 // TODO This currently causes a jump. 719 setCurrentItemInternal(newCurrItem, false, true); 720 needPopulate = true; 721 } 722 if (needPopulate) { 723 populate(); 724 requestLayout(); 725 } 726 } 727 728 void populate() { 729 populate(mCurItem); 730 } 731 732 void populate(int newCurrentItem) { 733 ItemInfo oldCurInfo = null; 734 if (mCurItem != newCurrentItem) { 735 oldCurInfo = infoForPosition(mCurItem); 736 mCurItem = newCurrentItem; 737 } 738 739 if (mAdapter == null) { 740 return; 741 } 742 743 // Bail now if we are waiting to populate. This is to hold off 744 // on creating views from the time the user releases their finger to 745 // fling to a new position until we have finished the scroll to 746 // that position, avoiding glitches from happening at that point. 747 if (mPopulatePending) { 748 if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 749 return; 750 } 751 752 // Also, don't populate until we are attached to a window. This is to 753 // avoid trying to populate before we have restored our view hierarchy 754 // state and conflicting with what is restored. 755 if (getWindowToken() == null) { 756 return; 757 } 758 759 mAdapter.startUpdate(this); 760 761 final int pageLimit = mOffscreenPageLimit; 762 final int startPos = Math.max(0, mCurItem - pageLimit); 763 final int N = mAdapter.getCount(); 764 final int endPos = Math.min(N-1, mCurItem + pageLimit); 765 766 // Locate the currently focused item or add it if needed. 767 int curIndex = -1; 768 ItemInfo curItem = null; 769 for (curIndex = 0; curIndex < mItems.size(); curIndex++) { 770 final ItemInfo ii = mItems.get(curIndex); 771 if (ii.position >= mCurItem) { 772 if (ii.position == mCurItem) curItem = ii; 773 break; 774 } 775 } 776 777 if (curItem == null && N > 0) { 778 curItem = addNewItem(mCurItem, curIndex); 779 } 780 781 // Fill 3x the available width or up to the number of offscreen 782 // pages requested to either side, whichever is larger. 783 // If we have no current item we have no work to do. 784 if (curItem != null) { 785 float extraWidthLeft = 0.f; 786 int itemIndex = curIndex - 1; 787 ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 788 for (int pos = mCurItem - 1; pos >= 0; pos--) { 789 if (extraWidthLeft >= 1.f && pos < startPos) { 790 if (ii == null) { 791 break; 792 } 793 if (pos == ii.position && !ii.scrolling) { 794 mItems.remove(itemIndex); 795 mAdapter.destroyItem(this, pos, ii.object); 796 itemIndex--; 797 curIndex--; 798 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 799 } 800 } else if (ii != null && pos == ii.position) { 801 extraWidthLeft += ii.widthFactor; 802 itemIndex--; 803 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 804 } else { 805 ii = addNewItem(pos, itemIndex + 1); 806 extraWidthLeft += ii.widthFactor; 807 curIndex++; 808 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 809 } 810 } 811 812 float extraWidthRight = curItem.widthFactor; 813 itemIndex = curIndex + 1; 814 if (extraWidthRight < 2.f) { 815 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 816 for (int pos = mCurItem + 1; pos < N; pos++) { 817 if (extraWidthRight >= 2.f && pos > endPos) { 818 if (ii == null) { 819 break; 820 } 821 if (pos == ii.position && !ii.scrolling) { 822 mItems.remove(itemIndex); 823 mAdapter.destroyItem(this, pos, ii.object); 824 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 825 } 826 } else if (ii != null && pos == ii.position) { 827 extraWidthRight += ii.widthFactor; 828 itemIndex++; 829 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 830 } else { 831 ii = addNewItem(pos, itemIndex); 832 itemIndex++; 833 extraWidthRight += ii.widthFactor; 834 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 835 } 836 } 837 } 838 839 calculatePageOffsets(curItem, curIndex, oldCurInfo); 840 } 841 842 if (DEBUG) { 843 Log.i(TAG, "Current page list:"); 844 for (int i=0; i<mItems.size(); i++) { 845 Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); 846 } 847 } 848 849 mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); 850 851 mAdapter.finishUpdate(this); 852 853 // Check width measurement of current pages. Update LayoutParams as needed. 854 final int childCount = getChildCount(); 855 for (int i = 0; i < childCount; i++) { 856 final View child = getChildAt(i); 857 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 858 if (!lp.isDecor && lp.widthFactor == 0.f) { 859 // 0 means requery the adapter for this, it doesn't have a valid width. 860 final ItemInfo ii = infoForChild(child); 861 if (ii != null) { 862 lp.widthFactor = ii.widthFactor; 863 } 864 } 865 } 866 867 if (hasFocus()) { 868 View currentFocused = findFocus(); 869 ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; 870 if (ii == null || ii.position != mCurItem) { 871 for (int i=0; i<getChildCount(); i++) { 872 View child = getChildAt(i); 873 ii = infoForChild(child); 874 if (ii != null && ii.position == mCurItem) { 875 if (child.requestFocus(FOCUS_FORWARD)) { 876 break; 877 } 878 } 879 } 880 } 881 } 882 } 883 884 private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { 885 final int N = mAdapter.getCount(); 886 final int width = getWidth(); 887 final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; 888 // Fix up offsets for later layout. 889 if (oldCurInfo != null) { 890 final int oldCurPosition = oldCurInfo.position; 891 // Base offsets off of oldCurInfo. 892 if (oldCurPosition < curItem.position) { 893 int itemIndex = 0; 894 ItemInfo ii = null; 895 float offset = oldCurInfo.offset + oldCurInfo.widthFactor; 896 for (int pos = oldCurPosition + 1; 897 pos <= curItem.position && itemIndex < mItems.size(); pos++) { 898 ii = mItems.get(itemIndex); 899 while (pos > ii.position && itemIndex < mItems.size() - 1) { 900 itemIndex++; 901 ii = mItems.get(itemIndex); 902 } 903 while (pos < ii.position) { 904 // We don't have an item populated for this, 905 // ask the adapter for an offset. 906 offset += mAdapter.getPageWidth(pos) + marginOffset; 907 pos++; 908 } 909 ii.offset = offset; 910 offset += ii.widthFactor + marginOffset; 911 } 912 } else if (oldCurPosition > curItem.position) { 913 int itemIndex = mItems.size() - 1; 914 ItemInfo ii = null; 915 float offset = oldCurInfo.offset; 916 for (int pos = oldCurPosition - 1; 917 pos >= curItem.position && itemIndex >= 0; pos--) { 918 ii = mItems.get(itemIndex); 919 while (pos < ii.position && itemIndex > 0) { 920 itemIndex--; 921 ii = mItems.get(itemIndex); 922 } 923 while (pos > ii.position) { 924 // We don't have an item populated for this, 925 // ask the adapter for an offset. 926 offset -= mAdapter.getPageWidth(pos) + marginOffset; 927 pos--; 928 } 929 offset -= ii.widthFactor + marginOffset; 930 ii.offset = offset; 931 } 932 } 933 } 934 935 // Base all offsets off of curItem. 936 final int itemCount = mItems.size(); 937 float offset = curItem.offset; 938 int pos = curItem.position - 1; 939 mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; 940 mLastOffset = curItem.position == N - 1 ? curItem.offset : Float.MAX_VALUE; 941 // Previous pages 942 for (int i = curIndex - 1; i >= 0; i--, pos--) { 943 final ItemInfo ii = mItems.get(i); 944 while (pos > ii.position) { 945 offset -= mAdapter.getPageWidth(pos--) + marginOffset; 946 } 947 offset -= ii.widthFactor + marginOffset; 948 ii.offset = offset; 949 if (ii.position == 0) mFirstOffset = offset; 950 } 951 offset = curItem.offset + curItem.widthFactor + marginOffset; 952 pos = curItem.position + 1; 953 // Next pages 954 for (int i = curIndex + 1; i < itemCount; i++, pos++) { 955 final ItemInfo ii = mItems.get(i); 956 while (pos < ii.position) { 957 offset += mAdapter.getPageWidth(pos++) + marginOffset; 958 } 959 if (ii.position == N - 1) mLastOffset = offset; 960 ii.offset = offset; 961 offset += ii.widthFactor + marginOffset; 962 } 963 964 mNeedCalculatePageOffsets = false; 965 } 966 967 public static class SavedState extends BaseSavedState { 968 int position; 969 Parcelable adapterState; 970 ClassLoader loader; 971 972 public SavedState(Parcelable superState) { 973 super(superState); 974 } 975 976 @Override 977 public void writeToParcel(Parcel out, int flags) { 978 super.writeToParcel(out, flags); 979 out.writeInt(position); 980 out.writeParcelable(adapterState, flags); 981 } 982 983 @Override 984 public String toString() { 985 return "FragmentPager.SavedState{" 986 + Integer.toHexString(System.identityHashCode(this)) 987 + " position=" + position + "}"; 988 } 989 990 public static final Parcelable.Creator<SavedState> CREATOR 991 = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() { 992 @Override 993 public SavedState createFromParcel(Parcel in, ClassLoader loader) { 994 return new SavedState(in, loader); 995 } 996 @Override 997 public SavedState[] newArray(int size) { 998 return new SavedState[size]; 999 } 1000 }); 1001 1002 SavedState(Parcel in, ClassLoader loader) { 1003 super(in); 1004 if (loader == null) { 1005 loader = getClass().getClassLoader(); 1006 } 1007 position = in.readInt(); 1008 adapterState = in.readParcelable(loader); 1009 this.loader = loader; 1010 } 1011 } 1012 1013 @Override 1014 public Parcelable onSaveInstanceState() { 1015 Parcelable superState = super.onSaveInstanceState(); 1016 SavedState ss = new SavedState(superState); 1017 ss.position = mCurItem; 1018 if (mAdapter != null) { 1019 ss.adapterState = mAdapter.saveState(); 1020 } 1021 return ss; 1022 } 1023 1024 @Override 1025 public void onRestoreInstanceState(Parcelable state) { 1026 if (!(state instanceof SavedState)) { 1027 super.onRestoreInstanceState(state); 1028 return; 1029 } 1030 1031 SavedState ss = (SavedState)state; 1032 super.onRestoreInstanceState(ss.getSuperState()); 1033 1034 if (mAdapter != null) { 1035 mAdapter.restoreState(ss.adapterState, ss.loader); 1036 setCurrentItemInternal(ss.position, false, true); 1037 } else { 1038 mRestoredCurItem = ss.position; 1039 mRestoredAdapterState = ss.adapterState; 1040 mRestoredClassLoader = ss.loader; 1041 } 1042 } 1043 1044 @Override 1045 public void addView(View child, int index, ViewGroup.LayoutParams params) { 1046 if (!checkLayoutParams(params)) { 1047 params = generateLayoutParams(params); 1048 } 1049 final LayoutParams lp = (LayoutParams) params; 1050 lp.isDecor |= child instanceof Decor; 1051 if (mInLayout) { 1052 if (lp != null && lp.isDecor) { 1053 throw new IllegalStateException("Cannot add pager decor view during layout"); 1054 } 1055 lp.needsMeasure = true; 1056 addViewInLayout(child, index, params); 1057 } else { 1058 super.addView(child, index, params); 1059 } 1060 1061 if (USE_CACHE) { 1062 if (child.getVisibility() != GONE) { 1063 child.setDrawingCacheEnabled(mScrollingCacheEnabled); 1064 } else { 1065 child.setDrawingCacheEnabled(false); 1066 } 1067 } 1068 } 1069 1070 ItemInfo infoForChild(View child) { 1071 for (int i=0; i<mItems.size(); i++) { 1072 ItemInfo ii = mItems.get(i); 1073 if (mAdapter.isViewFromObject(child, ii.object)) { 1074 return ii; 1075 } 1076 } 1077 return null; 1078 } 1079 1080 ItemInfo infoForAnyChild(View child) { 1081 ViewParent parent; 1082 while ((parent=child.getParent()) != this) { 1083 if (parent == null || !(parent instanceof View)) { 1084 return null; 1085 } 1086 child = (View)parent; 1087 } 1088 return infoForChild(child); 1089 } 1090 1091 ItemInfo infoForPosition(int position) { 1092 for (int i = 0; i < mItems.size(); i++) { 1093 ItemInfo ii = mItems.get(i); 1094 if (ii.position == position) { 1095 return ii; 1096 } 1097 } 1098 return null; 1099 } 1100 1101 @Override 1102 protected void onAttachedToWindow() { 1103 super.onAttachedToWindow(); 1104 mFirstLayout = true; 1105 } 1106 1107 @Override 1108 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1109 // For simple implementation, or internal size is always 0. 1110 // We depend on the container to specify the layout size of 1111 // our view. We can't really know what it is since we will be 1112 // adding and removing different arbitrary views and do not 1113 // want the layout to change as this happens. 1114 setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), 1115 getDefaultSize(0, heightMeasureSpec)); 1116 1117 // Children are just made to fill our space. 1118 int childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 1119 int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); 1120 1121 /* 1122 * Make sure all children have been properly measured. Decor views first. 1123 * Right now we cheat and make this less complicated by assuming decor 1124 * views won't intersect. We will pin to edges based on gravity. 1125 */ 1126 int size = getChildCount(); 1127 for (int i = 0; i < size; ++i) { 1128 final View child = getChildAt(i); 1129 if (child.getVisibility() != GONE) { 1130 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1131 if (lp != null && lp.isDecor) { 1132 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1133 final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1134 int widthMode = MeasureSpec.AT_MOST; 1135 int heightMode = MeasureSpec.AT_MOST; 1136 boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; 1137 boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT; 1138 1139 if (consumeVertical) { 1140 widthMode = MeasureSpec.EXACTLY; 1141 } else if (consumeHorizontal) { 1142 heightMode = MeasureSpec.EXACTLY; 1143 } 1144 1145 int widthSize = childWidthSize; 1146 int heightSize = childHeightSize; 1147 if (lp.width != LayoutParams.WRAP_CONTENT) { 1148 widthMode = MeasureSpec.EXACTLY; 1149 if (lp.width != LayoutParams.FILL_PARENT) { 1150 widthSize = lp.width; 1151 } 1152 } 1153 if (lp.height != LayoutParams.WRAP_CONTENT) { 1154 heightMode = MeasureSpec.EXACTLY; 1155 if (lp.height != LayoutParams.FILL_PARENT) { 1156 heightSize = lp.height; 1157 } 1158 } 1159 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); 1160 final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); 1161 child.measure(widthSpec, heightSpec); 1162 1163 if (consumeVertical) { 1164 childHeightSize -= child.getMeasuredHeight(); 1165 } else if (consumeHorizontal) { 1166 childWidthSize -= child.getMeasuredWidth(); 1167 } 1168 } 1169 } 1170 } 1171 1172 mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); 1173 mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); 1174 1175 // Make sure we have created all fragments that we need to have shown. 1176 mInLayout = true; 1177 populate(); 1178 mInLayout = false; 1179 1180 // Page views next. 1181 size = getChildCount(); 1182 for (int i = 0; i < size; ++i) { 1183 final View child = getChildAt(i); 1184 if (child.getVisibility() != GONE) { 1185 if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child 1186 + ": " + mChildWidthMeasureSpec); 1187 1188 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1189 if (lp == null || !lp.isDecor) { 1190 final int widthSpec = MeasureSpec.makeMeasureSpec( 1191 (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); 1192 child.measure(widthSpec, mChildHeightMeasureSpec); 1193 } 1194 } 1195 } 1196 } 1197 1198 @Override 1199 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1200 super.onSizeChanged(w, h, oldw, oldh); 1201 1202 // Make sure scroll position is set correctly. 1203 if (w != oldw) { 1204 recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); 1205 } 1206 } 1207 1208 private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) { 1209 if (oldWidth > 0 && !mItems.isEmpty()) { 1210 final int xpos = getScrollX(); 1211 final ItemInfo ii = infoForCurrentScrollPosition(); 1212 final float pageOffset = (((float) xpos / width) - ii.offset) / ii.widthFactor; 1213 final int offsetPixels = (int) (pageOffset * width); 1214 1215 scrollTo(offsetPixels, getScrollY()); 1216 if (!mScroller.isFinished()) { 1217 // We now return to your regularly scheduled scroll, already in progress. 1218 final int newDuration = mScroller.getDuration() - mScroller.timePassed(); 1219 ItemInfo targetInfo = infoForPosition(mCurItem); 1220 mScroller.startScroll(offsetPixels, 0, (int) (targetInfo.offset * width), 0, 1221 newDuration); 1222 } 1223 } else { 1224 final ItemInfo ii = infoForPosition(mCurItem); 1225 final int scrollPos = (int) ((ii != null ? ii.offset : 0) * width); 1226 if (scrollPos != getScrollX()) { 1227 completeScroll(); 1228 scrollTo(scrollPos, getScrollY()); 1229 } 1230 } 1231 } 1232 1233 @Override 1234 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1235 mInLayout = true; 1236 populate(); 1237 mInLayout = false; 1238 1239 final int count = getChildCount(); 1240 int width = r - l; 1241 int height = b - t; 1242 int paddingLeft = getPaddingLeft(); 1243 int paddingTop = getPaddingTop(); 1244 int paddingRight = getPaddingRight(); 1245 int paddingBottom = getPaddingBottom(); 1246 final int scrollX = getScrollX(); 1247 1248 int decorCount = 0; 1249 1250 // First pass - decor views. We need to do this in two passes so that 1251 // we have the proper offsets for non-decor views later. 1252 for (int i = 0; i < count; i++) { 1253 final View child = getChildAt(i); 1254 if (child.getVisibility() != GONE) { 1255 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1256 int childLeft = 0; 1257 int childTop = 0; 1258 if (lp.isDecor) { 1259 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1260 final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1261 switch (hgrav) { 1262 default: 1263 childLeft = paddingLeft; 1264 break; 1265 case Gravity.LEFT: 1266 childLeft = paddingLeft; 1267 paddingLeft += child.getMeasuredWidth(); 1268 break; 1269 case Gravity.CENTER_HORIZONTAL: 1270 childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1271 paddingLeft); 1272 break; 1273 case Gravity.RIGHT: 1274 childLeft = width - paddingRight - child.getMeasuredWidth(); 1275 paddingRight += child.getMeasuredWidth(); 1276 break; 1277 } 1278 switch (vgrav) { 1279 default: 1280 childTop = paddingTop; 1281 break; 1282 case Gravity.TOP: 1283 childTop = paddingTop; 1284 paddingTop += child.getMeasuredHeight(); 1285 break; 1286 case Gravity.CENTER_VERTICAL: 1287 childTop = Math.max((height - child.getMeasuredHeight()) / 2, 1288 paddingTop); 1289 break; 1290 case Gravity.BOTTOM: 1291 childTop = height - paddingBottom - child.getMeasuredHeight(); 1292 paddingBottom += child.getMeasuredHeight(); 1293 break; 1294 } 1295 childLeft += scrollX; 1296 child.layout(childLeft, childTop, 1297 childLeft + child.getMeasuredWidth(), 1298 childTop + child.getMeasuredHeight()); 1299 decorCount++; 1300 } 1301 } 1302 } 1303 1304 // Page views. Do this once we have the right padding offsets from above. 1305 for (int i = 0; i < count; i++) { 1306 final View child = getChildAt(i); 1307 if (child.getVisibility() != GONE) { 1308 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1309 ItemInfo ii; 1310 if (!lp.isDecor && (ii = infoForChild(child)) != null) { 1311 int loff = (int) (width * ii.offset); 1312 int childLeft = paddingLeft + loff; 1313 int childTop = paddingTop; 1314 if (lp.needsMeasure) { 1315 // This was added during layout and needs measurement. 1316 // Do it now that we know what we're working with. 1317 lp.needsMeasure = false; 1318 final int widthSpec = MeasureSpec.makeMeasureSpec( 1319 (int) ((width - paddingLeft - paddingRight) * lp.widthFactor), 1320 MeasureSpec.EXACTLY); 1321 final int heightSpec = MeasureSpec.makeMeasureSpec( 1322 (int) (height - paddingTop - paddingBottom), 1323 MeasureSpec.EXACTLY); 1324 child.measure(widthSpec, heightSpec); 1325 } 1326 if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object 1327 + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() 1328 + "x" + child.getMeasuredHeight()); 1329 child.layout(childLeft, childTop, 1330 childLeft + child.getMeasuredWidth(), 1331 childTop + child.getMeasuredHeight()); 1332 } 1333 } 1334 } 1335 mTopPageBounds = paddingTop; 1336 mBottomPageBounds = height - paddingBottom; 1337 mDecorChildCount = decorCount; 1338 mFirstLayout = false; 1339 } 1340 1341 @Override 1342 public void computeScroll() { 1343 if (DEBUG) Log.i(TAG, "computeScroll: finished=" + mScroller.isFinished()); 1344 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { 1345 if (DEBUG) Log.i(TAG, "computeScroll: still scrolling"); 1346 int oldX = getScrollX(); 1347 int oldY = getScrollY(); 1348 int x = mScroller.getCurrX(); 1349 int y = mScroller.getCurrY(); 1350 1351 if (oldX != x || oldY != y) { 1352 scrollTo(x, y); 1353 pageScrolled(x); 1354 } 1355 1356 // Keep on drawing until the animation has finished. 1357 invalidate(); 1358 return; 1359 } 1360 1361 // Done with scroll, clean up state. 1362 completeScroll(); 1363 } 1364 1365 private void pageScrolled(int xpos) { 1366 final ItemInfo ii = infoForCurrentScrollPosition(); 1367 final int width = getWidth(); 1368 final int widthWithMargin = width + mPageMargin; 1369 final float marginOffset = (float) mPageMargin / width; 1370 final int currentPage = ii.position; 1371 final float pageOffset = (((float) xpos / width) - ii.offset) / 1372 (ii.widthFactor + marginOffset); 1373 final int offsetPixels = (int) (pageOffset * widthWithMargin); 1374 1375 mCalledSuper = false; 1376 onPageScrolled(currentPage, pageOffset, offsetPixels); 1377 if (!mCalledSuper) { 1378 throw new IllegalStateException( 1379 "onPageScrolled did not call superclass implementation"); 1380 } 1381 } 1382 1383 /** 1384 * This method will be invoked when the current page is scrolled, either as part 1385 * of a programmatically initiated smooth scroll or a user initiated touch scroll. 1386 * If you override this method you must call through to the superclass implementation 1387 * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled 1388 * returns. 1389 * 1390 * @param position Position index of the first page currently being displayed. 1391 * Page position+1 will be visible if positionOffset is nonzero. 1392 * @param offset Value from [0, 1) indicating the offset from the page at position. 1393 * @param offsetPixels Value in pixels indicating the offset from position. 1394 */ 1395 protected void onPageScrolled(int position, float offset, int offsetPixels) { 1396 // Offset any decor views if needed - keep them on-screen at all times. 1397 if (mDecorChildCount > 0) { 1398 final int scrollX = getScrollX(); 1399 int paddingLeft = getPaddingLeft(); 1400 int paddingRight = getPaddingRight(); 1401 final int width = getWidth(); 1402 final int childCount = getChildCount(); 1403 for (int i = 0; i < childCount; i++) { 1404 final View child = getChildAt(i); 1405 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1406 if (!lp.isDecor) continue; 1407 1408 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1409 int childLeft = 0; 1410 switch (hgrav) { 1411 default: 1412 childLeft = paddingLeft; 1413 break; 1414 case Gravity.LEFT: 1415 childLeft = paddingLeft; 1416 paddingLeft += child.getWidth(); 1417 break; 1418 case Gravity.CENTER_HORIZONTAL: 1419 childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1420 paddingLeft); 1421 break; 1422 case Gravity.RIGHT: 1423 childLeft = width - paddingRight - child.getMeasuredWidth(); 1424 paddingRight += child.getMeasuredWidth(); 1425 break; 1426 } 1427 childLeft += scrollX; 1428 1429 final int childOffset = childLeft - child.getLeft(); 1430 if (childOffset != 0) { 1431 child.offsetLeftAndRight(childOffset); 1432 } 1433 } 1434 } 1435 1436 if (mOnPageChangeListener != null) { 1437 mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1438 } 1439 if (mInternalPageChangeListener != null) { 1440 mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1441 } 1442 mCalledSuper = true; 1443 } 1444 1445 private void completeScroll() { 1446 boolean needPopulate = mScrolling; 1447 if (needPopulate) { 1448 // Done with scroll, no longer want to cache view drawing. 1449 setScrollingCacheEnabled(false); 1450 mScroller.abortAnimation(); 1451 int oldX = getScrollX(); 1452 int oldY = getScrollY(); 1453 int x = mScroller.getCurrX(); 1454 int y = mScroller.getCurrY(); 1455 if (oldX != x || oldY != y) { 1456 scrollTo(x, y); 1457 } 1458 setScrollState(SCROLL_STATE_IDLE); 1459 } 1460 mPopulatePending = false; 1461 mScrolling = false; 1462 for (int i=0; i<mItems.size(); i++) { 1463 ItemInfo ii = mItems.get(i); 1464 if (ii.scrolling) { 1465 needPopulate = true; 1466 ii.scrolling = false; 1467 } 1468 } 1469 if (needPopulate) { 1470 populate(); 1471 } 1472 } 1473 1474 @Override 1475 public boolean onInterceptTouchEvent(MotionEvent ev) { 1476 /* 1477 * This method JUST determines whether we want to intercept the motion. 1478 * If we return true, onMotionEvent will be called and we do the actual 1479 * scrolling there. 1480 */ 1481 1482 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 1483 1484 // Always take care of the touch gesture being complete. 1485 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 1486 // Release the drag. 1487 if (DEBUG) Log.v(TAG, "Intercept done!"); 1488 mIsBeingDragged = false; 1489 mIsUnableToDrag = false; 1490 mActivePointerId = INVALID_POINTER; 1491 if (mVelocityTracker != null) { 1492 mVelocityTracker.recycle(); 1493 mVelocityTracker = null; 1494 } 1495 return false; 1496 } 1497 1498 // Nothing more to do here if we have decided whether or not we 1499 // are dragging. 1500 if (action != MotionEvent.ACTION_DOWN) { 1501 if (mIsBeingDragged) { 1502 if (DEBUG) Log.v(TAG, "Intercept returning true!"); 1503 return true; 1504 } 1505 if (mIsUnableToDrag) { 1506 if (DEBUG) Log.v(TAG, "Intercept returning false!"); 1507 return false; 1508 } 1509 } 1510 1511 switch (action) { 1512 case MotionEvent.ACTION_MOVE: { 1513 /* 1514 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1515 * whether the user has moved far enough from his original down touch. 1516 */ 1517 1518 /* 1519 * Locally do absolute value. mLastMotionY is set to the y value 1520 * of the down event. 1521 */ 1522 final int activePointerId = mActivePointerId; 1523 if (activePointerId == INVALID_POINTER) { 1524 // If we don't have a valid id, the touch down wasn't on content. 1525 break; 1526 } 1527 1528 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 1529 final float x = MotionEventCompat.getX(ev, pointerIndex); 1530 final float dx = x - mLastMotionX; 1531 final float xDiff = Math.abs(dx); 1532 final float y = MotionEventCompat.getY(ev, pointerIndex); 1533 final float yDiff = Math.abs(y - mLastMotionY); 1534 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1535 1536 if (canScroll(this, false, (int) dx, (int) x, (int) y)) { 1537 // Nested view has scrollable area under this point. Let it be handled there. 1538 mInitialMotionX = mLastMotionX = x; 1539 mLastMotionY = y; 1540 return false; 1541 } 1542 if (xDiff > mTouchSlop && xDiff > yDiff) { 1543 if (DEBUG) Log.v(TAG, "Starting drag!"); 1544 mIsBeingDragged = true; 1545 setScrollState(SCROLL_STATE_DRAGGING); 1546 mLastMotionX = x; 1547 setScrollingCacheEnabled(true); 1548 } else { 1549 if (yDiff > mTouchSlop) { 1550 // The finger has moved enough in the vertical 1551 // direction to be counted as a drag... abort 1552 // any attempt to drag horizontally, to work correctly 1553 // with children that have scrolling containers. 1554 if (DEBUG) Log.v(TAG, "Starting unable to drag!"); 1555 mIsUnableToDrag = true; 1556 } 1557 } 1558 break; 1559 } 1560 1561 case MotionEvent.ACTION_DOWN: { 1562 /* 1563 * Remember location of down touch. 1564 * ACTION_DOWN always refers to pointer index 0. 1565 */ 1566 mLastMotionX = mInitialMotionX = ev.getX(); 1567 mLastMotionY = ev.getY(); 1568 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1569 1570 if (mScrollState == SCROLL_STATE_SETTLING) { 1571 // Let the user 'catch' the pager as it animates. 1572 mIsBeingDragged = true; 1573 mIsUnableToDrag = false; 1574 setScrollState(SCROLL_STATE_DRAGGING); 1575 } else { 1576 completeScroll(); 1577 mIsBeingDragged = false; 1578 mIsUnableToDrag = false; 1579 } 1580 1581 if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY 1582 + " mIsBeingDragged=" + mIsBeingDragged 1583 + "mIsUnableToDrag=" + mIsUnableToDrag); 1584 break; 1585 } 1586 1587 case MotionEventCompat.ACTION_POINTER_UP: 1588 onSecondaryPointerUp(ev); 1589 break; 1590 } 1591 1592 if (!mIsBeingDragged) { 1593 // Track the velocity as long as we aren't dragging. 1594 // Once we start a real drag we will track in onTouchEvent. 1595 if (mVelocityTracker == null) { 1596 mVelocityTracker = VelocityTracker.obtain(); 1597 } 1598 mVelocityTracker.addMovement(ev); 1599 } 1600 1601 /* 1602 * The only time we want to intercept motion events is if we are in the 1603 * drag mode. 1604 */ 1605 return mIsBeingDragged; 1606 } 1607 1608 @Override 1609 public boolean onTouchEvent(MotionEvent ev) { 1610 if (mFakeDragging) { 1611 // A fake drag is in progress already, ignore this real one 1612 // but still eat the touch events. 1613 // (It is likely that the user is multi-touching the screen.) 1614 return true; 1615 } 1616 1617 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 1618 // Don't handle edge touches immediately -- they may actually belong to one of our 1619 // descendants. 1620 return false; 1621 } 1622 1623 if (mAdapter == null || mAdapter.getCount() == 0) { 1624 // Nothing to present or scroll; nothing to touch. 1625 return false; 1626 } 1627 1628 if (mVelocityTracker == null) { 1629 mVelocityTracker = VelocityTracker.obtain(); 1630 } 1631 mVelocityTracker.addMovement(ev); 1632 1633 final int action = ev.getAction(); 1634 boolean needsInvalidate = false; 1635 1636 switch (action & MotionEventCompat.ACTION_MASK) { 1637 case MotionEvent.ACTION_DOWN: { 1638 /* 1639 * If being flinged and user touches, stop the fling. isFinished 1640 * will be false if being flinged. 1641 */ 1642 completeScroll(); 1643 1644 // Remember where the motion event started 1645 mLastMotionX = mInitialMotionX = ev.getX(); 1646 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1647 break; 1648 } 1649 case MotionEvent.ACTION_MOVE: 1650 if (!mIsBeingDragged) { 1651 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1652 final float x = MotionEventCompat.getX(ev, pointerIndex); 1653 final float xDiff = Math.abs(x - mLastMotionX); 1654 final float y = MotionEventCompat.getY(ev, pointerIndex); 1655 final float yDiff = Math.abs(y - mLastMotionY); 1656 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1657 if (xDiff > mTouchSlop && xDiff > yDiff) { 1658 if (DEBUG) Log.v(TAG, "Starting drag!"); 1659 mIsBeingDragged = true; 1660 mLastMotionX = x; 1661 setScrollState(SCROLL_STATE_DRAGGING); 1662 setScrollingCacheEnabled(true); 1663 } 1664 } 1665 if (mIsBeingDragged) { 1666 // Scroll to follow the motion event 1667 final int activePointerIndex = MotionEventCompat.findPointerIndex( 1668 ev, mActivePointerId); 1669 final float x = MotionEventCompat.getX(ev, activePointerIndex); 1670 final float deltaX = mLastMotionX - x; 1671 mLastMotionX = x; 1672 float oldScrollX = getScrollX(); 1673 float scrollX = oldScrollX + deltaX; 1674 final int width = getWidth(); 1675 1676 float leftBound = width * mFirstOffset; 1677 float rightBound = width * mLastOffset; 1678 boolean leftAbsolute = true; 1679 boolean rightAbsolute = true; 1680 1681 final ItemInfo firstItem = mItems.get(0); 1682 final ItemInfo lastItem = mItems.get(mItems.size() - 1); 1683 if (firstItem.position != 0) { 1684 leftAbsolute = false; 1685 leftBound = firstItem.offset * width; 1686 } 1687 if (lastItem.position != mAdapter.getCount() - 1) { 1688 rightAbsolute = false; 1689 rightBound = lastItem.offset * width; 1690 } 1691 1692 if (scrollX < leftBound) { 1693 if (leftAbsolute) { 1694 float over = leftBound - scrollX; 1695 needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); 1696 } 1697 scrollX = leftBound; 1698 } else if (scrollX > rightBound) { 1699 if (rightAbsolute) { 1700 float over = scrollX - rightBound; 1701 needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); 1702 } 1703 scrollX = rightBound; 1704 } 1705 // Don't lose the rounded component 1706 mLastMotionX += scrollX - (int) scrollX; 1707 scrollTo((int) scrollX, getScrollY()); 1708 pageScrolled((int) scrollX); 1709 } 1710 break; 1711 case MotionEvent.ACTION_UP: 1712 if (mIsBeingDragged) { 1713 final VelocityTracker velocityTracker = mVelocityTracker; 1714 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1715 int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( 1716 velocityTracker, mActivePointerId); 1717 mPopulatePending = true; 1718 final int width = getWidth(); 1719 final int scrollX = getScrollX(); 1720 final ItemInfo ii = infoForCurrentScrollPosition(); 1721 final int currentPage = ii.position; 1722 final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; 1723 final int activePointerIndex = 1724 MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1725 final float x = MotionEventCompat.getX(ev, activePointerIndex); 1726 final int totalDelta = (int) (x - mInitialMotionX); 1727 int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 1728 totalDelta); 1729 setCurrentItemInternal(nextPage, true, true, initialVelocity); 1730 1731 mActivePointerId = INVALID_POINTER; 1732 endDrag(); 1733 needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); 1734 } 1735 break; 1736 case MotionEvent.ACTION_CANCEL: 1737 if (mIsBeingDragged) { 1738 setCurrentItemInternal(mCurItem, true, true); 1739 mActivePointerId = INVALID_POINTER; 1740 endDrag(); 1741 needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); 1742 } 1743 break; 1744 case MotionEventCompat.ACTION_POINTER_DOWN: { 1745 final int index = MotionEventCompat.getActionIndex(ev); 1746 final float x = MotionEventCompat.getX(ev, index); 1747 mLastMotionX = x; 1748 mActivePointerId = MotionEventCompat.getPointerId(ev, index); 1749 break; 1750 } 1751 case MotionEventCompat.ACTION_POINTER_UP: 1752 onSecondaryPointerUp(ev); 1753 mLastMotionX = MotionEventCompat.getX(ev, 1754 MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 1755 break; 1756 } 1757 if (needsInvalidate) { 1758 invalidate(); 1759 } 1760 return true; 1761 } 1762 1763 /** 1764 * @return Info about the page at the current scroll position. 1765 * This can be synthetic for a missing middle page; the 'object' field can be null. 1766 */ 1767 private ItemInfo infoForCurrentScrollPosition() { 1768 final int width = getWidth(); 1769 final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; 1770 final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; 1771 int lastPos = -1; 1772 float lastOffset = 0.f; 1773 float lastWidth = 0.f; 1774 boolean first = true; 1775 1776 for (int i = 0; i < mItems.size(); i++) { 1777 ItemInfo ii = mItems.get(i); 1778 float offset; 1779 if (!first && ii.position != lastPos + 1) { 1780 // Create a synthetic item for a missing page. 1781 ii = mTempItem; 1782 ii.offset = lastOffset + lastWidth + marginOffset; 1783 ii.position = lastPos + 1; 1784 ii.widthFactor = mAdapter.getPageWidth(ii.position); 1785 i--; 1786 } 1787 offset = ii.offset; 1788 1789 // TODO: These epsilon checks are lame. A better implementation wouldn't 1790 // accumulate floating point values and instead track actual pixel offsets. 1791 final float leftBound = offset - 0.0001f; 1792 final float rightBound = offset + ii.widthFactor + marginOffset + 0.0001f; 1793 if ((first || scrollOffset >= leftBound) && 1794 (scrollOffset < rightBound || i == mItems.size() - 1)) { 1795 return ii; 1796 } 1797 first = false; 1798 lastPos = ii.position; 1799 lastOffset = offset; 1800 lastWidth = ii.widthFactor; 1801 } 1802 1803 return null; 1804 } 1805 1806 private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { 1807 int targetPage; 1808 if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { 1809 targetPage = velocity > 0 ? currentPage : currentPage + 1; 1810 } else { 1811 targetPage = (int) (currentPage + pageOffset + 0.5f); 1812 } 1813 1814 if (mItems.size() > 0) { 1815 final ItemInfo firstItem = mItems.get(0); 1816 final ItemInfo lastItem = mItems.get(mItems.size() - 1); 1817 1818 // Only let the user target pages we have items for 1819 targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); 1820 } 1821 1822 return targetPage; 1823 } 1824 1825 @Override 1826 public void draw(Canvas canvas) { 1827 super.draw(canvas); 1828 boolean needsInvalidate = false; 1829 1830 final int overScrollMode = ViewCompat.getOverScrollMode(this); 1831 if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 1832 (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && 1833 mAdapter != null && mAdapter.getCount() > 1)) { 1834 if (!mLeftEdge.isFinished()) { 1835 final int restoreCount = canvas.save(); 1836 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 1837 final int width = getWidth(); 1838 1839 canvas.rotate(270); 1840 canvas.translate(-height + getPaddingTop(), mFirstOffset * width); 1841 mLeftEdge.setSize(height, width); 1842 needsInvalidate |= mLeftEdge.draw(canvas); 1843 canvas.restoreToCount(restoreCount); 1844 } 1845 if (!mRightEdge.isFinished()) { 1846 final int restoreCount = canvas.save(); 1847 final int width = getWidth(); 1848 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 1849 1850 canvas.rotate(90); 1851 canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); 1852 mRightEdge.setSize(height, width); 1853 needsInvalidate |= mRightEdge.draw(canvas); 1854 canvas.restoreToCount(restoreCount); 1855 } 1856 } else { 1857 mLeftEdge.finish(); 1858 mRightEdge.finish(); 1859 } 1860 1861 if (needsInvalidate) { 1862 // Keep animating 1863 invalidate(); 1864 } 1865 } 1866 1867 @Override 1868 protected void onDraw(Canvas canvas) { 1869 super.onDraw(canvas); 1870 1871 // Draw the margin drawable between pages if needed. 1872 if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { 1873 final int scrollX = getScrollX(); 1874 final int width = getWidth(); 1875 1876 final float marginOffset = (float) mPageMargin / width; 1877 int itemIndex = 0; 1878 ItemInfo ii = mItems.get(0); 1879 float offset = ii.offset; 1880 final int itemCount = mItems.size(); 1881 final int firstPos = ii.position; 1882 final int lastPos = mItems.get(itemCount - 1).position; 1883 for (int pos = firstPos; pos < lastPos; pos++) { 1884 while (pos > ii.position && itemIndex < itemCount) { 1885 ii = mItems.get(++itemIndex); 1886 } 1887 1888 float drawAt; 1889 if (pos == ii.position) { 1890 drawAt = (ii.offset + ii.widthFactor) * width; 1891 offset = ii.offset + ii.widthFactor + marginOffset; 1892 } else { 1893 float widthFactor = mAdapter.getPageWidth(pos); 1894 drawAt = (offset + widthFactor) * width; 1895 offset += widthFactor + marginOffset; 1896 } 1897 1898 if (drawAt + mPageMargin > scrollX) { 1899 mMarginDrawable.setBounds((int) (drawAt - 0.5f), mTopPageBounds, 1900 (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds); 1901 mMarginDrawable.draw(canvas); 1902 } 1903 1904 if (drawAt > scrollX + width) { 1905 break; // No more visible, no sense in continuing 1906 } 1907 } 1908 } 1909 } 1910 1911 /** 1912 * Start a fake drag of the pager. 1913 * 1914 * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager 1915 * with the touch scrolling of another view, while still letting the ViewPager 1916 * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) 1917 * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call 1918 * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. 1919 * 1920 * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag 1921 * is already in progress, this method will return false. 1922 * 1923 * @return true if the fake drag began successfully, false if it could not be started. 1924 * 1925 * @see #fakeDragBy(float) 1926 * @see #endFakeDrag() 1927 */ 1928 public boolean beginFakeDrag() { 1929 if (mIsBeingDragged) { 1930 return false; 1931 } 1932 mFakeDragging = true; 1933 setScrollState(SCROLL_STATE_DRAGGING); 1934 mInitialMotionX = mLastMotionX = 0; 1935 if (mVelocityTracker == null) { 1936 mVelocityTracker = VelocityTracker.obtain(); 1937 } else { 1938 mVelocityTracker.clear(); 1939 } 1940 final long time = SystemClock.uptimeMillis(); 1941 final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); 1942 mVelocityTracker.addMovement(ev); 1943 ev.recycle(); 1944 mFakeDragBeginTime = time; 1945 return true; 1946 } 1947 1948 /** 1949 * End a fake drag of the pager. 1950 * 1951 * @see #beginFakeDrag() 1952 * @see #fakeDragBy(float) 1953 */ 1954 public void endFakeDrag() { 1955 if (!mFakeDragging) { 1956 throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 1957 } 1958 1959 final VelocityTracker velocityTracker = mVelocityTracker; 1960 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1961 int initialVelocity = (int)VelocityTrackerCompat.getYVelocity( 1962 velocityTracker, mActivePointerId); 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