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