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