ViewPager.java revision b469af6dc2f8cda4020a78fb4582c1483089fd6e
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.content.Context; 20import android.content.res.TypedArray; 21import android.database.DataSetObserver; 22import android.graphics.Canvas; 23import android.graphics.Rect; 24import android.graphics.drawable.Drawable; 25import android.os.Build; 26import android.os.Parcel; 27import android.os.Parcelable; 28import android.os.SystemClock; 29import android.support.v4.os.ParcelableCompat; 30import android.support.v4.os.ParcelableCompatCreatorCallbacks; 31import android.support.v4.widget.EdgeEffectCompat; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.view.FocusFinder; 35import android.view.Gravity; 36import android.view.KeyEvent; 37import android.view.MotionEvent; 38import android.view.SoundEffectConstants; 39import android.view.VelocityTracker; 40import android.view.View; 41import android.view.ViewConfiguration; 42import android.view.ViewGroup; 43import android.view.ViewParent; 44import android.view.accessibility.AccessibilityEvent; 45import android.view.animation.Interpolator; 46import android.widget.Scroller; 47 48import java.util.ArrayList; 49import java.util.Collections; 50import java.util.Comparator; 51 52/** 53 * Layout manager that allows the user to flip left and right 54 * through pages of data. You supply an implementation of a 55 * {@link PagerAdapter} to generate the pages that the view shows. 56 * 57 * <p>Note this class is currently under early design and 58 * development. The API will likely change in later updates of 59 * the compatibility library, requiring changes to the source code 60 * of apps when they are compiled against the newer version.</p> 61 * 62 * <p>ViewPager is most often used in conjunction with {@link android.app.Fragment}, 63 * which is a convenient way to supply and manage the lifecycle of each page. 64 * There are standard adapters implemented for using fragments with the ViewPager, 65 * which cover the most common use cases. These are 66 * {@link android.support.v4.app.FragmentPagerAdapter}, 67 * {@link android.support.v4.app.FragmentStatePagerAdapter}, 68 * {@link android.support.v13.app.FragmentPagerAdapter}, and 69 * {@link android.support.v13.app.FragmentStatePagerAdapter}; each of these 70 * classes have simple code showing how to build a full user interface 71 * with them. 72 * 73 * <p>Here is a more complicated example of ViewPager, using it in conjuction 74 * with {@link android.app.ActionBar} tabs. You can find other examples of using 75 * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code. 76 * 77 * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java 78 * complete} 79 */ 80public class ViewPager extends ViewGroup { 81 private static final String TAG = "ViewPager"; 82 private static final boolean DEBUG = false; 83 84 private static final boolean USE_CACHE = false; 85 86 private static final int DEFAULT_OFFSCREEN_PAGES = 1; 87 private static final int MAX_SETTLE_DURATION = 600; // ms 88 private static final int MIN_DISTANCE_FOR_FLING = 25; // dips 89 90 private static final int[] LAYOUT_ATTRS = new int[] { 91 android.R.attr.layout_gravity 92 }; 93 94 static class ItemInfo { 95 Object object; 96 int position; 97 boolean scrolling; 98 float widthFactor; 99 float offset; 100 } 101 102 private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){ 103 @Override 104 public int compare(ItemInfo lhs, ItemInfo rhs) { 105 return lhs.position - rhs.position; 106 } 107 }; 108 109 private static final Interpolator sInterpolator = new Interpolator() { 110 public float getInterpolation(float t) { 111 t -= 1.0f; 112 return t * t * t * t * t + 1.0f; 113 } 114 }; 115 116 private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>(); 117 private final ItemInfo mTempItem = new ItemInfo(); 118 119 private PagerAdapter mAdapter; 120 private int mCurItem; // Index of currently displayed page. 121 private int mRestoredCurItem = -1; 122 private Parcelable mRestoredAdapterState = null; 123 private ClassLoader mRestoredClassLoader = null; 124 private Scroller mScroller; 125 private PagerObserver mObserver; 126 127 private int mPageMargin; 128 private Drawable mMarginDrawable; 129 private int mTopPageBounds; 130 private int mBottomPageBounds; 131 132 // Offsets of the first and last items, if known. 133 // Set during population, used to determine if we are at the beginning 134 // or end of the pager data set during touch scrolling. 135 private float mFirstOffset = -Float.MAX_VALUE; 136 private float mLastOffset = Float.MAX_VALUE; 137 138 private int mChildWidthMeasureSpec; 139 private int mChildHeightMeasureSpec; 140 private boolean mInLayout; 141 142 private boolean mScrollingCacheEnabled; 143 144 private boolean mPopulatePending; 145 private boolean mScrolling; 146 private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; 147 148 private boolean mIsBeingDragged; 149 private boolean mIsUnableToDrag; 150 private int mTouchSlop; 151 private float mInitialMotionX; 152 /** 153 * Position of the last motion event. 154 */ 155 private float mLastMotionX; 156 private float mLastMotionY; 157 /** 158 * ID of the active pointer. This is used to retain consistency during 159 * drags/flings if multiple pointers are used. 160 */ 161 private int mActivePointerId = INVALID_POINTER; 162 /** 163 * Sentinel value for no current active pointer. 164 * Used by {@link #mActivePointerId}. 165 */ 166 private static final int INVALID_POINTER = -1; 167 168 /** 169 * Determines speed during touch scrolling 170 */ 171 private VelocityTracker mVelocityTracker; 172 private int mMinimumVelocity; 173 private int mMaximumVelocity; 174 private int mFlingDistance; 175 private int mCloseEnough; 176 177 // If the pager is at least this close to its final position, complete the scroll 178 // on touch down and let the user interact with the content inside instead of 179 // "catching" the flinging pager. 180 private static final int CLOSE_ENOUGH = 2; // dp 181 182 private boolean mFakeDragging; 183 private long mFakeDragBeginTime; 184 185 private EdgeEffectCompat mLeftEdge; 186 private EdgeEffectCompat mRightEdge; 187 188 private boolean mFirstLayout = true; 189 private boolean mNeedCalculatePageOffsets = false; 190 private boolean mCalledSuper; 191 private int mDecorChildCount; 192 193 private OnPageChangeListener mOnPageChangeListener; 194 private OnPageChangeListener mInternalPageChangeListener; 195 private OnAdapterChangeListener mAdapterChangeListener; 196 197 /** 198 * Indicates that the pager is in an idle, settled state. The current page 199 * is fully in view and no animation is in progress. 200 */ 201 public static final int SCROLL_STATE_IDLE = 0; 202 203 /** 204 * Indicates that the pager is currently being dragged by the user. 205 */ 206 public static final int SCROLL_STATE_DRAGGING = 1; 207 208 /** 209 * Indicates that the pager is in the process of settling to a final position. 210 */ 211 public static final int SCROLL_STATE_SETTLING = 2; 212 213 private int mScrollState = SCROLL_STATE_IDLE; 214 215 /** 216 * Callback interface for responding to changing state of the selected page. 217 */ 218 public interface OnPageChangeListener { 219 220 /** 221 * This method will be invoked when the current page is scrolled, either as part 222 * of a programmatically initiated smooth scroll or a user initiated touch scroll. 223 * 224 * @param position Position index of the first page currently being displayed. 225 * Page position+1 will be visible if positionOffset is nonzero. 226 * @param positionOffset Value from [0, 1) indicating the offset from the page at position. 227 * @param positionOffsetPixels Value in pixels indicating the offset from position. 228 */ 229 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); 230 231 /** 232 * This method will be invoked when a new page becomes selected. Animation is not 233 * necessarily complete. 234 * 235 * @param position Position index of the new selected page. 236 */ 237 public void onPageSelected(int position); 238 239 /** 240 * Called when the scroll state changes. Useful for discovering when the user 241 * begins dragging, when the pager is automatically settling to the current page, 242 * or when it is fully stopped/idle. 243 * 244 * @param state The new scroll state. 245 * @see ViewPager#SCROLL_STATE_IDLE 246 * @see ViewPager#SCROLL_STATE_DRAGGING 247 * @see ViewPager#SCROLL_STATE_SETTLING 248 */ 249 public void onPageScrollStateChanged(int state); 250 } 251 252 /** 253 * Simple implementation of the {@link OnPageChangeListener} interface with stub 254 * implementations of each method. Extend this if you do not intend to override 255 * every method of {@link OnPageChangeListener}. 256 */ 257 public static class SimpleOnPageChangeListener implements OnPageChangeListener { 258 @Override 259 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 260 // This space for rent 261 } 262 263 @Override 264 public void onPageSelected(int position) { 265 // This space for rent 266 } 267 268 @Override 269 public void onPageScrollStateChanged(int state) { 270 // This space for rent 271 } 272 } 273 274 /** 275 * Used internally to monitor when adapters are switched. 276 */ 277 interface OnAdapterChangeListener { 278 public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); 279 } 280 281 /** 282 * Used internally to tag special types of child views that should be added as 283 * pager decorations by default. 284 */ 285 interface Decor {} 286 287 public ViewPager(Context context) { 288 super(context); 289 initViewPager(); 290 } 291 292 public ViewPager(Context context, AttributeSet attrs) { 293 super(context, attrs); 294 initViewPager(); 295 } 296 297 void initViewPager() { 298 setWillNotDraw(false); 299 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 300 setFocusable(true); 301 final Context context = getContext(); 302 mScroller = new Scroller(context, sInterpolator); 303 final ViewConfiguration configuration = ViewConfiguration.get(context); 304 mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); 305 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 306 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 307 mLeftEdge = new EdgeEffectCompat(context); 308 mRightEdge = new EdgeEffectCompat(context); 309 310 final float density = context.getResources().getDisplayMetrics().density; 311 mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); 312 mCloseEnough = (int) (CLOSE_ENOUGH * density); 313 } 314 315 private void setScrollState(int newState) { 316 if (mScrollState == newState) { 317 return; 318 } 319 320 mScrollState = newState; 321 if (mOnPageChangeListener != null) { 322 mOnPageChangeListener.onPageScrollStateChanged(newState); 323 } 324 } 325 326 /** 327 * Set a PagerAdapter that will supply views for this pager as needed. 328 * 329 * @param adapter Adapter to use 330 */ 331 public void setAdapter(PagerAdapter adapter) { 332 if (mAdapter != null) { 333 mAdapter.unregisterDataSetObserver(mObserver); 334 mAdapter.startUpdate(this); 335 for (int i = 0; i < mItems.size(); i++) { 336 final ItemInfo ii = mItems.get(i); 337 mAdapter.destroyItem(this, ii.position, ii.object); 338 } 339 mAdapter.finishUpdate(this); 340 mItems.clear(); 341 removeNonDecorViews(); 342 mCurItem = 0; 343 scrollTo(0, 0); 344 } 345 346 final PagerAdapter oldAdapter = mAdapter; 347 mAdapter = adapter; 348 349 if (mAdapter != null) { 350 if (mObserver == null) { 351 mObserver = new PagerObserver(); 352 } 353 mAdapter.registerDataSetObserver(mObserver); 354 mPopulatePending = false; 355 mFirstLayout = true; 356 if (mRestoredCurItem >= 0) { 357 mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); 358 setCurrentItemInternal(mRestoredCurItem, false, true); 359 mRestoredCurItem = -1; 360 mRestoredAdapterState = null; 361 mRestoredClassLoader = null; 362 } else { 363 populate(); 364 } 365 } 366 367 if (mAdapterChangeListener != null && oldAdapter != adapter) { 368 mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); 369 } 370 } 371 372 private void removeNonDecorViews() { 373 for (int i = 0; i < getChildCount(); i++) { 374 final View child = getChildAt(i); 375 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 376 if (!lp.isDecor) { 377 removeViewAt(i); 378 i--; 379 } 380 } 381 } 382 383 /** 384 * Retrieve the current adapter supplying pages. 385 * 386 * @return The currently registered PagerAdapter 387 */ 388 public PagerAdapter getAdapter() { 389 return mAdapter; 390 } 391 392 void setOnAdapterChangeListener(OnAdapterChangeListener listener) { 393 mAdapterChangeListener = listener; 394 } 395 396 /** 397 * Set the currently selected page. If the ViewPager has already been through its first 398 * layout with its current adapter there will be a smooth animated transition between 399 * the current item and the specified item. 400 * 401 * @param item Item index to select 402 */ 403 public void setCurrentItem(int item) { 404 mPopulatePending = false; 405 setCurrentItemInternal(item, !mFirstLayout, false); 406 } 407 408 /** 409 * Set the currently selected page. 410 * 411 * @param item Item index to select 412 * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately 413 */ 414 public void setCurrentItem(int item, boolean smoothScroll) { 415 mPopulatePending = false; 416 setCurrentItemInternal(item, smoothScroll, false); 417 } 418 419 public int getCurrentItem() { 420 return mCurItem; 421 } 422 423 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { 424 setCurrentItemInternal(item, smoothScroll, always, 0); 425 } 426 427 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { 428 if (mAdapter == null || mAdapter.getCount() <= 0) { 429 setScrollingCacheEnabled(false); 430 return; 431 } 432 if (!always && mCurItem == item && mItems.size() != 0) { 433 setScrollingCacheEnabled(false); 434 return; 435 } 436 if (item < 0) { 437 item = 0; 438 } else if (item >= mAdapter.getCount()) { 439 item = mAdapter.getCount() - 1; 440 } 441 final int pageLimit = mOffscreenPageLimit; 442 if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { 443 // We are doing a jump by more than one page. To avoid 444 // glitches, we want to keep all current pages in the view 445 // until the scroll ends. 446 for (int i=0; i<mItems.size(); i++) { 447 mItems.get(i).scrolling = true; 448 } 449 } 450 final boolean dispatchSelected = mCurItem != item; 451 populate(item); 452 final ItemInfo curInfo = infoForPosition(item); 453 int destX = 0; 454 if (curInfo != null) { 455 final int width = getWidth(); 456 destX = (int) (width * Math.max(mFirstOffset, 457 Math.min(curInfo.offset, mLastOffset))); 458 } 459 if (smoothScroll) { 460 smoothScrollTo(destX, 0, velocity); 461 if (dispatchSelected && mOnPageChangeListener != null) { 462 mOnPageChangeListener.onPageSelected(item); 463 } 464 if (dispatchSelected && mInternalPageChangeListener != null) { 465 mInternalPageChangeListener.onPageSelected(item); 466 } 467 } else { 468 if (dispatchSelected && mOnPageChangeListener != null) { 469 mOnPageChangeListener.onPageSelected(item); 470 } 471 if (dispatchSelected && mInternalPageChangeListener != null) { 472 mInternalPageChangeListener.onPageSelected(item); 473 } 474 completeScroll(); 475 scrollTo(destX, 0); 476 } 477 } 478 479 /** 480 * Set a listener that will be invoked whenever the page changes or is incrementally 481 * scrolled. See {@link OnPageChangeListener}. 482 * 483 * @param listener Listener to set 484 */ 485 public void setOnPageChangeListener(OnPageChangeListener listener) { 486 mOnPageChangeListener = listener; 487 } 488 489 /** 490 * Set a separate OnPageChangeListener for internal use by the support library. 491 * 492 * @param listener Listener to set 493 * @return The old listener that was set, if any. 494 */ 495 OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { 496 OnPageChangeListener oldListener = mInternalPageChangeListener; 497 mInternalPageChangeListener = listener; 498 return oldListener; 499 } 500 501 /** 502 * Returns the number of pages that will be retained to either side of the 503 * current page in the view hierarchy in an idle state. Defaults to 1. 504 * 505 * @return How many pages will be kept offscreen on either side 506 * @see #setOffscreenPageLimit(int) 507 */ 508 public int getOffscreenPageLimit() { 509 return mOffscreenPageLimit; 510 } 511 512 /** 513 * Set the number of pages that should be retained to either side of the 514 * current page in the view hierarchy in an idle state. Pages beyond this 515 * limit will be recreated from the adapter when needed. 516 * 517 * <p>This is offered as an optimization. If you know in advance the number 518 * of pages you will need to support or have lazy-loading mechanisms in place 519 * on your pages, tweaking this setting can have benefits in perceived smoothness 520 * of paging animations and interaction. If you have a small number of pages (3-4) 521 * that you can keep active all at once, less time will be spent in layout for 522 * newly created view subtrees as the user pages back and forth.</p> 523 * 524 * <p>You should keep this limit low, especially if your pages have complex layouts. 525 * This setting defaults to 1.</p> 526 * 527 * @param limit How many pages will be kept offscreen in an idle state. 528 */ 529 public void setOffscreenPageLimit(int limit) { 530 if (limit < DEFAULT_OFFSCREEN_PAGES) { 531 Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + 532 DEFAULT_OFFSCREEN_PAGES); 533 limit = DEFAULT_OFFSCREEN_PAGES; 534 } 535 if (limit != mOffscreenPageLimit) { 536 mOffscreenPageLimit = limit; 537 populate(); 538 } 539 } 540 541 /** 542 * Set the margin between pages. 543 * 544 * @param marginPixels Distance between adjacent pages in pixels 545 * @see #getPageMargin() 546 * @see #setPageMarginDrawable(Drawable) 547 * @see #setPageMarginDrawable(int) 548 */ 549 public void setPageMargin(int marginPixels) { 550 final int oldMargin = mPageMargin; 551 mPageMargin = marginPixels; 552 553 final int width = getWidth(); 554 recomputeScrollPosition(width, width, marginPixels, oldMargin); 555 556 requestLayout(); 557 } 558 559 /** 560 * Return the margin between pages. 561 * 562 * @return The size of the margin in pixels 563 */ 564 public int getPageMargin() { 565 return mPageMargin; 566 } 567 568 /** 569 * Set a drawable that will be used to fill the margin between pages. 570 * 571 * @param d Drawable to display between pages 572 */ 573 public void setPageMarginDrawable(Drawable d) { 574 mMarginDrawable = d; 575 if (d != null) refreshDrawableState(); 576 setWillNotDraw(d == null); 577 invalidate(); 578 } 579 580 /** 581 * Set a drawable that will be used to fill the margin between pages. 582 * 583 * @param resId Resource ID of a drawable to display between pages 584 */ 585 public void setPageMarginDrawable(int resId) { 586 setPageMarginDrawable(getContext().getResources().getDrawable(resId)); 587 } 588 589 @Override 590 protected boolean verifyDrawable(Drawable who) { 591 return super.verifyDrawable(who) || who == mMarginDrawable; 592 } 593 594 @Override 595 protected void drawableStateChanged() { 596 super.drawableStateChanged(); 597 final Drawable d = mMarginDrawable; 598 if (d != null && d.isStateful()) { 599 d.setState(getDrawableState()); 600 } 601 } 602 603 // We want the duration of the page snap animation to be influenced by the distance that 604 // the screen has to travel, however, we don't want this duration to be effected in a 605 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 606 // of travel has on the overall snap duration. 607 float distanceInfluenceForSnapDuration(float f) { 608 f -= 0.5f; // center the values about 0. 609 f *= 0.3f * Math.PI / 2.0f; 610 return (float) Math.sin(f); 611 } 612 613 /** 614 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 615 * 616 * @param x the number of pixels to scroll by on the X axis 617 * @param y the number of pixels to scroll by on the Y axis 618 */ 619 void smoothScrollTo(int x, int y) { 620 smoothScrollTo(x, y, 0); 621 } 622 623 /** 624 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 625 * 626 * @param x the number of pixels to scroll by on the X axis 627 * @param y the number of pixels to scroll by on the Y axis 628 * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) 629 */ 630 void smoothScrollTo(int x, int y, int velocity) { 631 if (getChildCount() == 0) { 632 // Nothing to do. 633 setScrollingCacheEnabled(false); 634 return; 635 } 636 int sx = getScrollX(); 637 int sy = getScrollY(); 638 int dx = x - sx; 639 int dy = y - sy; 640 if (dx == 0 && dy == 0) { 641 completeScroll(); 642 populate(); 643 setScrollState(SCROLL_STATE_IDLE); 644 return; 645 } 646 647 setScrollingCacheEnabled(true); 648 mScrolling = true; 649 setScrollState(SCROLL_STATE_SETTLING); 650 651 final int width = getWidth(); 652 final int halfWidth = width / 2; 653 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); 654 final float distance = halfWidth + halfWidth * 655 distanceInfluenceForSnapDuration(distanceRatio); 656 657 int duration = 0; 658 velocity = Math.abs(velocity); 659 if (velocity > 0) { 660 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 661 } else { 662 final float pageWidth = width * mAdapter.getPageWidth(mCurItem); 663 final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); 664 duration = (int) ((pageDelta + 1) * 100); 665 } 666 duration = Math.min(duration, MAX_SETTLE_DURATION); 667 668 mScroller.startScroll(sx, sy, dx, dy, duration); 669 ViewCompat.postInvalidateOnAnimation(this); 670 } 671 672 ItemInfo addNewItem(int position, int index) { 673 ItemInfo ii = new ItemInfo(); 674 ii.position = position; 675 ii.object = mAdapter.instantiateItem(this, position); 676 ii.widthFactor = mAdapter.getPageWidth(position); 677 if (index < 0 || index >= mItems.size()) { 678 mItems.add(ii); 679 } else { 680 mItems.add(index, ii); 681 } 682 return ii; 683 } 684 685 void dataSetChanged() { 686 // This method only gets called if our observer is attached, so mAdapter is non-null. 687 688 boolean needPopulate = mItems.size() < 3 && mItems.size() < mAdapter.getCount(); 689 int newCurrItem = -1; 690 691 boolean isUpdating = false; 692 for (int i = 0; i < mItems.size(); i++) { 693 final ItemInfo ii = mItems.get(i); 694 final int newPos = mAdapter.getItemPosition(ii.object); 695 696 if (newPos == PagerAdapter.POSITION_UNCHANGED) { 697 continue; 698 } 699 700 if (newPos == PagerAdapter.POSITION_NONE) { 701 mItems.remove(i); 702 i--; 703 704 if (!isUpdating) { 705 mAdapter.startUpdate(this); 706 isUpdating = true; 707 } 708 709 mAdapter.destroyItem(this, ii.position, ii.object); 710 needPopulate = true; 711 712 if (mCurItem == ii.position) { 713 // Keep the current item in the valid range 714 newCurrItem = Math.max(0, Math.min(mCurItem, mAdapter.getCount() - 1)); 715 } 716 continue; 717 } 718 719 if (ii.position != newPos) { 720 if (ii.position == mCurItem) { 721 // Our current item changed position. Follow it. 722 newCurrItem = newPos; 723 } 724 725 ii.position = newPos; 726 needPopulate = true; 727 } 728 } 729 730 if (isUpdating) { 731 mAdapter.finishUpdate(this); 732 } 733 734 Collections.sort(mItems, COMPARATOR); 735 736 if (needPopulate) { 737 // Reset our known page widths; populate will recompute them. 738 final int childCount = getChildCount(); 739 for (int i = 0; i < childCount; i++) { 740 final View child = getChildAt(i); 741 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 742 if (!lp.isDecor) { 743 lp.widthFactor = 0.f; 744 } 745 } 746 } 747 748 if (newCurrItem >= 0) { 749 // TODO This currently causes a jump. 750 setCurrentItemInternal(newCurrItem, false, true); 751 needPopulate = true; 752 } 753 if (needPopulate) { 754 populate(); 755 requestLayout(); 756 } 757 } 758 759 void populate() { 760 populate(mCurItem); 761 } 762 763 void populate(int newCurrentItem) { 764 ItemInfo oldCurInfo = null; 765 if (mCurItem != newCurrentItem) { 766 oldCurInfo = infoForPosition(mCurItem); 767 mCurItem = newCurrentItem; 768 } 769 770 if (mAdapter == null) { 771 return; 772 } 773 774 // Bail now if we are waiting to populate. This is to hold off 775 // on creating views from the time the user releases their finger to 776 // fling to a new position until we have finished the scroll to 777 // that position, avoiding glitches from happening at that point. 778 if (mPopulatePending) { 779 if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 780 return; 781 } 782 783 // Also, don't populate until we are attached to a window. This is to 784 // avoid trying to populate before we have restored our view hierarchy 785 // state and conflicting with what is restored. 786 if (getWindowToken() == null) { 787 return; 788 } 789 790 mAdapter.startUpdate(this); 791 792 final int pageLimit = mOffscreenPageLimit; 793 final int startPos = Math.max(0, mCurItem - pageLimit); 794 final int N = mAdapter.getCount(); 795 final int endPos = Math.min(N-1, mCurItem + pageLimit); 796 797 // Locate the currently focused item or add it if needed. 798 int curIndex = -1; 799 ItemInfo curItem = null; 800 for (curIndex = 0; curIndex < mItems.size(); curIndex++) { 801 final ItemInfo ii = mItems.get(curIndex); 802 if (ii.position >= mCurItem) { 803 if (ii.position == mCurItem) curItem = ii; 804 break; 805 } 806 } 807 808 if (curItem == null && N > 0) { 809 curItem = addNewItem(mCurItem, curIndex); 810 } 811 812 // Fill 3x the available width or up to the number of offscreen 813 // pages requested to either side, whichever is larger. 814 // If we have no current item we have no work to do. 815 if (curItem != null) { 816 float extraWidthLeft = 0.f; 817 int itemIndex = curIndex - 1; 818 ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 819 final float leftWidthNeeded = 2.f - curItem.widthFactor; 820 for (int pos = mCurItem - 1; pos >= 0; pos--) { 821 if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { 822 if (ii == null) { 823 break; 824 } 825 if (pos == ii.position && !ii.scrolling) { 826 mItems.remove(itemIndex); 827 mAdapter.destroyItem(this, pos, ii.object); 828 itemIndex--; 829 curIndex--; 830 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 831 } 832 } else if (ii != null && pos == ii.position) { 833 extraWidthLeft += ii.widthFactor; 834 itemIndex--; 835 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 836 } else { 837 ii = addNewItem(pos, itemIndex + 1); 838 extraWidthLeft += ii.widthFactor; 839 curIndex++; 840 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 841 } 842 } 843 844 float extraWidthRight = curItem.widthFactor; 845 itemIndex = curIndex + 1; 846 if (extraWidthRight < 2.f) { 847 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 848 for (int pos = mCurItem + 1; pos < N; pos++) { 849 if (extraWidthRight >= 2.f && pos > endPos) { 850 if (ii == null) { 851 break; 852 } 853 if (pos == ii.position && !ii.scrolling) { 854 mItems.remove(itemIndex); 855 mAdapter.destroyItem(this, pos, ii.object); 856 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 857 } 858 } else if (ii != null && pos == ii.position) { 859 extraWidthRight += ii.widthFactor; 860 itemIndex++; 861 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 862 } else { 863 ii = addNewItem(pos, itemIndex); 864 itemIndex++; 865 extraWidthRight += ii.widthFactor; 866 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 867 } 868 } 869 } 870 871 calculatePageOffsets(curItem, curIndex, oldCurInfo); 872 } 873 874 if (DEBUG) { 875 Log.i(TAG, "Current page list:"); 876 for (int i=0; i<mItems.size(); i++) { 877 Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); 878 } 879 } 880 881 mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); 882 883 mAdapter.finishUpdate(this); 884 885 // Check width measurement of current pages. Update LayoutParams as needed. 886 final int childCount = getChildCount(); 887 for (int i = 0; i < childCount; i++) { 888 final View child = getChildAt(i); 889 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 890 if (!lp.isDecor && lp.widthFactor == 0.f) { 891 // 0 means requery the adapter for this, it doesn't have a valid width. 892 final ItemInfo ii = infoForChild(child); 893 if (ii != null) { 894 lp.widthFactor = ii.widthFactor; 895 } 896 } 897 } 898 899 if (hasFocus()) { 900 View currentFocused = findFocus(); 901 ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; 902 if (ii == null || ii.position != mCurItem) { 903 for (int i=0; i<getChildCount(); i++) { 904 View child = getChildAt(i); 905 ii = infoForChild(child); 906 if (ii != null && ii.position == mCurItem) { 907 if (child.requestFocus(FOCUS_FORWARD)) { 908 break; 909 } 910 } 911 } 912 } 913 } 914 } 915 916 private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { 917 final int N = mAdapter.getCount(); 918 final int width = getWidth(); 919 final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; 920 // Fix up offsets for later layout. 921 if (oldCurInfo != null) { 922 final int oldCurPosition = oldCurInfo.position; 923 // Base offsets off of oldCurInfo. 924 if (oldCurPosition < curItem.position) { 925 int itemIndex = 0; 926 ItemInfo ii = null; 927 float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; 928 for (int pos = oldCurPosition + 1; 929 pos <= curItem.position && itemIndex < mItems.size(); pos++) { 930 ii = mItems.get(itemIndex); 931 while (pos > ii.position && itemIndex < mItems.size() - 1) { 932 itemIndex++; 933 ii = mItems.get(itemIndex); 934 } 935 while (pos < ii.position) { 936 // We don't have an item populated for this, 937 // ask the adapter for an offset. 938 offset += mAdapter.getPageWidth(pos) + marginOffset; 939 pos++; 940 } 941 ii.offset = offset; 942 offset += ii.widthFactor + marginOffset; 943 } 944 } else if (oldCurPosition > curItem.position) { 945 int itemIndex = mItems.size() - 1; 946 ItemInfo ii = null; 947 float offset = oldCurInfo.offset; 948 for (int pos = oldCurPosition - 1; 949 pos >= curItem.position && itemIndex >= 0; pos--) { 950 ii = mItems.get(itemIndex); 951 while (pos < ii.position && itemIndex > 0) { 952 itemIndex--; 953 ii = mItems.get(itemIndex); 954 } 955 while (pos > ii.position) { 956 // We don't have an item populated for this, 957 // ask the adapter for an offset. 958 offset -= mAdapter.getPageWidth(pos) + marginOffset; 959 pos--; 960 } 961 offset -= ii.widthFactor + marginOffset; 962 ii.offset = offset; 963 } 964 } 965 } 966 967 // Base all offsets off of curItem. 968 final int itemCount = mItems.size(); 969 float offset = curItem.offset; 970 int pos = curItem.position - 1; 971 mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; 972 mLastOffset = curItem.position == N - 1 ? 973 curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; 974 // Previous pages 975 for (int i = curIndex - 1; i >= 0; i--, pos--) { 976 final ItemInfo ii = mItems.get(i); 977 while (pos > ii.position) { 978 offset -= mAdapter.getPageWidth(pos--) + marginOffset; 979 } 980 offset -= ii.widthFactor + marginOffset; 981 ii.offset = offset; 982 if (ii.position == 0) mFirstOffset = offset; 983 } 984 offset = curItem.offset + curItem.widthFactor + marginOffset; 985 pos = curItem.position + 1; 986 // Next pages 987 for (int i = curIndex + 1; i < itemCount; i++, pos++) { 988 final ItemInfo ii = mItems.get(i); 989 while (pos < ii.position) { 990 offset += mAdapter.getPageWidth(pos++) + marginOffset; 991 } 992 if (ii.position == N - 1) { 993 mLastOffset = offset + ii.widthFactor - 1; 994 } 995 ii.offset = offset; 996 offset += ii.widthFactor + marginOffset; 997 } 998 999 mNeedCalculatePageOffsets = false; 1000 } 1001 1002 /** 1003 * This is the persistent state that is saved by ViewPager. Only needed 1004 * if you are creating a sublass of ViewPager that must save its own 1005 * state, in which case it should implement a subclass of this which 1006 * contains that state. 1007 */ 1008 public static class SavedState extends BaseSavedState { 1009 int position; 1010 Parcelable adapterState; 1011 ClassLoader loader; 1012 1013 public SavedState(Parcelable superState) { 1014 super(superState); 1015 } 1016 1017 @Override 1018 public void writeToParcel(Parcel out, int flags) { 1019 super.writeToParcel(out, flags); 1020 out.writeInt(position); 1021 out.writeParcelable(adapterState, flags); 1022 } 1023 1024 @Override 1025 public String toString() { 1026 return "FragmentPager.SavedState{" 1027 + Integer.toHexString(System.identityHashCode(this)) 1028 + " position=" + position + "}"; 1029 } 1030 1031 public static final Parcelable.Creator<SavedState> CREATOR 1032 = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() { 1033 @Override 1034 public SavedState createFromParcel(Parcel in, ClassLoader loader) { 1035 return new SavedState(in, loader); 1036 } 1037 @Override 1038 public SavedState[] newArray(int size) { 1039 return new SavedState[size]; 1040 } 1041 }); 1042 1043 SavedState(Parcel in, ClassLoader loader) { 1044 super(in); 1045 if (loader == null) { 1046 loader = getClass().getClassLoader(); 1047 } 1048 position = in.readInt(); 1049 adapterState = in.readParcelable(loader); 1050 this.loader = loader; 1051 } 1052 } 1053 1054 @Override 1055 public Parcelable onSaveInstanceState() { 1056 Parcelable superState = super.onSaveInstanceState(); 1057 SavedState ss = new SavedState(superState); 1058 ss.position = mCurItem; 1059 if (mAdapter != null) { 1060 ss.adapterState = mAdapter.saveState(); 1061 } 1062 return ss; 1063 } 1064 1065 @Override 1066 public void onRestoreInstanceState(Parcelable state) { 1067 if (!(state instanceof SavedState)) { 1068 super.onRestoreInstanceState(state); 1069 return; 1070 } 1071 1072 SavedState ss = (SavedState)state; 1073 super.onRestoreInstanceState(ss.getSuperState()); 1074 1075 if (mAdapter != null) { 1076 mAdapter.restoreState(ss.adapterState, ss.loader); 1077 setCurrentItemInternal(ss.position, false, true); 1078 } else { 1079 mRestoredCurItem = ss.position; 1080 mRestoredAdapterState = ss.adapterState; 1081 mRestoredClassLoader = ss.loader; 1082 } 1083 } 1084 1085 @Override 1086 public void addView(View child, int index, ViewGroup.LayoutParams params) { 1087 if (!checkLayoutParams(params)) { 1088 params = generateLayoutParams(params); 1089 } 1090 final LayoutParams lp = (LayoutParams) params; 1091 lp.isDecor |= child instanceof Decor; 1092 if (mInLayout) { 1093 if (lp != null && lp.isDecor) { 1094 throw new IllegalStateException("Cannot add pager decor view during layout"); 1095 } 1096 lp.needsMeasure = true; 1097 addViewInLayout(child, index, params); 1098 } else { 1099 super.addView(child, index, params); 1100 } 1101 1102 if (USE_CACHE) { 1103 if (child.getVisibility() != GONE) { 1104 child.setDrawingCacheEnabled(mScrollingCacheEnabled); 1105 } else { 1106 child.setDrawingCacheEnabled(false); 1107 } 1108 } 1109 } 1110 1111 ItemInfo infoForChild(View child) { 1112 for (int i=0; i<mItems.size(); i++) { 1113 ItemInfo ii = mItems.get(i); 1114 if (mAdapter.isViewFromObject(child, ii.object)) { 1115 return ii; 1116 } 1117 } 1118 return null; 1119 } 1120 1121 ItemInfo infoForAnyChild(View child) { 1122 ViewParent parent; 1123 while ((parent=child.getParent()) != this) { 1124 if (parent == null || !(parent instanceof View)) { 1125 return null; 1126 } 1127 child = (View)parent; 1128 } 1129 return infoForChild(child); 1130 } 1131 1132 ItemInfo infoForPosition(int position) { 1133 for (int i = 0; i < mItems.size(); i++) { 1134 ItemInfo ii = mItems.get(i); 1135 if (ii.position == position) { 1136 return ii; 1137 } 1138 } 1139 return null; 1140 } 1141 1142 @Override 1143 protected void onAttachedToWindow() { 1144 super.onAttachedToWindow(); 1145 mFirstLayout = true; 1146 } 1147 1148 @Override 1149 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1150 // For simple implementation, or internal size is always 0. 1151 // We depend on the container to specify the layout size of 1152 // our view. We can't really know what it is since we will be 1153 // adding and removing different arbitrary views and do not 1154 // want the layout to change as this happens. 1155 setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), 1156 getDefaultSize(0, heightMeasureSpec)); 1157 1158 // Children are just made to fill our space. 1159 int childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 1160 int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); 1161 1162 /* 1163 * Make sure all children have been properly measured. Decor views first. 1164 * Right now we cheat and make this less complicated by assuming decor 1165 * views won't intersect. We will pin to edges based on gravity. 1166 */ 1167 int size = getChildCount(); 1168 for (int i = 0; i < size; ++i) { 1169 final View child = getChildAt(i); 1170 if (child.getVisibility() != GONE) { 1171 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1172 if (lp != null && lp.isDecor) { 1173 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1174 final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1175 int widthMode = MeasureSpec.AT_MOST; 1176 int heightMode = MeasureSpec.AT_MOST; 1177 boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; 1178 boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT; 1179 1180 if (consumeVertical) { 1181 widthMode = MeasureSpec.EXACTLY; 1182 } else if (consumeHorizontal) { 1183 heightMode = MeasureSpec.EXACTLY; 1184 } 1185 1186 int widthSize = childWidthSize; 1187 int heightSize = childHeightSize; 1188 if (lp.width != LayoutParams.WRAP_CONTENT) { 1189 widthMode = MeasureSpec.EXACTLY; 1190 if (lp.width != LayoutParams.FILL_PARENT) { 1191 widthSize = lp.width; 1192 } 1193 } 1194 if (lp.height != LayoutParams.WRAP_CONTENT) { 1195 heightMode = MeasureSpec.EXACTLY; 1196 if (lp.height != LayoutParams.FILL_PARENT) { 1197 heightSize = lp.height; 1198 } 1199 } 1200 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); 1201 final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); 1202 child.measure(widthSpec, heightSpec); 1203 1204 if (consumeVertical) { 1205 childHeightSize -= child.getMeasuredHeight(); 1206 } else if (consumeHorizontal) { 1207 childWidthSize -= child.getMeasuredWidth(); 1208 } 1209 } 1210 } 1211 } 1212 1213 mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); 1214 mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); 1215 1216 // Make sure we have created all fragments that we need to have shown. 1217 mInLayout = true; 1218 populate(); 1219 mInLayout = false; 1220 1221 // Page views next. 1222 size = getChildCount(); 1223 for (int i = 0; i < size; ++i) { 1224 final View child = getChildAt(i); 1225 if (child.getVisibility() != GONE) { 1226 if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child 1227 + ": " + mChildWidthMeasureSpec); 1228 1229 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1230 if (lp == null || !lp.isDecor) { 1231 final int widthSpec = MeasureSpec.makeMeasureSpec( 1232 (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); 1233 child.measure(widthSpec, mChildHeightMeasureSpec); 1234 } 1235 } 1236 } 1237 } 1238 1239 @Override 1240 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1241 super.onSizeChanged(w, h, oldw, oldh); 1242 1243 // Make sure scroll position is set correctly. 1244 if (w != oldw) { 1245 recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); 1246 } 1247 } 1248 1249 private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) { 1250 if (oldWidth > 0 && !mItems.isEmpty()) { 1251 final int xpos = getScrollX(); 1252 final ItemInfo ii = infoForCurrentScrollPosition(); 1253 final float pageOffset = (((float) xpos / width) - ii.offset) / ii.widthFactor; 1254 final int offsetPixels = (int) (pageOffset * width); 1255 1256 scrollTo(offsetPixels, getScrollY()); 1257 if (!mScroller.isFinished()) { 1258 // We now return to your regularly scheduled scroll, already in progress. 1259 final int newDuration = mScroller.getDuration() - mScroller.timePassed(); 1260 ItemInfo targetInfo = infoForPosition(mCurItem); 1261 mScroller.startScroll(offsetPixels, 0, (int) (targetInfo.offset * width), 0, 1262 newDuration); 1263 } 1264 } else { 1265 final ItemInfo ii = infoForPosition(mCurItem); 1266 final int scrollPos = 1267 (int) ((ii != null ? Math.min(ii.offset, mLastOffset) : 0) * width); 1268 if (scrollPos != getScrollX()) { 1269 completeScroll(); 1270 scrollTo(scrollPos, getScrollY()); 1271 } 1272 } 1273 } 1274 1275 @Override 1276 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1277 mInLayout = true; 1278 populate(); 1279 mInLayout = false; 1280 1281 final int count = getChildCount(); 1282 int width = r - l; 1283 int height = b - t; 1284 int paddingLeft = getPaddingLeft(); 1285 int paddingTop = getPaddingTop(); 1286 int paddingRight = getPaddingRight(); 1287 int paddingBottom = getPaddingBottom(); 1288 final int scrollX = getScrollX(); 1289 1290 int decorCount = 0; 1291 1292 // First pass - decor views. We need to do this in two passes so that 1293 // we have the proper offsets for non-decor views later. 1294 for (int i = 0; i < count; i++) { 1295 final View child = getChildAt(i); 1296 if (child.getVisibility() != GONE) { 1297 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1298 int childLeft = 0; 1299 int childTop = 0; 1300 if (lp.isDecor) { 1301 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1302 final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1303 switch (hgrav) { 1304 default: 1305 childLeft = paddingLeft; 1306 break; 1307 case Gravity.LEFT: 1308 childLeft = paddingLeft; 1309 paddingLeft += child.getMeasuredWidth(); 1310 break; 1311 case Gravity.CENTER_HORIZONTAL: 1312 childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1313 paddingLeft); 1314 break; 1315 case Gravity.RIGHT: 1316 childLeft = width - paddingRight - child.getMeasuredWidth(); 1317 paddingRight += child.getMeasuredWidth(); 1318 break; 1319 } 1320 switch (vgrav) { 1321 default: 1322 childTop = paddingTop; 1323 break; 1324 case Gravity.TOP: 1325 childTop = paddingTop; 1326 paddingTop += child.getMeasuredHeight(); 1327 break; 1328 case Gravity.CENTER_VERTICAL: 1329 childTop = Math.max((height - child.getMeasuredHeight()) / 2, 1330 paddingTop); 1331 break; 1332 case Gravity.BOTTOM: 1333 childTop = height - paddingBottom - child.getMeasuredHeight(); 1334 paddingBottom += child.getMeasuredHeight(); 1335 break; 1336 } 1337 childLeft += scrollX; 1338 child.layout(childLeft, childTop, 1339 childLeft + child.getMeasuredWidth(), 1340 childTop + child.getMeasuredHeight()); 1341 decorCount++; 1342 } 1343 } 1344 } 1345 1346 // Page views. Do this once we have the right padding offsets from above. 1347 for (int i = 0; i < count; i++) { 1348 final View child = getChildAt(i); 1349 if (child.getVisibility() != GONE) { 1350 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1351 ItemInfo ii; 1352 if (!lp.isDecor && (ii = infoForChild(child)) != null) { 1353 int loff = (int) (width * ii.offset); 1354 int childLeft = paddingLeft + loff; 1355 int childTop = paddingTop; 1356 if (lp.needsMeasure) { 1357 // This was added during layout and needs measurement. 1358 // Do it now that we know what we're working with. 1359 lp.needsMeasure = false; 1360 final int widthSpec = MeasureSpec.makeMeasureSpec( 1361 (int) ((width - paddingLeft - paddingRight) * lp.widthFactor), 1362 MeasureSpec.EXACTLY); 1363 final int heightSpec = MeasureSpec.makeMeasureSpec( 1364 (int) (height - paddingTop - paddingBottom), 1365 MeasureSpec.EXACTLY); 1366 child.measure(widthSpec, heightSpec); 1367 } 1368 if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object 1369 + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() 1370 + "x" + child.getMeasuredHeight()); 1371 child.layout(childLeft, childTop, 1372 childLeft + child.getMeasuredWidth(), 1373 childTop + child.getMeasuredHeight()); 1374 } 1375 } 1376 } 1377 mTopPageBounds = paddingTop; 1378 mBottomPageBounds = height - paddingBottom; 1379 mDecorChildCount = decorCount; 1380 mFirstLayout = false; 1381 } 1382 1383 @Override 1384 public void computeScroll() { 1385 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { 1386 int oldX = getScrollX(); 1387 int oldY = getScrollY(); 1388 int x = mScroller.getCurrX(); 1389 int y = mScroller.getCurrY(); 1390 1391 if (oldX != x || oldY != y) { 1392 scrollTo(x, y); 1393 if (!pageScrolled(x)) { 1394 mScroller.abortAnimation(); 1395 scrollTo(0, y); 1396 } 1397 } 1398 1399 // Keep on drawing until the animation has finished. 1400 ViewCompat.postInvalidateOnAnimation(this); 1401 return; 1402 } 1403 1404 // Done with scroll, clean up state. 1405 completeScroll(); 1406 } 1407 1408 private boolean pageScrolled(int xpos) { 1409 if (mItems.size() == 0) { 1410 mCalledSuper = false; 1411 onPageScrolled(0, 0, 0); 1412 if (!mCalledSuper) { 1413 throw new IllegalStateException( 1414 "onPageScrolled did not call superclass implementation"); 1415 } 1416 return false; 1417 } 1418 final ItemInfo ii = infoForCurrentScrollPosition(); 1419 final int width = getWidth(); 1420 final int widthWithMargin = width + mPageMargin; 1421 final float marginOffset = (float) mPageMargin / width; 1422 final int currentPage = ii.position; 1423 final float pageOffset = (((float) xpos / width) - ii.offset) / 1424 (ii.widthFactor + marginOffset); 1425 final int offsetPixels = (int) (pageOffset * widthWithMargin); 1426 1427 mCalledSuper = false; 1428 onPageScrolled(currentPage, pageOffset, offsetPixels); 1429 if (!mCalledSuper) { 1430 throw new IllegalStateException( 1431 "onPageScrolled did not call superclass implementation"); 1432 } 1433 return true; 1434 } 1435 1436 /** 1437 * This method will be invoked when the current page is scrolled, either as part 1438 * of a programmatically initiated smooth scroll or a user initiated touch scroll. 1439 * If you override this method you must call through to the superclass implementation 1440 * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled 1441 * returns. 1442 * 1443 * @param position Position index of the first page currently being displayed. 1444 * Page position+1 will be visible if positionOffset is nonzero. 1445 * @param offset Value from [0, 1) indicating the offset from the page at position. 1446 * @param offsetPixels Value in pixels indicating the offset from position. 1447 */ 1448 protected void onPageScrolled(int position, float offset, int offsetPixels) { 1449 // Offset any decor views if needed - keep them on-screen at all times. 1450 if (mDecorChildCount > 0) { 1451 final int scrollX = getScrollX(); 1452 int paddingLeft = getPaddingLeft(); 1453 int paddingRight = getPaddingRight(); 1454 final int width = getWidth(); 1455 final int childCount = getChildCount(); 1456 for (int i = 0; i < childCount; i++) { 1457 final View child = getChildAt(i); 1458 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1459 if (!lp.isDecor) continue; 1460 1461 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1462 int childLeft = 0; 1463 switch (hgrav) { 1464 default: 1465 childLeft = paddingLeft; 1466 break; 1467 case Gravity.LEFT: 1468 childLeft = paddingLeft; 1469 paddingLeft += child.getWidth(); 1470 break; 1471 case Gravity.CENTER_HORIZONTAL: 1472 childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1473 paddingLeft); 1474 break; 1475 case Gravity.RIGHT: 1476 childLeft = width - paddingRight - child.getMeasuredWidth(); 1477 paddingRight += child.getMeasuredWidth(); 1478 break; 1479 } 1480 childLeft += scrollX; 1481 1482 final int childOffset = childLeft - child.getLeft(); 1483 if (childOffset != 0) { 1484 child.offsetLeftAndRight(childOffset); 1485 } 1486 } 1487 } 1488 1489 if (mOnPageChangeListener != null) { 1490 mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1491 } 1492 if (mInternalPageChangeListener != null) { 1493 mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1494 } 1495 mCalledSuper = true; 1496 } 1497 1498 private void completeScroll() { 1499 boolean needPopulate = mScrolling; 1500 if (needPopulate) { 1501 // Done with scroll, no longer want to cache view drawing. 1502 setScrollingCacheEnabled(false); 1503 mScroller.abortAnimation(); 1504 int oldX = getScrollX(); 1505 int oldY = getScrollY(); 1506 int x = mScroller.getCurrX(); 1507 int y = mScroller.getCurrY(); 1508 if (oldX != x || oldY != y) { 1509 scrollTo(x, y); 1510 } 1511 setScrollState(SCROLL_STATE_IDLE); 1512 } 1513 mPopulatePending = false; 1514 mScrolling = false; 1515 for (int i=0; i<mItems.size(); i++) { 1516 ItemInfo ii = mItems.get(i); 1517 if (ii.scrolling) { 1518 needPopulate = true; 1519 ii.scrolling = false; 1520 } 1521 } 1522 if (needPopulate) { 1523 populate(); 1524 } 1525 } 1526 1527 @Override 1528 public boolean onInterceptTouchEvent(MotionEvent ev) { 1529 /* 1530 * This method JUST determines whether we want to intercept the motion. 1531 * If we return true, onMotionEvent will be called and we do the actual 1532 * scrolling there. 1533 */ 1534 1535 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 1536 1537 // Always take care of the touch gesture being complete. 1538 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 1539 // Release the drag. 1540 if (DEBUG) Log.v(TAG, "Intercept done!"); 1541 mIsBeingDragged = false; 1542 mIsUnableToDrag = false; 1543 mActivePointerId = INVALID_POINTER; 1544 if (mVelocityTracker != null) { 1545 mVelocityTracker.recycle(); 1546 mVelocityTracker = null; 1547 } 1548 return false; 1549 } 1550 1551 // Nothing more to do here if we have decided whether or not we 1552 // are dragging. 1553 if (action != MotionEvent.ACTION_DOWN) { 1554 if (mIsBeingDragged) { 1555 if (DEBUG) Log.v(TAG, "Intercept returning true!"); 1556 return true; 1557 } 1558 if (mIsUnableToDrag) { 1559 if (DEBUG) Log.v(TAG, "Intercept returning false!"); 1560 return false; 1561 } 1562 } 1563 1564 switch (action) { 1565 case MotionEvent.ACTION_MOVE: { 1566 /* 1567 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1568 * whether the user has moved far enough from his original down touch. 1569 */ 1570 1571 /* 1572 * Locally do absolute value. mLastMotionY is set to the y value 1573 * of the down event. 1574 */ 1575 final int activePointerId = mActivePointerId; 1576 if (activePointerId == INVALID_POINTER) { 1577 // If we don't have a valid id, the touch down wasn't on content. 1578 break; 1579 } 1580 1581 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 1582 final float x = MotionEventCompat.getX(ev, pointerIndex); 1583 final float dx = x - mLastMotionX; 1584 final float xDiff = Math.abs(dx); 1585 final float y = MotionEventCompat.getY(ev, pointerIndex); 1586 final float yDiff = Math.abs(y - mLastMotionY); 1587 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1588 1589 if (canScroll(this, false, (int) dx, (int) x, (int) y)) { 1590 // Nested view has scrollable area under this point. Let it be handled there. 1591 mInitialMotionX = mLastMotionX = x; 1592 mLastMotionY = y; 1593 mIsUnableToDrag = true; 1594 return false; 1595 } 1596 if (xDiff > mTouchSlop && xDiff > yDiff) { 1597 if (DEBUG) Log.v(TAG, "Starting drag!"); 1598 mIsBeingDragged = true; 1599 setScrollState(SCROLL_STATE_DRAGGING); 1600 mLastMotionX = x; 1601 setScrollingCacheEnabled(true); 1602 } else { 1603 if (yDiff > mTouchSlop) { 1604 // The finger has moved enough in the vertical 1605 // direction to be counted as a drag... abort 1606 // any attempt to drag horizontally, to work correctly 1607 // with children that have scrolling containers. 1608 if (DEBUG) Log.v(TAG, "Starting unable to drag!"); 1609 mIsUnableToDrag = true; 1610 } 1611 } 1612 break; 1613 } 1614 1615 case MotionEvent.ACTION_DOWN: { 1616 /* 1617 * Remember location of down touch. 1618 * ACTION_DOWN always refers to pointer index 0. 1619 */ 1620 mLastMotionX = mInitialMotionX = ev.getX(); 1621 mLastMotionY = ev.getY(); 1622 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1623 mIsUnableToDrag = false; 1624 1625 if (mScrollState == SCROLL_STATE_SETTLING && 1626 Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { 1627 // Let the user 'catch' the pager as it animates. 1628 mIsBeingDragged = true; 1629 setScrollState(SCROLL_STATE_DRAGGING); 1630 } else { 1631 completeScroll(); 1632 mIsBeingDragged = false; 1633 } 1634 1635 if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY 1636 + " mIsBeingDragged=" + mIsBeingDragged 1637 + "mIsUnableToDrag=" + mIsUnableToDrag); 1638 break; 1639 } 1640 1641 case MotionEventCompat.ACTION_POINTER_UP: 1642 onSecondaryPointerUp(ev); 1643 break; 1644 } 1645 1646 if (!mIsBeingDragged) { 1647 // Track the velocity as long as we aren't dragging. 1648 // Once we start a real drag we will track in onTouchEvent. 1649 if (mVelocityTracker == null) { 1650 mVelocityTracker = VelocityTracker.obtain(); 1651 } 1652 mVelocityTracker.addMovement(ev); 1653 } 1654 1655 /* 1656 * The only time we want to intercept motion events is if we are in the 1657 * drag mode. 1658 */ 1659 return mIsBeingDragged; 1660 } 1661 1662 @Override 1663 public boolean onTouchEvent(MotionEvent ev) { 1664 if (mFakeDragging) { 1665 // A fake drag is in progress already, ignore this real one 1666 // but still eat the touch events. 1667 // (It is likely that the user is multi-touching the screen.) 1668 return true; 1669 } 1670 1671 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 1672 // Don't handle edge touches immediately -- they may actually belong to one of our 1673 // descendants. 1674 return false; 1675 } 1676 1677 if (mAdapter == null || mAdapter.getCount() == 0) { 1678 // Nothing to present or scroll; nothing to touch. 1679 return false; 1680 } 1681 1682 if (mVelocityTracker == null) { 1683 mVelocityTracker = VelocityTracker.obtain(); 1684 } 1685 mVelocityTracker.addMovement(ev); 1686 1687 final int action = ev.getAction(); 1688 boolean needsInvalidate = false; 1689 1690 switch (action & MotionEventCompat.ACTION_MASK) { 1691 case MotionEvent.ACTION_DOWN: { 1692 /* 1693 * If being flinged and user touches, stop the fling. isFinished 1694 * will be false if being flinged. 1695 */ 1696 completeScroll(); 1697 1698 // Remember where the motion event started 1699 mLastMotionX = mInitialMotionX = ev.getX(); 1700 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1701 break; 1702 } 1703 case MotionEvent.ACTION_MOVE: 1704 if (!mIsBeingDragged) { 1705 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1706 final float x = MotionEventCompat.getX(ev, pointerIndex); 1707 final float xDiff = Math.abs(x - mLastMotionX); 1708 final float y = MotionEventCompat.getY(ev, pointerIndex); 1709 final float yDiff = Math.abs(y - mLastMotionY); 1710 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1711 if (xDiff > mTouchSlop && xDiff > yDiff) { 1712 if (DEBUG) Log.v(TAG, "Starting drag!"); 1713 mIsBeingDragged = true; 1714 mLastMotionX = x; 1715 setScrollState(SCROLL_STATE_DRAGGING); 1716 setScrollingCacheEnabled(true); 1717 } 1718 } 1719 // Not else! Note that mIsBeingDragged can be set above. 1720 if (mIsBeingDragged) { 1721 // Scroll to follow the motion event 1722 final int activePointerIndex = MotionEventCompat.findPointerIndex( 1723 ev, mActivePointerId); 1724 final float x = MotionEventCompat.getX(ev, activePointerIndex); 1725 final float deltaX = mLastMotionX - x; 1726 mLastMotionX = x; 1727 float oldScrollX = getScrollX(); 1728 float scrollX = oldScrollX + deltaX; 1729 final int width = getWidth(); 1730 1731 float leftBound = width * mFirstOffset; 1732 float rightBound = width * mLastOffset; 1733 boolean leftAbsolute = true; 1734 boolean rightAbsolute = true; 1735 1736 final ItemInfo firstItem = mItems.get(0); 1737 final ItemInfo lastItem = mItems.get(mItems.size() - 1); 1738 if (firstItem.position != 0) { 1739 leftAbsolute = false; 1740 leftBound = firstItem.offset * width; 1741 } 1742 if (lastItem.position != mAdapter.getCount() - 1) { 1743 rightAbsolute = false; 1744 rightBound = lastItem.offset * width; 1745 } 1746 1747 if (scrollX < leftBound) { 1748 if (leftAbsolute) { 1749 float over = leftBound - scrollX; 1750 needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); 1751 } 1752 scrollX = leftBound; 1753 } else if (scrollX > rightBound) { 1754 if (rightAbsolute) { 1755 float over = scrollX - rightBound; 1756 needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); 1757 } 1758 scrollX = rightBound; 1759 } 1760 // Don't lose the rounded component 1761 mLastMotionX += scrollX - (int) scrollX; 1762 scrollTo((int) scrollX, getScrollY()); 1763 pageScrolled((int) scrollX); 1764 } 1765 break; 1766 case MotionEvent.ACTION_UP: 1767 if (mIsBeingDragged) { 1768 final VelocityTracker velocityTracker = mVelocityTracker; 1769 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1770 int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( 1771 velocityTracker, mActivePointerId); 1772 mPopulatePending = true; 1773 final int width = getWidth(); 1774 final int scrollX = getScrollX(); 1775 final ItemInfo ii = infoForCurrentScrollPosition(); 1776 final int currentPage = ii.position; 1777 final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; 1778 final int activePointerIndex = 1779 MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1780 final float x = MotionEventCompat.getX(ev, activePointerIndex); 1781 final int totalDelta = (int) (x - mInitialMotionX); 1782 int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 1783 totalDelta); 1784 setCurrentItemInternal(nextPage, true, true, initialVelocity); 1785 1786 mActivePointerId = INVALID_POINTER; 1787 endDrag(); 1788 needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); 1789 } 1790 break; 1791 case MotionEvent.ACTION_CANCEL: 1792 if (mIsBeingDragged) { 1793 setCurrentItemInternal(mCurItem, true, true); 1794 mActivePointerId = INVALID_POINTER; 1795 endDrag(); 1796 needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); 1797 } 1798 break; 1799 case MotionEventCompat.ACTION_POINTER_DOWN: { 1800 final int index = MotionEventCompat.getActionIndex(ev); 1801 final float x = MotionEventCompat.getX(ev, index); 1802 mLastMotionX = x; 1803 mActivePointerId = MotionEventCompat.getPointerId(ev, index); 1804 break; 1805 } 1806 case MotionEventCompat.ACTION_POINTER_UP: 1807 onSecondaryPointerUp(ev); 1808 mLastMotionX = MotionEventCompat.getX(ev, 1809 MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 1810 break; 1811 } 1812 if (needsInvalidate) { 1813 ViewCompat.postInvalidateOnAnimation(this); 1814 } 1815 return true; 1816 } 1817 1818 /** 1819 * @return Info about the page at the current scroll position. 1820 * This can be synthetic for a missing middle page; the 'object' field can be null. 1821 */ 1822 private ItemInfo infoForCurrentScrollPosition() { 1823 final int width = getWidth(); 1824 final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; 1825 final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; 1826 int lastPos = -1; 1827 float lastOffset = 0.f; 1828 float lastWidth = 0.f; 1829 boolean first = true; 1830 1831 ItemInfo lastItem = null; 1832 for (int i = 0; i < mItems.size(); i++) { 1833 ItemInfo ii = mItems.get(i); 1834 float offset; 1835 if (!first && ii.position != lastPos + 1) { 1836 // Create a synthetic item for a missing page. 1837 ii = mTempItem; 1838 ii.offset = lastOffset + lastWidth + marginOffset; 1839 ii.position = lastPos + 1; 1840 ii.widthFactor = mAdapter.getPageWidth(ii.position); 1841 i--; 1842 } 1843 offset = ii.offset; 1844 1845 final float leftBound = offset - 0.0001f; 1846 final float rightBound = offset + ii.widthFactor + marginOffset + 0.0001f; 1847 if (first || scrollOffset >= leftBound) { 1848 if (scrollOffset < rightBound || i == mItems.size() - 1) { 1849 return ii; 1850 } 1851 } else { 1852 return lastItem; 1853 } 1854 first = false; 1855 lastPos = ii.position; 1856 lastOffset = offset; 1857 lastWidth = ii.widthFactor; 1858 lastItem = ii; 1859 } 1860 1861 return lastItem; 1862 } 1863 1864 private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { 1865 int targetPage; 1866 if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { 1867 targetPage = velocity > 0 ? currentPage : currentPage + 1; 1868 } else { 1869 targetPage = (int) (currentPage + pageOffset + 0.5f); 1870 } 1871 1872 if (mItems.size() > 0) { 1873 final ItemInfo firstItem = mItems.get(0); 1874 final ItemInfo lastItem = mItems.get(mItems.size() - 1); 1875 1876 // Only let the user target pages we have items for 1877 targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); 1878 } 1879 1880 return targetPage; 1881 } 1882 1883 @Override 1884 public void draw(Canvas canvas) { 1885 super.draw(canvas); 1886 boolean needsInvalidate = false; 1887 1888 final int overScrollMode = ViewCompat.getOverScrollMode(this); 1889 if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 1890 (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && 1891 mAdapter != null && mAdapter.getCount() > 1)) { 1892 if (!mLeftEdge.isFinished()) { 1893 final int restoreCount = canvas.save(); 1894 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 1895 final int width = getWidth(); 1896 1897 canvas.rotate(270); 1898 canvas.translate(-height + getPaddingTop(), mFirstOffset * width); 1899 mLeftEdge.setSize(height, width); 1900 needsInvalidate |= mLeftEdge.draw(canvas); 1901 canvas.restoreToCount(restoreCount); 1902 } 1903 if (!mRightEdge.isFinished()) { 1904 final int restoreCount = canvas.save(); 1905 final int width = getWidth(); 1906 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 1907 1908 canvas.rotate(90); 1909 canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); 1910 mRightEdge.setSize(height, width); 1911 needsInvalidate |= mRightEdge.draw(canvas); 1912 canvas.restoreToCount(restoreCount); 1913 } 1914 } else { 1915 mLeftEdge.finish(); 1916 mRightEdge.finish(); 1917 } 1918 1919 if (needsInvalidate) { 1920 // Keep animating 1921 ViewCompat.postInvalidateOnAnimation(this); 1922 } 1923 } 1924 1925 @Override 1926 protected void onDraw(Canvas canvas) { 1927 super.onDraw(canvas); 1928 1929 // Draw the margin drawable between pages if needed. 1930 if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { 1931 final int scrollX = getScrollX(); 1932 final int width = getWidth(); 1933 1934 final float marginOffset = (float) mPageMargin / width; 1935 int itemIndex = 0; 1936 ItemInfo ii = mItems.get(0); 1937 float offset = ii.offset; 1938 final int itemCount = mItems.size(); 1939 final int firstPos = ii.position; 1940 final int lastPos = mItems.get(itemCount - 1).position; 1941 for (int pos = firstPos; pos < lastPos; pos++) { 1942 while (pos > ii.position && itemIndex < itemCount) { 1943 ii = mItems.get(++itemIndex); 1944 } 1945 1946 float drawAt; 1947 if (pos == ii.position) { 1948 drawAt = (ii.offset + ii.widthFactor) * width; 1949 offset = ii.offset + ii.widthFactor + marginOffset; 1950 } else { 1951 float widthFactor = mAdapter.getPageWidth(pos); 1952 drawAt = (offset + widthFactor) * width; 1953 offset += widthFactor + marginOffset; 1954 } 1955 1956 if (drawAt + mPageMargin > scrollX) { 1957 mMarginDrawable.setBounds((int) (drawAt - 0.5f), mTopPageBounds, 1958 (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds); 1959 mMarginDrawable.draw(canvas); 1960 } 1961 1962 if (drawAt > scrollX + width) { 1963 break; // No more visible, no sense in continuing 1964 } 1965 } 1966 } 1967 } 1968 1969 /** 1970 * Start a fake drag of the pager. 1971 * 1972 * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager 1973 * with the touch scrolling of another view, while still letting the ViewPager 1974 * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) 1975 * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call 1976 * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. 1977 * 1978 * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag 1979 * is already in progress, this method will return false. 1980 * 1981 * @return true if the fake drag began successfully, false if it could not be started. 1982 * 1983 * @see #fakeDragBy(float) 1984 * @see #endFakeDrag() 1985 */ 1986 public boolean beginFakeDrag() { 1987 if (mIsBeingDragged) { 1988 return false; 1989 } 1990 mFakeDragging = true; 1991 setScrollState(SCROLL_STATE_DRAGGING); 1992 mInitialMotionX = mLastMotionX = 0; 1993 if (mVelocityTracker == null) { 1994 mVelocityTracker = VelocityTracker.obtain(); 1995 } else { 1996 mVelocityTracker.clear(); 1997 } 1998 final long time = SystemClock.uptimeMillis(); 1999 final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); 2000 mVelocityTracker.addMovement(ev); 2001 ev.recycle(); 2002 mFakeDragBeginTime = time; 2003 return true; 2004 } 2005 2006 /** 2007 * End a fake drag of the pager. 2008 * 2009 * @see #beginFakeDrag() 2010 * @see #fakeDragBy(float) 2011 */ 2012 public void endFakeDrag() { 2013 if (!mFakeDragging) { 2014 throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2015 } 2016 2017 final VelocityTracker velocityTracker = mVelocityTracker; 2018 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 2019 int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( 2020 velocityTracker, mActivePointerId); 2021 mPopulatePending = true; 2022 final int width = getWidth(); 2023 final int scrollX = getScrollX(); 2024 final ItemInfo ii = infoForCurrentScrollPosition(); 2025 final int currentPage = ii.position; 2026 final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; 2027 final int totalDelta = (int) (mLastMotionX - mInitialMotionX); 2028 int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 2029 totalDelta); 2030 setCurrentItemInternal(nextPage, true, true, initialVelocity); 2031 endDrag(); 2032 2033 mFakeDragging = false; 2034 } 2035 2036 /** 2037 * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. 2038 * 2039 * @param xOffset Offset in pixels to drag by. 2040 * @see #beginFakeDrag() 2041 * @see #endFakeDrag() 2042 */ 2043 public void fakeDragBy(float xOffset) { 2044 if (!mFakeDragging) { 2045 throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2046 } 2047 2048 mLastMotionX += xOffset; 2049 2050 float oldScrollX = getScrollX(); 2051 float scrollX = oldScrollX - xOffset; 2052 final int width = getWidth(); 2053 2054 float leftBound = width * mFirstOffset; 2055 float rightBound = width * mLastOffset; 2056 2057 final ItemInfo firstItem = mItems.get(0); 2058 final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2059 if (firstItem.position != 0) { 2060 leftBound = firstItem.offset * width; 2061 } 2062 if (lastItem.position != mAdapter.getCount() - 1) { 2063 rightBound = lastItem.offset * width; 2064 } 2065 2066 if (scrollX < leftBound) { 2067 scrollX = leftBound; 2068 } else if (scrollX > rightBound) { 2069 scrollX = rightBound; 2070 } 2071 // Don't lose the rounded component 2072 mLastMotionX += scrollX - (int) scrollX; 2073 scrollTo((int) scrollX, getScrollY()); 2074 pageScrolled((int) scrollX); 2075 2076 // Synthesize an event for the VelocityTracker. 2077 final long time = SystemClock.uptimeMillis(); 2078 final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, 2079 mLastMotionX, 0, 0); 2080 mVelocityTracker.addMovement(ev); 2081 ev.recycle(); 2082 } 2083 2084 /** 2085 * Returns true if a fake drag is in progress. 2086 * 2087 * @return true if currently in a fake drag, false otherwise. 2088 * 2089 * @see #beginFakeDrag() 2090 * @see #fakeDragBy(float) 2091 * @see #endFakeDrag() 2092 */ 2093 public boolean isFakeDragging() { 2094 return mFakeDragging; 2095 } 2096 2097 private void onSecondaryPointerUp(MotionEvent ev) { 2098 final int pointerIndex = MotionEventCompat.getActionIndex(ev); 2099 final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 2100 if (pointerId == mActivePointerId) { 2101 // This was our active pointer going up. Choose a new 2102 // active pointer and adjust accordingly. 2103 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 2104 mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); 2105 mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 2106 if (mVelocityTracker != null) { 2107 mVelocityTracker.clear(); 2108 } 2109 } 2110 } 2111 2112 private void endDrag() { 2113 mIsBeingDragged = false; 2114 mIsUnableToDrag = false; 2115 2116 if (mVelocityTracker != null) { 2117 mVelocityTracker.recycle(); 2118 mVelocityTracker = null; 2119 } 2120 } 2121 2122 private void setScrollingCacheEnabled(boolean enabled) { 2123 if (mScrollingCacheEnabled != enabled) { 2124 mScrollingCacheEnabled = enabled; 2125 if (USE_CACHE) { 2126 final int size = getChildCount(); 2127 for (int i = 0; i < size; ++i) { 2128 final View child = getChildAt(i); 2129 if (child.getVisibility() != GONE) { 2130 child.setDrawingCacheEnabled(enabled); 2131 } 2132 } 2133 } 2134 } 2135 } 2136 2137 /** 2138 * Tests scrollability within child views of v given a delta of dx. 2139 * 2140 * @param v View to test for horizontal scrollability 2141 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 2142 * or just its children (false). 2143 * @param dx Delta scrolled in pixels 2144 * @param x X coordinate of the active touch point 2145 * @param y Y coordinate of the active touch point 2146 * @return true if child views of v can be scrolled by delta of dx. 2147 */ 2148 protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { 2149 if (v instanceof ViewGroup) { 2150 final ViewGroup group = (ViewGroup) v; 2151 final int scrollX = v.getScrollX(); 2152 final int scrollY = v.getScrollY(); 2153 final int count = group.getChildCount(); 2154 // Count backwards - let topmost views consume scroll distance first. 2155 for (int i = count - 1; i >= 0; i--) { 2156 // TODO: Add versioned support here for transformed views. 2157 // This will not work for transformed views in Honeycomb+ 2158 final View child = group.getChildAt(i); 2159 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 2160 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 2161 canScroll(child, true, dx, x + scrollX - child.getLeft(), 2162 y + scrollY - child.getTop())) { 2163 return true; 2164 } 2165 } 2166 } 2167 2168 return checkV && ViewCompat.canScrollHorizontally(v, -dx); 2169 } 2170 2171 @Override 2172 public boolean dispatchKeyEvent(KeyEvent event) { 2173 // Let the focused view and/or our descendants get the key first 2174 return super.dispatchKeyEvent(event) || executeKeyEvent(event); 2175 } 2176 2177 /** 2178 * You can call this function yourself to have the scroll view perform 2179 * scrolling from a key event, just as if the event had been dispatched to 2180 * it by the view hierarchy. 2181 * 2182 * @param event The key event to execute. 2183 * @return Return true if the event was handled, else false. 2184 */ 2185 public boolean executeKeyEvent(KeyEvent event) { 2186 boolean handled = false; 2187 if (event.getAction() == KeyEvent.ACTION_DOWN) { 2188 switch (event.getKeyCode()) { 2189 case KeyEvent.KEYCODE_DPAD_LEFT: 2190 handled = arrowScroll(FOCUS_LEFT); 2191 break; 2192 case KeyEvent.KEYCODE_DPAD_RIGHT: 2193 handled = arrowScroll(FOCUS_RIGHT); 2194 break; 2195 case KeyEvent.KEYCODE_TAB: 2196 if (Build.VERSION.SDK_INT >= 11) { 2197 // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD 2198 // before Android 3.0. Ignore the tab key on those devices. 2199 if (KeyEventCompat.hasNoModifiers(event)) { 2200 handled = arrowScroll(FOCUS_FORWARD); 2201 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) { 2202 handled = arrowScroll(FOCUS_BACKWARD); 2203 } 2204 } 2205 break; 2206 } 2207 } 2208 return handled; 2209 } 2210 2211 public boolean arrowScroll(int direction) { 2212 View currentFocused = findFocus(); 2213 if (currentFocused == this) currentFocused = null; 2214 2215 boolean handled = false; 2216 2217 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, 2218 direction); 2219 if (nextFocused != null && nextFocused != currentFocused) { 2220 if (direction == View.FOCUS_LEFT) { 2221 // If there is nothing to the left, or this is causing us to 2222 // jump to the right, then what we really want to do is page left. 2223 if (currentFocused != null && nextFocused.getLeft() >= currentFocused.getLeft()) { 2224 handled = pageLeft(); 2225 } else { 2226 handled = nextFocused.requestFocus(); 2227 } 2228 } else if (direction == View.FOCUS_RIGHT) { 2229 // If there is nothing to the right, or this is causing us to 2230 // jump to the left, then what we really want to do is page right. 2231 if (currentFocused != null && nextFocused.getLeft() <= currentFocused.getLeft()) { 2232 handled = pageRight(); 2233 } else { 2234 handled = nextFocused.requestFocus(); 2235 } 2236 } 2237 } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { 2238 // Trying to move left and nothing there; try to page. 2239 handled = pageLeft(); 2240 } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { 2241 // Trying to move right and nothing there; try to page. 2242 handled = pageRight(); 2243 } 2244 if (handled) { 2245 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 2246 } 2247 return handled; 2248 } 2249 2250 boolean pageLeft() { 2251 if (mCurItem > 0) { 2252 setCurrentItem(mCurItem-1, true); 2253 return true; 2254 } 2255 return false; 2256 } 2257 2258 boolean pageRight() { 2259 if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) { 2260 setCurrentItem(mCurItem+1, true); 2261 return true; 2262 } 2263 return false; 2264 } 2265 2266 /** 2267 * We only want the current page that is being shown to be focusable. 2268 */ 2269 @Override 2270 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { 2271 final int focusableCount = views.size(); 2272 2273 final int descendantFocusability = getDescendantFocusability(); 2274 2275 if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { 2276 for (int i = 0; i < getChildCount(); i++) { 2277 final View child = getChildAt(i); 2278 if (child.getVisibility() == VISIBLE) { 2279 ItemInfo ii = infoForChild(child); 2280 if (ii != null && ii.position == mCurItem) { 2281 child.addFocusables(views, direction, focusableMode); 2282 } 2283 } 2284 } 2285 } 2286 2287 // we add ourselves (if focusable) in all cases except for when we are 2288 // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is 2289 // to avoid the focus search finding layouts when a more precise search 2290 // among the focusable children would be more interesting. 2291 if ( 2292 descendantFocusability != FOCUS_AFTER_DESCENDANTS || 2293 // No focusable descendants 2294 (focusableCount == views.size())) { 2295 // Note that we can't call the superclass here, because it will 2296 // add all views in. So we need to do the same thing View does. 2297 if (!isFocusable()) { 2298 return; 2299 } 2300 if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && 2301 isInTouchMode() && !isFocusableInTouchMode()) { 2302 return; 2303 } 2304 if (views != null) { 2305 views.add(this); 2306 } 2307 } 2308 } 2309 2310 /** 2311 * We only want the current page that is being shown to be touchable. 2312 */ 2313 @Override 2314 public void addTouchables(ArrayList<View> views) { 2315 // Note that we don't call super.addTouchables(), which means that 2316 // we don't call View.addTouchables(). This is okay because a ViewPager 2317 // is itself not touchable. 2318 for (int i = 0; i < getChildCount(); i++) { 2319 final View child = getChildAt(i); 2320 if (child.getVisibility() == VISIBLE) { 2321 ItemInfo ii = infoForChild(child); 2322 if (ii != null && ii.position == mCurItem) { 2323 child.addTouchables(views); 2324 } 2325 } 2326 } 2327 } 2328 2329 /** 2330 * We only want the current page that is being shown to be focusable. 2331 */ 2332 @Override 2333 protected boolean onRequestFocusInDescendants(int direction, 2334 Rect previouslyFocusedRect) { 2335 int index; 2336 int increment; 2337 int end; 2338 int count = getChildCount(); 2339 if ((direction & FOCUS_FORWARD) != 0) { 2340 index = 0; 2341 increment = 1; 2342 end = count; 2343 } else { 2344 index = count - 1; 2345 increment = -1; 2346 end = -1; 2347 } 2348 for (int i = index; i != end; i += increment) { 2349 View child = getChildAt(i); 2350 if (child.getVisibility() == VISIBLE) { 2351 ItemInfo ii = infoForChild(child); 2352 if (ii != null && ii.position == mCurItem) { 2353 if (child.requestFocus(direction, previouslyFocusedRect)) { 2354 return true; 2355 } 2356 } 2357 } 2358 } 2359 return false; 2360 } 2361 2362 @Override 2363 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 2364 // ViewPagers should only report accessibility info for the current page, 2365 // otherwise things get very confusing. 2366 2367 // TODO: Should this note something about the paging container? 2368 2369 final int childCount = getChildCount(); 2370 for (int i = 0; i < childCount; i++) { 2371 final View child = getChildAt(i); 2372 if (child.getVisibility() == VISIBLE) { 2373 final ItemInfo ii = infoForChild(child); 2374 if (ii != null && ii.position == mCurItem && 2375 child.dispatchPopulateAccessibilityEvent(event)) { 2376 return true; 2377 } 2378 } 2379 } 2380 2381 return false; 2382 } 2383 2384 @Override 2385 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 2386 return new LayoutParams(); 2387 } 2388 2389 @Override 2390 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 2391 return generateDefaultLayoutParams(); 2392 } 2393 2394 @Override 2395 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 2396 return p instanceof LayoutParams && super.checkLayoutParams(p); 2397 } 2398 2399 @Override 2400 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 2401 return new LayoutParams(getContext(), attrs); 2402 } 2403 2404 private class PagerObserver extends DataSetObserver { 2405 @Override 2406 public void onChanged() { 2407 dataSetChanged(); 2408 } 2409 @Override 2410 public void onInvalidated() { 2411 dataSetChanged(); 2412 } 2413 } 2414 2415 /** 2416 * Layout parameters that should be supplied for views added to a 2417 * ViewPager. 2418 */ 2419 public static class LayoutParams extends ViewGroup.LayoutParams { 2420 /** 2421 * true if this view is a decoration on the pager itself and not 2422 * a view supplied by the adapter. 2423 */ 2424 public boolean isDecor; 2425 2426 /** 2427 * Gravity setting for use on decor views only: 2428 * Where to position the view page within the overall ViewPager 2429 * container; constants are defined in {@link android.view.Gravity}. 2430 */ 2431 public int gravity; 2432 2433 /** 2434 * Width as a 0-1 multiplier of the measured pager width 2435 */ 2436 public float widthFactor = 0.f; 2437 2438 /** 2439 * true if this view was added during layout and needs to be measured 2440 * before being positioned. 2441 */ 2442 public boolean needsMeasure; 2443 2444 public LayoutParams() { 2445 super(FILL_PARENT, FILL_PARENT); 2446 } 2447 2448 public LayoutParams(Context context, AttributeSet attrs) { 2449 super(context, attrs); 2450 2451 final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 2452 gravity = a.getInteger(0, Gravity.TOP); 2453 a.recycle(); 2454 } 2455 } 2456} 2457