ViewPager.java revision 677ada6ea45f3889fb5118e85a3aa706f99e4d72
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 if (mAdapter != null) { 460 ss.adapterState = mAdapter.saveState(); 461 } 462 return ss; 463 } 464 465 @Override 466 public void onRestoreInstanceState(Parcelable state) { 467 if (!(state instanceof SavedState)) { 468 super.onRestoreInstanceState(state); 469 return; 470 } 471 472 SavedState ss = (SavedState)state; 473 super.onRestoreInstanceState(ss.getSuperState()); 474 475 if (mAdapter != null) { 476 mAdapter.restoreState(ss.adapterState, ss.loader); 477 setCurrentItemInternal(ss.position, false, true); 478 } else { 479 mRestoredCurItem = ss.position; 480 mRestoredAdapterState = ss.adapterState; 481 mRestoredClassLoader = ss.loader; 482 } 483 } 484 485 @Override 486 public void addView(View child, int index, LayoutParams params) { 487 if (mInLayout) { 488 addViewInLayout(child, index, params); 489 child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec); 490 } else { 491 super.addView(child, index, params); 492 } 493 494 if (USE_CACHE) { 495 if (child.getVisibility() != GONE) { 496 child.setDrawingCacheEnabled(mScrollingCacheEnabled); 497 } else { 498 child.setDrawingCacheEnabled(false); 499 } 500 } 501 } 502 503 ItemInfo infoForChild(View child) { 504 for (int i=0; i<mItems.size(); i++) { 505 ItemInfo ii = mItems.get(i); 506 if (mAdapter.isViewFromObject(child, ii.object)) { 507 return ii; 508 } 509 } 510 return null; 511 } 512 513 @Override 514 protected void onAttachedToWindow() { 515 super.onAttachedToWindow(); 516 if (mAdapter != null) { 517 populate(); 518 } 519 } 520 521 @Override 522 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 523 // For simple implementation, or internal size is always 0. 524 // We depend on the container to specify the layout size of 525 // our view. We can't really know what it is since we will be 526 // adding and removing different arbitrary views and do not 527 // want the layout to change as this happens. 528 setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), 529 getDefaultSize(0, heightMeasureSpec)); 530 531 // Children are just made to fill our space. 532 mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - 533 getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY); 534 mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - 535 getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY); 536 537 // Make sure we have created all fragments that we need to have shown. 538 mInLayout = true; 539 populate(); 540 mInLayout = false; 541 542 // Make sure all children have been properly measured. 543 final int size = getChildCount(); 544 for (int i = 0; i < size; ++i) { 545 final View child = getChildAt(i); 546 if (child.getVisibility() != GONE) { 547 if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child 548 + ": " + mChildWidthMeasureSpec); 549 child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec); 550 } 551 } 552 } 553 554 @Override 555 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 556 super.onSizeChanged(w, h, oldw, oldh); 557 558 // Make sure scroll position is set correctly. 559 int scrollPos = mCurItem*w; 560 if (scrollPos != getScrollX()) { 561 completeScroll(true); 562 scrollTo(scrollPos, getScrollY()); 563 } 564 } 565 566 @Override 567 protected void onLayout(boolean changed, int l, int t, int r, int b) { 568 mInLayout = true; 569 populate(); 570 mInLayout = false; 571 572 final int count = getChildCount(); 573 final int width = r-l; 574 575 for (int i = 0; i < count; i++) { 576 View child = getChildAt(i); 577 ItemInfo ii; 578 if (child.getVisibility() != GONE && (ii=infoForChild(child)) != null) { 579 int loff = width*ii.position; 580 int childLeft = getPaddingLeft() + loff; 581 int childTop = getPaddingTop(); 582 if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object 583 + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() 584 + "x" + child.getMeasuredHeight()); 585 child.layout(childLeft, childTop, 586 childLeft + child.getMeasuredWidth(), 587 childTop + child.getMeasuredHeight()); 588 } 589 } 590 } 591 592 @Override 593 public void computeScroll() { 594 if (DEBUG) Log.i(TAG, "computeScroll: finished=" + mScroller.isFinished()); 595 if (!mScroller.isFinished()) { 596 if (mScroller.computeScrollOffset()) { 597 if (DEBUG) Log.i(TAG, "computeScroll: still scrolling"); 598 int oldX = getScrollX(); 599 int oldY = getScrollY(); 600 int x = mScroller.getCurrX(); 601 int y = mScroller.getCurrY(); 602 603 if (oldX != x || oldY != y) { 604 scrollTo(x, y); 605 } 606 607 if (mOnPageChangeListener != null) { 608 final int width = getWidth(); 609 final int position = x / width; 610 final int offsetPixels = x % width; 611 final float offset = (float) offsetPixels / width; 612 mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); 613 } 614 615 // Keep on drawing until the animation has finished. 616 invalidate(); 617 return; 618 } 619 } 620 621 // Done with scroll, clean up state. 622 completeScroll(true); 623 } 624 625 private void completeScroll(boolean selected) { 626 boolean needPopulate; 627 if ((needPopulate=mScrolling)) { 628 // Done with scroll, no longer want to cache view drawing. 629 setScrollingCacheEnabled(false); 630 mScroller.abortAnimation(); 631 int oldX = getScrollX(); 632 int oldY = getScrollY(); 633 int x = mScroller.getCurrX(); 634 int y = mScroller.getCurrY(); 635 if (oldX != x || oldY != y) { 636 scrollTo(x, y); 637 } 638 639 if (selected && mOnPageChangeListener != null) { 640 final int position = x / getWidth(); 641 mOnPageChangeListener.onPageSelected(position); 642 } 643 } 644 mPopulatePending = false; 645 mScrolling = false; 646 for (int i=0; i<mItems.size(); i++) { 647 ItemInfo ii = mItems.get(i); 648 if (ii.scrolling) { 649 needPopulate = true; 650 ii.scrolling = false; 651 } 652 } 653 if (needPopulate) { 654 populate(); 655 } 656 } 657 658 @Override 659 public boolean onInterceptTouchEvent(MotionEvent ev) { 660 /* 661 * This method JUST determines whether we want to intercept the motion. 662 * If we return true, onMotionEvent will be called and we do the actual 663 * scrolling there. 664 */ 665 666 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 667 668 // Always take care of the touch gesture being complete. 669 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 670 // Release the drag. 671 if (DEBUG) Log.v(TAG, "Intercept done!"); 672 mIsBeingDragged = false; 673 mIsUnableToDrag = false; 674 mActivePointerId = INVALID_POINTER; 675 return false; 676 } 677 678 // Nothing more to do here if we have decided whether or not we 679 // are dragging. 680 if (action != MotionEvent.ACTION_DOWN) { 681 if (mIsBeingDragged) { 682 if (DEBUG) Log.v(TAG, "Intercept returning true!"); 683 return true; 684 } 685 if (mIsUnableToDrag) { 686 if (DEBUG) Log.v(TAG, "Intercept returning false!"); 687 return false; 688 } 689 } 690 691 switch (action) { 692 case MotionEvent.ACTION_MOVE: { 693 /* 694 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 695 * whether the user has moved far enough from his original down touch. 696 */ 697 698 /* 699 * Locally do absolute value. mLastMotionY is set to the y value 700 * of the down event. 701 */ 702 final int activePointerId = mActivePointerId; 703 if (activePointerId == INVALID_POINTER) { 704 // If we don't have a valid id, the touch down wasn't on content. 705 break; 706 } 707 708 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 709 final float x = MotionEventCompat.getX(ev, pointerIndex); 710 final int dx = (int) (x - mLastMotionX); 711 final int xDiff = Math.abs(dx); 712 final float y = MotionEventCompat.getY(ev, pointerIndex); 713 final int yDiff = (int) Math.abs(y - mLastMotionY); 714 final int scrollX = getScrollX(); 715 final boolean atEdge = (dx > 0 && scrollX == 0) || (dx < 0 && mAdapter != null && 716 scrollX >= (mAdapter.getCount() - 1) * getWidth() - 1); 717 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 718 719 if (atEdge || canScroll(this, false, dx, (int) x, (int) y)) { 720 // Nested view has scrollable area under this point. Let it be handled there. 721 mInitialMotionX = mLastMotionX = x; 722 mLastMotionY = y; 723 return false; 724 } 725 if (xDiff > mTouchSlop && xDiff > yDiff) { 726 if (DEBUG) Log.v(TAG, "Starting drag!"); 727 mIsBeingDragged = true; 728 mLastMotionX = x; 729 setScrollingCacheEnabled(true); 730 } else { 731 if (yDiff > mTouchSlop) { 732 // The finger has moved enough in the vertical 733 // direction to be counted as a drag... abort 734 // any attempt to drag horizontally, to work correctly 735 // with children that have scrolling containers. 736 if (DEBUG) Log.v(TAG, "Starting unable to drag!"); 737 mIsUnableToDrag = true; 738 } 739 } 740 break; 741 } 742 743 case MotionEvent.ACTION_DOWN: { 744 /* 745 * Remember location of down touch. 746 * ACTION_DOWN always refers to pointer index 0. 747 */ 748 mLastMotionX = mInitialMotionX = ev.getX(); 749 mLastMotionY = ev.getY(); 750 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 751 752 completeScroll(false); 753 mIsBeingDragged = false; 754 mIsUnableToDrag = false; 755 756 if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY 757 + " mIsBeingDragged=" + mIsBeingDragged 758 + "mIsUnableToDrag=" + mIsUnableToDrag); 759 break; 760 } 761 762 case MotionEventCompat.ACTION_POINTER_UP: 763 onSecondaryPointerUp(ev); 764 break; 765 } 766 767 /* 768 * The only time we want to intercept motion events is if we are in the 769 * drag mode. 770 */ 771 return mIsBeingDragged; 772 } 773 774 @Override 775 public boolean onTouchEvent(MotionEvent ev) { 776 777 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 778 // Don't handle edge touches immediately -- they may actually belong to one of our 779 // descendants. 780 return false; 781 } 782 783 if (mAdapter == null || mAdapter.getCount() == 0) { 784 // Nothing to present or scroll; nothing to touch. 785 return false; 786 } 787 788 if (mVelocityTracker == null) { 789 mVelocityTracker = VelocityTracker.obtain(); 790 } 791 mVelocityTracker.addMovement(ev); 792 793 final int action = ev.getAction(); 794 795 switch (action & MotionEventCompat.ACTION_MASK) { 796 case MotionEvent.ACTION_DOWN: { 797 /* 798 * If being flinged and user touches, stop the fling. isFinished 799 * will be false if being flinged. 800 */ 801 completeScroll(false); 802 803 // Remember where the motion event started 804 mLastMotionX = mInitialMotionX = ev.getX(); 805 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 806 break; 807 } 808 case MotionEvent.ACTION_MOVE: 809 if (!mIsBeingDragged) { 810 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 811 final float x = MotionEventCompat.getX(ev, pointerIndex); 812 final int xDiff = (int) Math.abs(x - mLastMotionX); 813 final float y = MotionEventCompat.getY(ev, pointerIndex); 814 final int yDiff = (int) Math.abs(y - mLastMotionY); 815 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 816 if (xDiff > mTouchSlop && xDiff > yDiff) { 817 if (DEBUG) Log.v(TAG, "Starting drag!"); 818 mIsBeingDragged = true; 819 mLastMotionX = x; 820 setScrollingCacheEnabled(true); 821 } 822 } 823 if (mIsBeingDragged) { 824 // Scroll to follow the motion event 825 final int activePointerIndex = MotionEventCompat.findPointerIndex( 826 ev, mActivePointerId); 827 final float x = MotionEventCompat.getX(ev, activePointerIndex); 828 final int deltaX = (int) (mLastMotionX - x); 829 mLastMotionX = x; 830 int scrollX = getScrollX() + deltaX; 831 final int width = getWidth(); 832 if (scrollX < 0) { 833 scrollX = 0; 834 } else if (scrollX > ((mAdapter.getCount() - 1) * width)) { 835 scrollX = (mAdapter.getCount() - 1) * width; 836 } 837 scrollTo(scrollX, getScrollY()); 838 if (mOnPageChangeListener != null) { 839 final int position = scrollX / width; 840 final int positionOffsetPixels = scrollX % width; 841 final float positionOffset = (float) positionOffsetPixels / width; 842 mOnPageChangeListener.onPageScrolled(position, positionOffset, 843 positionOffsetPixels); 844 } 845 } 846 break; 847 case MotionEvent.ACTION_UP: 848 if (mIsBeingDragged) { 849 final VelocityTracker velocityTracker = mVelocityTracker; 850 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 851 int initialVelocity = (int)VelocityTrackerCompat.getYVelocity( 852 velocityTracker, mActivePointerId); 853 mPopulatePending = true; 854 if ((Math.abs(initialVelocity) > mMinimumVelocity) 855 || Math.abs(mInitialMotionX-mLastMotionX) >= (getWidth()/3)) { 856 if (mLastMotionX > mInitialMotionX) { 857 setCurrentItemInternal(mCurItem-1, true, true); 858 } else { 859 setCurrentItemInternal(mCurItem+1, true, true); 860 } 861 } else { 862 setCurrentItemInternal(mCurItem, true, true); 863 } 864 865 mActivePointerId = INVALID_POINTER; 866 endDrag(); 867 } 868 break; 869 case MotionEvent.ACTION_CANCEL: 870 if (mIsBeingDragged) { 871 setCurrentItemInternal(mCurItem, true, true); 872 mActivePointerId = INVALID_POINTER; 873 endDrag(); 874 } 875 break; 876 case MotionEventCompat.ACTION_POINTER_DOWN: { 877 final int index = MotionEventCompat.getActionIndex(ev); 878 final float x = MotionEventCompat.getX(ev, index); 879 mLastMotionX = x; 880 mActivePointerId = MotionEventCompat.getPointerId(ev, index); 881 break; 882 } 883 case MotionEventCompat.ACTION_POINTER_UP: 884 onSecondaryPointerUp(ev); 885 mLastMotionX = MotionEventCompat.getX(ev, 886 MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 887 break; 888 } 889 return true; 890 } 891 892 private void onSecondaryPointerUp(MotionEvent ev) { 893 final int pointerIndex = MotionEventCompat.getActionIndex(ev); 894 final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 895 if (pointerId == mActivePointerId) { 896 // This was our active pointer going up. Choose a new 897 // active pointer and adjust accordingly. 898 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 899 mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); 900 mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 901 if (mVelocityTracker != null) { 902 mVelocityTracker.clear(); 903 } 904 } 905 } 906 907 private void endDrag() { 908 mIsBeingDragged = false; 909 mIsUnableToDrag = false; 910 911 if (mVelocityTracker != null) { 912 mVelocityTracker.recycle(); 913 mVelocityTracker = null; 914 } 915 } 916 917 private void setScrollingCacheEnabled(boolean enabled) { 918 if (mScrollingCacheEnabled != enabled) { 919 mScrollingCacheEnabled = enabled; 920 if (USE_CACHE) { 921 final int size = getChildCount(); 922 for (int i = 0; i < size; ++i) { 923 final View child = getChildAt(i); 924 if (child.getVisibility() != GONE) { 925 child.setDrawingCacheEnabled(enabled); 926 } 927 } 928 } 929 } 930 } 931 932 /** 933 * Test scrollability within child views of v given a delta of dx. 934 * 935 * @param v View to test for horizontal scrollability 936 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 937 * or just its children (false). 938 * @param dx Delta scrolled in pixels 939 * @param x X coorindate of the active touch point 940 * @param y Y coordinate of the active touch point 941 * @return Delta still left to be scrolled by a parent. 942 */ 943 static boolean canScroll(View v, boolean checkV, int dx, int x, int y) { 944 if (v instanceof ViewGroup) { 945 final ViewGroup group = (ViewGroup) v; 946 final int scrollX = v.getScrollX(); 947 final int scrollY = v.getScrollY(); 948 final int count = group.getChildCount(); 949 // Count backwards - let topmost views consume scroll distance first. 950 for (int i = count - 1; i >= 0; i--) { 951 // TODO: Add versioned support here for transformed views. 952 // This will not work for transformed views in Honeycomb+ 953 final View child = group.getChildAt(i); 954 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 955 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 956 canScroll(child, true, dx, x + scrollX - child.getLeft(), 957 y + scrollY - child.getTop())) { 958 return true; 959 } 960 } 961 } 962 963 return checkV && ViewCompat.canScrollHorizontally(v, -dx); 964 } 965 966 private class DataSetObserver implements PagerAdapter.DataSetObserver { 967 @Override 968 public void onDataSetChanged() { 969 dataSetChanged(); 970 } 971 } 972} 973