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