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