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