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