ViewPager.java revision 66c0e6a0a5fd20c66b6c86133b8bd91fe303e430
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.os.Parcel; 20import android.os.Parcelable; 21import android.os.SystemClock; 22import android.content.Context; 23import android.support.v4.os.ParcelableCompat; 24import android.support.v4.os.ParcelableCompatCreatorCallbacks; 25import android.util.AttributeSet; 26import android.util.Log; 27import android.view.MotionEvent; 28import android.view.VelocityTracker; 29import android.view.View; 30import android.view.ViewConfiguration; 31import android.view.ViewGroup; 32import android.widget.Scroller; 33 34import java.util.ArrayList; 35import java.util.Collections; 36import java.util.Comparator; 37 38/** 39 * Layout manager that allows the user to flip left and right 40 * through pages of data. You supply an implementation of a 41 * {@link PagerAdapter} to generate the pages that the view shows. 42 * 43 * <p>Note this class is currently under early design and 44 * development. The API will likely change in later updates of 45 * the compatibility library, requiring changes to the source code 46 * of apps when they are compiled against the newer version.</p> 47 */ 48public class ViewPager extends ViewGroup { 49 private static final String TAG = "ViewPager"; 50 private static final boolean DEBUG = false; 51 52 private static final boolean USE_CACHE = false; 53 54 static class ItemInfo { 55 Object object; 56 int position; 57 boolean scrolling; 58 } 59 60 private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){ 61 @Override 62 public int compare(ItemInfo lhs, ItemInfo rhs) { 63 return lhs.position - rhs.position; 64 }}; 65 66 private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>(); 67 68 private PagerAdapter mAdapter; 69 private int mCurItem; // Index of currently displayed page. 70 private int mRestoredCurItem = -1; 71 private Parcelable mRestoredAdapterState = null; 72 private ClassLoader mRestoredClassLoader = null; 73 private Scroller mScroller; 74 private PagerAdapter.DataSetObserver mObserver; 75 76 private int mChildWidthMeasureSpec; 77 private int mChildHeightMeasureSpec; 78 private boolean mInLayout; 79 80 private boolean mScrollingCacheEnabled; 81 82 private boolean mPopulatePending; 83 private boolean mScrolling; 84 85 private boolean mIsBeingDragged; 86 private boolean mIsUnableToDrag; 87 private int mTouchSlop; 88 private float mInitialMotionX; 89 /** 90 * Position of the last motion event. 91 */ 92 private float mLastMotionX; 93 private float mLastMotionY; 94 /** 95 * ID of the active pointer. This is used to retain consistency during 96 * drags/flings if multiple pointers are used. 97 */ 98 private int mActivePointerId = INVALID_POINTER; 99 /** 100 * Sentinel value for no current active pointer. 101 * Used by {@link #mActivePointerId}. 102 */ 103 private static final int INVALID_POINTER = -1; 104 105 /** 106 * Determines speed during touch scrolling 107 */ 108 private VelocityTracker mVelocityTracker; 109 private int mMinimumVelocity; 110 private int mMaximumVelocity; 111 112 private boolean mFakeDragging; 113 private long mFakeDragBeginTime; 114 115 private boolean mFirstLayout = true; 116 117 private OnPageChangeListener mOnPageChangeListener; 118 119 /** 120 * Indicates that the pager is in an idle, settled state. The current page 121 * is fully in view and no animation is in progress. 122 */ 123 public static final int SCROLL_STATE_IDLE = 0; 124 125 /** 126 * Indicates that the pager is currently being dragged by the user. 127 */ 128 public static final int SCROLL_STATE_DRAGGING = 1; 129 130 /** 131 * Indicates that the pager is in the process of settling to a final position. 132 */ 133 public static final int SCROLL_STATE_SETTLING = 2; 134 135 private int mScrollState = SCROLL_STATE_IDLE; 136 137 /** 138 * Callback interface for responding to changing state of the selected page. 139 */ 140 public interface OnPageChangeListener { 141 142 /** 143 * This method will be invoked when the current page is scrolled, either as part 144 * of a programmatically initiated smooth scroll or a user initiated touch scroll. 145 * 146 * @param position Position index of the first page currently being displayed. 147 * Page position+1 will be visible if positionOffset is nonzero. 148 * @param positionOffset Value from [0, 1) indicating the offset from the page at position. 149 * @param positionOffsetPixels Value in pixels indicating the offset from position. 150 */ 151 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); 152 153 /** 154 * This method will be invoked when a new page becomes selected. Animation is not 155 * necessarily complete. 156 * 157 * @param position Position index of the new selected page. 158 */ 159 public void onPageSelected(int position); 160 161 /** 162 * Called when the scroll state changes. Useful for discovering when the user 163 * begins dragging, when the pager is automatically settling to the current page, 164 * or when it is fully stopped/idle. 165 * 166 * @param state The new scroll state. 167 * @see ViewPager#SCROLL_STATE_IDLE 168 * @see ViewPager#SCROLL_STATE_DRAGGING 169 * @see ViewPager#SCROLL_STATE_SETTLING 170 */ 171 public void onPageScrollStateChanged(int state); 172 } 173 174 /** 175 * Simple implementation of the {@link OnPageChangeListener} interface with stub 176 * implementations of each method. Extend this if you do not intend to override 177 * every method of {@link OnPageChangeListener}. 178 */ 179 public static class SimpleOnPageChangeListener implements OnPageChangeListener { 180 @Override 181 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 182 // This space for rent 183 } 184 185 @Override 186 public void onPageSelected(int position) { 187 // This space for rent 188 } 189 190 @Override 191 public void onPageScrollStateChanged(int state) { 192 // This space for rent 193 } 194 } 195 196 public ViewPager(Context context) { 197 super(context); 198 initViewPager(); 199 } 200 201 public ViewPager(Context context, AttributeSet attrs) { 202 super(context, attrs); 203 initViewPager(); 204 } 205 206 void initViewPager() { 207 setWillNotDraw(false); 208 mScroller = new Scroller(getContext()); 209 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 210 mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); 211 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 212 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 213 } 214 215 private void setScrollState(int newState) { 216 if (mScrollState == newState) { 217 return; 218 } 219 220 mScrollState = newState; 221 if (mOnPageChangeListener != null) { 222 mOnPageChangeListener.onPageScrollStateChanged(newState); 223 } 224 } 225 226 public void setAdapter(PagerAdapter adapter) { 227 if (mAdapter != null) { 228 mAdapter.setDataSetObserver(null); 229 mAdapter.startUpdate(this); 230 for (int i = 0; i < mItems.size(); i++) { 231 final ItemInfo ii = mItems.get(i); 232 mAdapter.destroyItem(this, ii.position, ii.object); 233 } 234 mAdapter.finishUpdate(this); 235 mItems.clear(); 236 removeAllViews(); 237 mCurItem = 0; 238 scrollTo(0, 0); 239 } 240 241 mAdapter = adapter; 242 243 if (mAdapter != null) { 244 if (mObserver == null) { 245 mObserver = new DataSetObserver(); 246 } 247 mAdapter.setDataSetObserver(mObserver); 248 mPopulatePending = false; 249 if (mRestoredCurItem >= 0) { 250 mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); 251 setCurrentItemInternal(mRestoredCurItem, false, true); 252 mRestoredCurItem = -1; 253 mRestoredAdapterState = null; 254 mRestoredClassLoader = null; 255 } else { 256 populate(); 257 } 258 } 259 } 260 261 public PagerAdapter getAdapter() { 262 return mAdapter; 263 } 264 265 /** 266 * Set the currently selected page. If the ViewPager has already been through its first 267 * layout there will be a smooth animated transition between the current item and the 268 * specified item. 269 * 270 * @param item Item index to select 271 */ 272 public void setCurrentItem(int item) { 273 mPopulatePending = false; 274 setCurrentItemInternal(item, !mFirstLayout, false); 275 } 276 277 /** 278 * Set the currently selected page. 279 * 280 * @param item Item index to select 281 * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately 282 */ 283 public void setCurrentItem(int item, boolean smoothScroll) { 284 mPopulatePending = false; 285 setCurrentItemInternal(item, smoothScroll, false); 286 } 287 288 public int getCurrentItem() { 289 return mCurItem; 290 } 291 292 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { 293 if (mAdapter == null || mAdapter.getCount() <= 0) { 294 setScrollingCacheEnabled(false); 295 return; 296 } 297 if (!always && mCurItem == item && mItems.size() != 0) { 298 setScrollingCacheEnabled(false); 299 return; 300 } 301 if (item < 0) { 302 item = 0; 303 } else if (item >= mAdapter.getCount()) { 304 item = mAdapter.getCount() - 1; 305 } 306 if (item > (mCurItem+1) || item < (mCurItem-1)) { 307 // We are doing a jump by more than one page. To avoid 308 // glitches, we want to keep all current pages in the view 309 // until the scroll ends. 310 for (int i=0; i<mItems.size(); i++) { 311 mItems.get(i).scrolling = true; 312 } 313 } 314 final boolean dispatchSelected = mCurItem != item; 315 mCurItem = item; 316 populate(); 317 if (smoothScroll) { 318 smoothScrollTo(getWidth()*item, 0); 319 if (dispatchSelected && mOnPageChangeListener != null) { 320 mOnPageChangeListener.onPageSelected(item); 321 } 322 } else { 323 if (dispatchSelected && mOnPageChangeListener != null) { 324 mOnPageChangeListener.onPageSelected(item); 325 } 326 completeScroll(); 327 scrollTo(getWidth()*item, 0); 328 } 329 } 330 331 public void setOnPageChangeListener(OnPageChangeListener listener) { 332 mOnPageChangeListener = listener; 333 } 334 335 /** 336 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 337 * 338 * @param x the number of pixels to scroll by on the X axis 339 * @param y the number of pixels to scroll by on the Y axis 340 */ 341 void smoothScrollTo(int x, int y) { 342 if (getChildCount() == 0) { 343 // Nothing to do. 344 setScrollingCacheEnabled(false); 345 return; 346 } 347 int sx = getScrollX(); 348 int sy = getScrollY(); 349 int dx = x - sx; 350 int dy = y - sy; 351 if (dx == 0 && dy == 0) { 352 completeScroll(); 353 return; 354 } 355 356 setScrollingCacheEnabled(true); 357 mScrolling = true; 358 setScrollState(SCROLL_STATE_SETTLING); 359 mScroller.startScroll(sx, sy, dx, dy); 360 invalidate(); 361 } 362 363 void addNewItem(int position, int index) { 364 ItemInfo ii = new ItemInfo(); 365 ii.position = position; 366 ii.object = mAdapter.instantiateItem(this, position); 367 if (index < 0) { 368 mItems.add(ii); 369 } else { 370 mItems.add(index, ii); 371 } 372 } 373 374 void dataSetChanged() { 375 // This method only gets called if our observer is attached, so mAdapter is non-null. 376 377 boolean needPopulate = mItems.isEmpty() && mAdapter.getCount() > 0; 378 int newCurrItem = -1; 379 380 for (int i = 0; i < mItems.size(); i++) { 381 final ItemInfo ii = mItems.get(i); 382 final int newPos = mAdapter.getItemPosition(ii.object); 383 384 if (newPos == PagerAdapter.POSITION_UNCHANGED) { 385 continue; 386 } 387 388 if (newPos == PagerAdapter.POSITION_NONE) { 389 mItems.remove(i); 390 i--; 391 mAdapter.destroyItem(this, ii.position, ii.object); 392 needPopulate = true; 393 394 if (mCurItem == ii.position) { 395 // Keep the current item in the valid range 396 newCurrItem = Math.max(0, Math.min(mCurItem, mAdapter.getCount() - 1)); 397 } 398 continue; 399 } 400 401 if (ii.position != newPos) { 402 if (ii.position == mCurItem) { 403 // Our current item changed position. Follow it. 404 newCurrItem = newPos; 405 } 406 407 ii.position = newPos; 408 needPopulate = true; 409 } 410 } 411 412 Collections.sort(mItems, COMPARATOR); 413 414 if (newCurrItem >= 0) { 415 // TODO This currently causes a jump. 416 setCurrentItemInternal(newCurrItem, false, true); 417 needPopulate = true; 418 } 419 if (needPopulate) { 420 populate(); 421 requestLayout(); 422 } 423 } 424 425 void populate() { 426 if (mAdapter == null) { 427 return; 428 } 429 430 // Bail now if we are waiting to populate. This is to hold off 431 // on creating views from the time the user releases their finger to 432 // fling to a new position until we have finished the scroll to 433 // that position, avoiding glitches from happening at that point. 434 if (mPopulatePending) { 435 if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 436 return; 437 } 438 439 // Also, don't populate until we are attached to a window. This is to 440 // avoid trying to populate before we have restored our view hierarchy 441 // state and conflicting with what is restored. 442 if (getWindowToken() == null) { 443 return; 444 } 445 446 mAdapter.startUpdate(this); 447 448 final int startPos = mCurItem > 0 ? mCurItem - 1 : mCurItem; 449 final int N = mAdapter.getCount(); 450 final int endPos = mCurItem < (N-1) ? mCurItem+1 : N-1; 451 452 if (DEBUG) Log.v(TAG, "populating: startPos=" + startPos + " endPos=" + endPos); 453 454 // Add and remove pages in the existing list. 455 int lastPos = -1; 456 for (int i=0; i<mItems.size(); i++) { 457 ItemInfo ii = mItems.get(i); 458 if ((ii.position < startPos || ii.position > endPos) && !ii.scrolling) { 459 if (DEBUG) Log.i(TAG, "removing: " + ii.position + " @ " + i); 460 mItems.remove(i); 461 i--; 462 mAdapter.destroyItem(this, ii.position, ii.object); 463 } else if (lastPos < endPos && ii.position > startPos) { 464 // The next item is outside of our range, but we have a gap 465 // between it and the last item where we want to have a page 466 // shown. Fill in the gap. 467 lastPos++; 468 if (lastPos < startPos) { 469 lastPos = startPos; 470 } 471 while (lastPos <= endPos && lastPos < ii.position) { 472 if (DEBUG) Log.i(TAG, "inserting: " + lastPos + " @ " + i); 473 addNewItem(lastPos, i); 474 lastPos++; 475 i++; 476 } 477 } 478 lastPos = ii.position; 479 } 480 481 // Add any new pages we need at the end. 482 lastPos = mItems.size() > 0 ? mItems.get(mItems.size()-1).position : -1; 483 if (lastPos < endPos) { 484 lastPos++; 485 lastPos = lastPos > startPos ? lastPos : startPos; 486 while (lastPos <= endPos) { 487 if (DEBUG) Log.i(TAG, "appending: " + lastPos); 488 addNewItem(lastPos, -1); 489 lastPos++; 490 } 491 } 492 493 if (DEBUG) { 494 Log.i(TAG, "Current page list:"); 495 for (int i=0; i<mItems.size(); i++) { 496 Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); 497 } 498 } 499 500 mAdapter.finishUpdate(this); 501 } 502 503 public static class SavedState extends BaseSavedState { 504 int position; 505 Parcelable adapterState; 506 ClassLoader loader; 507 508 public SavedState(Parcelable superState) { 509 super(superState); 510 } 511 512 @Override 513 public void writeToParcel(Parcel out, int flags) { 514 super.writeToParcel(out, flags); 515 out.writeInt(position); 516 out.writeParcelable(adapterState, flags); 517 } 518 519 @Override 520 public String toString() { 521 return "FragmentPager.SavedState{" 522 + Integer.toHexString(System.identityHashCode(this)) 523 + " position=" + position + "}"; 524 } 525 526 public static final Parcelable.Creator<SavedState> CREATOR 527 = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() { 528 @Override 529 public SavedState createFromParcel(Parcel in, ClassLoader loader) { 530 return new SavedState(in, loader); 531 } 532 @Override 533 public SavedState[] newArray(int size) { 534 return new SavedState[size]; 535 } 536 }); 537 538 SavedState(Parcel in, ClassLoader loader) { 539 super(in); 540 if (loader == null) { 541 loader = getClass().getClassLoader(); 542 } 543 position = in.readInt(); 544 adapterState = in.readParcelable(loader); 545 this.loader = loader; 546 } 547 } 548 549 @Override 550 public Parcelable onSaveInstanceState() { 551 Parcelable superState = super.onSaveInstanceState(); 552 SavedState ss = new SavedState(superState); 553 ss.position = mCurItem; 554 if (mAdapter != null) { 555 ss.adapterState = mAdapter.saveState(); 556 } 557 return ss; 558 } 559 560 @Override 561 public void onRestoreInstanceState(Parcelable state) { 562 if (!(state instanceof SavedState)) { 563 super.onRestoreInstanceState(state); 564 return; 565 } 566 567 SavedState ss = (SavedState)state; 568 super.onRestoreInstanceState(ss.getSuperState()); 569 570 if (mAdapter != null) { 571 mAdapter.restoreState(ss.adapterState, ss.loader); 572 setCurrentItemInternal(ss.position, false, true); 573 } else { 574 mRestoredCurItem = ss.position; 575 mRestoredAdapterState = ss.adapterState; 576 mRestoredClassLoader = ss.loader; 577 } 578 } 579 580 @Override 581 public void addView(View child, int index, LayoutParams params) { 582 if (mInLayout) { 583 addViewInLayout(child, index, params); 584 child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec); 585 } else { 586 super.addView(child, index, params); 587 } 588 589 if (USE_CACHE) { 590 if (child.getVisibility() != GONE) { 591 child.setDrawingCacheEnabled(mScrollingCacheEnabled); 592 } else { 593 child.setDrawingCacheEnabled(false); 594 } 595 } 596 } 597 598 ItemInfo infoForChild(View child) { 599 for (int i=0; i<mItems.size(); i++) { 600 ItemInfo ii = mItems.get(i); 601 if (mAdapter.isViewFromObject(child, ii.object)) { 602 return ii; 603 } 604 } 605 return null; 606 } 607 608 @Override 609 protected void onAttachedToWindow() { 610 super.onAttachedToWindow(); 611 mFirstLayout = true; 612 if (mAdapter != null) { 613 populate(); 614 } 615 } 616 617 @Override 618 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 619 // For simple implementation, or internal size is always 0. 620 // We depend on the container to specify the layout size of 621 // our view. We can't really know what it is since we will be 622 // adding and removing different arbitrary views and do not 623 // want the layout to change as this happens. 624 setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), 625 getDefaultSize(0, heightMeasureSpec)); 626 627 // Children are just made to fill our space. 628 mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - 629 getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY); 630 mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - 631 getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY); 632 633 // Make sure we have created all fragments that we need to have shown. 634 mInLayout = true; 635 populate(); 636 mInLayout = false; 637 638 // Make sure all children have been properly measured. 639 final int size = getChildCount(); 640 for (int i = 0; i < size; ++i) { 641 final View child = getChildAt(i); 642 if (child.getVisibility() != GONE) { 643 if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child 644 + ": " + mChildWidthMeasureSpec); 645 child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec); 646 } 647 } 648 } 649 650 @Override 651 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 652 super.onSizeChanged(w, h, oldw, oldh); 653 654 // Make sure scroll position is set correctly. 655 if (w != oldw) { 656 if (oldw > 0) { 657 final int oldScrollPos = getScrollX(); 658 final int oldScrollItem = oldScrollPos / oldw; 659 final float scrollOffset = (float) (oldScrollPos % oldw) / oldw; 660 final int scrollPos = (int) ((oldScrollItem + scrollOffset) * w); 661 scrollTo(scrollPos, getScrollY()); 662 if (!mScroller.isFinished()) { 663 // We now return to your regularly scheduled scroll, already in progress. 664 final int newDuration = mScroller.getDuration() - mScroller.timePassed(); 665 mScroller.startScroll(scrollPos, 0, mCurItem * w, 0, newDuration); 666 } 667 } else { 668 int scrollPos = mCurItem * w; 669 if (scrollPos != getScrollX()) { 670 completeScroll(); 671 scrollTo(scrollPos, getScrollY()); 672 } 673 } 674 } 675 } 676 677 @Override 678 protected void onLayout(boolean changed, int l, int t, int r, int b) { 679 mInLayout = true; 680 populate(); 681 mInLayout = false; 682 683 final int count = getChildCount(); 684 final int width = r-l; 685 686 for (int i = 0; i < count; i++) { 687 View child = getChildAt(i); 688 ItemInfo ii; 689 if (child.getVisibility() != GONE && (ii=infoForChild(child)) != null) { 690 int loff = width*ii.position; 691 int childLeft = getPaddingLeft() + loff; 692 int childTop = getPaddingTop(); 693 if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object 694 + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() 695 + "x" + child.getMeasuredHeight()); 696 child.layout(childLeft, childTop, 697 childLeft + child.getMeasuredWidth(), 698 childTop + child.getMeasuredHeight()); 699 } 700 } 701 mFirstLayout = false; 702 } 703 704 @Override 705 public void computeScroll() { 706 if (DEBUG) Log.i(TAG, "computeScroll: finished=" + mScroller.isFinished()); 707 if (!mScroller.isFinished()) { 708 if (mScroller.computeScrollOffset()) { 709 if (DEBUG) Log.i(TAG, "computeScroll: still scrolling"); 710 int oldX = getScrollX(); 711 int oldY = getScrollY(); 712 int x = mScroller.getCurrX(); 713 int y = mScroller.getCurrY(); 714 715 if (oldX != x || oldY != y) { 716 scrollTo(x, y); 717 } 718 719 if (mOnPageChangeListener != null) { 720 final int width = getWidth(); 721 final int position = x / width; 722 final int offsetPixels = x % width; 723 final float offset = (float) offsetPixels / width; 724 mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); 725 } 726 727 // Keep on drawing until the animation has finished. 728 invalidate(); 729 return; 730 } 731 } 732 733 // Done with scroll, clean up state. 734 completeScroll(); 735 } 736 737 private void completeScroll() { 738 boolean needPopulate = mScrolling; 739 if (needPopulate) { 740 // Done with scroll, no longer want to cache view drawing. 741 setScrollingCacheEnabled(false); 742 mScroller.abortAnimation(); 743 int oldX = getScrollX(); 744 int oldY = getScrollY(); 745 int x = mScroller.getCurrX(); 746 int y = mScroller.getCurrY(); 747 if (oldX != x || oldY != y) { 748 scrollTo(x, y); 749 } 750 setScrollState(SCROLL_STATE_IDLE); 751 } 752 mPopulatePending = false; 753 mScrolling = false; 754 for (int i=0; i<mItems.size(); i++) { 755 ItemInfo ii = mItems.get(i); 756 if (ii.scrolling) { 757 needPopulate = true; 758 ii.scrolling = false; 759 } 760 } 761 if (needPopulate) { 762 populate(); 763 } 764 } 765 766 @Override 767 public boolean onInterceptTouchEvent(MotionEvent ev) { 768 /* 769 * This method JUST determines whether we want to intercept the motion. 770 * If we return true, onMotionEvent will be called and we do the actual 771 * scrolling there. 772 */ 773 774 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 775 776 // Always take care of the touch gesture being complete. 777 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 778 // Release the drag. 779 if (DEBUG) Log.v(TAG, "Intercept done!"); 780 mIsBeingDragged = false; 781 mIsUnableToDrag = false; 782 mActivePointerId = INVALID_POINTER; 783 return false; 784 } 785 786 // Nothing more to do here if we have decided whether or not we 787 // are dragging. 788 if (action != MotionEvent.ACTION_DOWN) { 789 if (mIsBeingDragged) { 790 if (DEBUG) Log.v(TAG, "Intercept returning true!"); 791 return true; 792 } 793 if (mIsUnableToDrag) { 794 if (DEBUG) Log.v(TAG, "Intercept returning false!"); 795 return false; 796 } 797 } 798 799 switch (action) { 800 case MotionEvent.ACTION_MOVE: { 801 /* 802 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 803 * whether the user has moved far enough from his original down touch. 804 */ 805 806 /* 807 * Locally do absolute value. mLastMotionY is set to the y value 808 * of the down event. 809 */ 810 final int activePointerId = mActivePointerId; 811 if (activePointerId == INVALID_POINTER) { 812 // If we don't have a valid id, the touch down wasn't on content. 813 break; 814 } 815 816 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 817 final float x = MotionEventCompat.getX(ev, pointerIndex); 818 final float dx = x - mLastMotionX; 819 final float xDiff = Math.abs(dx); 820 final float y = MotionEventCompat.getY(ev, pointerIndex); 821 final float yDiff = Math.abs(y - mLastMotionY); 822 final int scrollX = getScrollX(); 823 final boolean atEdge = (dx > 0 && scrollX == 0) || (dx < 0 && mAdapter != null && 824 scrollX >= (mAdapter.getCount() - 1) * getWidth() - 1); 825 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 826 827 if (atEdge || canScroll(this, false, (int) dx, (int) x, (int) y)) { 828 // Nested view has scrollable area under this point. Let it be handled there. 829 mInitialMotionX = mLastMotionX = x; 830 mLastMotionY = y; 831 return false; 832 } 833 if (xDiff > mTouchSlop && xDiff > yDiff) { 834 if (DEBUG) Log.v(TAG, "Starting drag!"); 835 mIsBeingDragged = true; 836 setScrollState(SCROLL_STATE_DRAGGING); 837 mLastMotionX = x; 838 setScrollingCacheEnabled(true); 839 } else { 840 if (yDiff > mTouchSlop) { 841 // The finger has moved enough in the vertical 842 // direction to be counted as a drag... abort 843 // any attempt to drag horizontally, to work correctly 844 // with children that have scrolling containers. 845 if (DEBUG) Log.v(TAG, "Starting unable to drag!"); 846 mIsUnableToDrag = true; 847 } 848 } 849 break; 850 } 851 852 case MotionEvent.ACTION_DOWN: { 853 /* 854 * Remember location of down touch. 855 * ACTION_DOWN always refers to pointer index 0. 856 */ 857 mLastMotionX = mInitialMotionX = ev.getX(); 858 mLastMotionY = ev.getY(); 859 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 860 861 if (mScrollState == SCROLL_STATE_SETTLING) { 862 // Let the user 'catch' the pager as it animates. 863 mIsBeingDragged = true; 864 mIsUnableToDrag = false; 865 setScrollState(SCROLL_STATE_DRAGGING); 866 } else { 867 completeScroll(); 868 mIsBeingDragged = false; 869 mIsUnableToDrag = false; 870 } 871 872 if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY 873 + " mIsBeingDragged=" + mIsBeingDragged 874 + "mIsUnableToDrag=" + mIsUnableToDrag); 875 break; 876 } 877 878 case MotionEventCompat.ACTION_POINTER_UP: 879 onSecondaryPointerUp(ev); 880 break; 881 } 882 883 /* 884 * The only time we want to intercept motion events is if we are in the 885 * drag mode. 886 */ 887 return mIsBeingDragged; 888 } 889 890 @Override 891 public boolean onTouchEvent(MotionEvent ev) { 892 if (mFakeDragging) { 893 // A fake drag is in progress already, ignore this real one 894 // but still eat the touch events. 895 // (It is likely that the user is multi-touching the screen.) 896 return true; 897 } 898 899 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 900 // Don't handle edge touches immediately -- they may actually belong to one of our 901 // descendants. 902 return false; 903 } 904 905 if (mAdapter == null || mAdapter.getCount() == 0) { 906 // Nothing to present or scroll; nothing to touch. 907 return false; 908 } 909 910 if (mVelocityTracker == null) { 911 mVelocityTracker = VelocityTracker.obtain(); 912 } 913 mVelocityTracker.addMovement(ev); 914 915 final int action = ev.getAction(); 916 917 switch (action & MotionEventCompat.ACTION_MASK) { 918 case MotionEvent.ACTION_DOWN: { 919 /* 920 * If being flinged and user touches, stop the fling. isFinished 921 * will be false if being flinged. 922 */ 923 completeScroll(); 924 925 // Remember where the motion event started 926 mLastMotionX = mInitialMotionX = ev.getX(); 927 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 928 break; 929 } 930 case MotionEvent.ACTION_MOVE: 931 if (!mIsBeingDragged) { 932 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 933 final float x = MotionEventCompat.getX(ev, pointerIndex); 934 final float xDiff = Math.abs(x - mLastMotionX); 935 final float y = MotionEventCompat.getY(ev, pointerIndex); 936 final float yDiff = Math.abs(y - mLastMotionY); 937 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 938 if (xDiff > mTouchSlop && xDiff > yDiff) { 939 if (DEBUG) Log.v(TAG, "Starting drag!"); 940 mIsBeingDragged = true; 941 mLastMotionX = x; 942 setScrollState(SCROLL_STATE_DRAGGING); 943 setScrollingCacheEnabled(true); 944 } 945 } 946 if (mIsBeingDragged) { 947 // Scroll to follow the motion event 948 final int activePointerIndex = MotionEventCompat.findPointerIndex( 949 ev, mActivePointerId); 950 final float x = MotionEventCompat.getX(ev, activePointerIndex); 951 final float deltaX = mLastMotionX - x; 952 mLastMotionX = x; 953 float scrollX = getScrollX() + deltaX; 954 final int width = getWidth(); 955 956 final float leftBound = Math.max(0, (mCurItem - 1) * width); 957 final float rightBound = 958 Math.min(mCurItem + 1, mAdapter.getCount() - 1) * width; 959 if (scrollX < leftBound) { 960 scrollX = leftBound; 961 } else if (scrollX > rightBound) { 962 scrollX = rightBound; 963 } 964 // Don't lose the rounded component 965 mLastMotionX += scrollX - (int) scrollX; 966 scrollTo((int) scrollX, getScrollY()); 967 if (mOnPageChangeListener != null) { 968 final int position = (int) scrollX / width; 969 final int positionOffsetPixels = (int) scrollX % width; 970 final float positionOffset = (float) positionOffsetPixels / width; 971 mOnPageChangeListener.onPageScrolled(position, positionOffset, 972 positionOffsetPixels); 973 } 974 } 975 break; 976 case MotionEvent.ACTION_UP: 977 if (mIsBeingDragged) { 978 final VelocityTracker velocityTracker = mVelocityTracker; 979 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 980 int initialVelocity = (int)VelocityTrackerCompat.getYVelocity( 981 velocityTracker, mActivePointerId); 982 mPopulatePending = true; 983 if ((Math.abs(initialVelocity) > mMinimumVelocity) 984 || Math.abs(mInitialMotionX-mLastMotionX) >= (getWidth()/3)) { 985 if (mLastMotionX > mInitialMotionX) { 986 setCurrentItemInternal(mCurItem-1, true, true); 987 } else { 988 setCurrentItemInternal(mCurItem+1, true, true); 989 } 990 } else { 991 setCurrentItemInternal(mCurItem, true, true); 992 } 993 994 mActivePointerId = INVALID_POINTER; 995 endDrag(); 996 } 997 break; 998 case MotionEvent.ACTION_CANCEL: 999 if (mIsBeingDragged) { 1000 setCurrentItemInternal(mCurItem, true, true); 1001 mActivePointerId = INVALID_POINTER; 1002 endDrag(); 1003 } 1004 break; 1005 case MotionEventCompat.ACTION_POINTER_DOWN: { 1006 final int index = MotionEventCompat.getActionIndex(ev); 1007 final float x = MotionEventCompat.getX(ev, index); 1008 mLastMotionX = x; 1009 mActivePointerId = MotionEventCompat.getPointerId(ev, index); 1010 break; 1011 } 1012 case MotionEventCompat.ACTION_POINTER_UP: 1013 onSecondaryPointerUp(ev); 1014 mLastMotionX = MotionEventCompat.getX(ev, 1015 MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 1016 break; 1017 } 1018 return true; 1019 } 1020 1021 /** 1022 * Start a fake drag of the pager. 1023 * 1024 * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager 1025 * with the touch scrolling of another view, while still letting the ViewPager 1026 * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) 1027 * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call 1028 * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. 1029 * 1030 * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag 1031 * is already in progress, this method will return false. 1032 * 1033 * @return true if the fake drag began successfully, false if it could not be started. 1034 * 1035 * @see #fakeDragBy(float) 1036 * @see #endFakeDrag() 1037 */ 1038 public boolean beginFakeDrag() { 1039 if (mIsBeingDragged) { 1040 return false; 1041 } 1042 mFakeDragging = true; 1043 setScrollState(SCROLL_STATE_DRAGGING); 1044 mInitialMotionX = mLastMotionX = 0; 1045 if (mVelocityTracker == null) { 1046 mVelocityTracker = VelocityTracker.obtain(); 1047 } else { 1048 mVelocityTracker.clear(); 1049 } 1050 final long time = SystemClock.uptimeMillis(); 1051 final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); 1052 mVelocityTracker.addMovement(ev); 1053 ev.recycle(); 1054 mFakeDragBeginTime = time; 1055 return true; 1056 } 1057 1058 /** 1059 * End a fake drag of the pager. 1060 * 1061 * @see #beginFakeDrag() 1062 * @see #fakeDragBy(float) 1063 */ 1064 public void endFakeDrag() { 1065 if (!mFakeDragging) { 1066 throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 1067 } 1068 1069 final VelocityTracker velocityTracker = mVelocityTracker; 1070 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1071 int initialVelocity = (int)VelocityTrackerCompat.getYVelocity( 1072 velocityTracker, mActivePointerId); 1073 mPopulatePending = true; 1074 if ((Math.abs(initialVelocity) > mMinimumVelocity) 1075 || Math.abs(mInitialMotionX-mLastMotionX) >= (getWidth()/3)) { 1076 if (mLastMotionX > mInitialMotionX) { 1077 setCurrentItemInternal(mCurItem-1, true, true); 1078 } else { 1079 setCurrentItemInternal(mCurItem+1, true, true); 1080 } 1081 } else { 1082 setCurrentItemInternal(mCurItem, true, true); 1083 } 1084 endDrag(); 1085 1086 mFakeDragging = false; 1087 } 1088 1089 /** 1090 * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. 1091 * 1092 * @param xOffset Offset in pixels to drag by. 1093 * @see #beginFakeDrag() 1094 * @see #endFakeDrag() 1095 */ 1096 public void fakeDragBy(float xOffset) { 1097 if (!mFakeDragging) { 1098 throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 1099 } 1100 1101 mLastMotionX += xOffset; 1102 float scrollX = getScrollX() - xOffset; 1103 final int width = getWidth(); 1104 1105 final float leftBound = Math.max(0, (mCurItem - 1) * width); 1106 final float rightBound = 1107 Math.min(mCurItem + 1, mAdapter.getCount() - 1) * width; 1108 if (scrollX < leftBound) { 1109 scrollX = leftBound; 1110 } else if (scrollX > rightBound) { 1111 scrollX = rightBound; 1112 } 1113 // Don't lose the rounded component 1114 mLastMotionX += scrollX - (int) scrollX; 1115 scrollTo((int) scrollX, getScrollY()); 1116 if (mOnPageChangeListener != null) { 1117 final int position = (int) scrollX / width; 1118 final int positionOffsetPixels = (int) scrollX % width; 1119 final float positionOffset = (float) positionOffsetPixels / width; 1120 mOnPageChangeListener.onPageScrolled(position, positionOffset, 1121 positionOffsetPixels); 1122 } 1123 1124 // Synthesize an event for the VelocityTracker. 1125 final long time = SystemClock.uptimeMillis(); 1126 final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, 1127 mLastMotionX, 0, 0); 1128 mVelocityTracker.addMovement(ev); 1129 ev.recycle(); 1130 } 1131 1132 /** 1133 * Returns true if a fake drag is in progress. 1134 * 1135 * @return true if currently in a fake drag, false otherwise. 1136 * 1137 * @see #beginFakeDrag() 1138 * @see #fakeDragBy(float) 1139 * @see #endFakeDrag() 1140 */ 1141 public boolean isFakeDragging() { 1142 return mFakeDragging; 1143 } 1144 1145 private void onSecondaryPointerUp(MotionEvent ev) { 1146 final int pointerIndex = MotionEventCompat.getActionIndex(ev); 1147 final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 1148 if (pointerId == mActivePointerId) { 1149 // This was our active pointer going up. Choose a new 1150 // active pointer and adjust accordingly. 1151 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1152 mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); 1153 mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 1154 if (mVelocityTracker != null) { 1155 mVelocityTracker.clear(); 1156 } 1157 } 1158 } 1159 1160 private void endDrag() { 1161 mIsBeingDragged = false; 1162 mIsUnableToDrag = false; 1163 1164 if (mVelocityTracker != null) { 1165 mVelocityTracker.recycle(); 1166 mVelocityTracker = null; 1167 } 1168 } 1169 1170 private void setScrollingCacheEnabled(boolean enabled) { 1171 if (mScrollingCacheEnabled != enabled) { 1172 mScrollingCacheEnabled = enabled; 1173 if (USE_CACHE) { 1174 final int size = getChildCount(); 1175 for (int i = 0; i < size; ++i) { 1176 final View child = getChildAt(i); 1177 if (child.getVisibility() != GONE) { 1178 child.setDrawingCacheEnabled(enabled); 1179 } 1180 } 1181 } 1182 } 1183 } 1184 1185 /** 1186 * Tests scrollability within child views of v given a delta of dx. 1187 * 1188 * @param v View to test for horizontal scrollability 1189 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 1190 * or just its children (false). 1191 * @param dx Delta scrolled in pixels 1192 * @param x X coordinate of the active touch point 1193 * @param y Y coordinate of the active touch point 1194 * @return true if child views of v can be scrolled by delta of dx. 1195 */ 1196 protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { 1197 if (v instanceof ViewGroup) { 1198 final ViewGroup group = (ViewGroup) v; 1199 final int scrollX = v.getScrollX(); 1200 final int scrollY = v.getScrollY(); 1201 final int count = group.getChildCount(); 1202 // Count backwards - let topmost views consume scroll distance first. 1203 for (int i = count - 1; i >= 0; i--) { 1204 // TODO: Add versioned support here for transformed views. 1205 // This will not work for transformed views in Honeycomb+ 1206 final View child = group.getChildAt(i); 1207 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 1208 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 1209 canScroll(child, true, dx, x + scrollX - child.getLeft(), 1210 y + scrollY - child.getTop())) { 1211 return true; 1212 } 1213 } 1214 } 1215 1216 return checkV && ViewCompat.canScrollHorizontally(v, -dx); 1217 } 1218 1219 private class DataSetObserver implements PagerAdapter.DataSetObserver { 1220 @Override 1221 public void onDataSetChanged() { 1222 dataSetChanged(); 1223 } 1224 } 1225} 1226