ViewPager.java revision 5f6568e7e269783e2668527461878cadfbe65215
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 final int widthWithMargin = width + margin; 1251 if (oldWidth > 0 && !mItems.isEmpty()) { 1252 final int xpos = getScrollX(); 1253 final int oldWidthWithMargin = oldWidth + oldMargin; 1254 final float pageOffset = (float) xpos / oldWidthWithMargin; 1255 final int newOffsetPixels = (int) (pageOffset * widthWithMargin); 1256 1257 scrollTo(newOffsetPixels, getScrollY()); 1258 if (!mScroller.isFinished()) { 1259 // We now return to your regularly scheduled scroll, already in progress. 1260 final int newDuration = mScroller.getDuration() - mScroller.timePassed(); 1261 ItemInfo targetInfo = infoForPosition(mCurItem); 1262 mScroller.startScroll(newOffsetPixels, 0, 1263 (int) (targetInfo.offset * widthWithMargin), 0, newDuration); 1264 } 1265 } else { 1266 final ItemInfo ii = infoForPosition(mCurItem); 1267 final int scrollPos = 1268 (int) ((ii != null ? Math.min(ii.offset, mLastOffset) : 0) * widthWithMargin); 1269 if (scrollPos != getScrollX()) { 1270 completeScroll(); 1271 scrollTo(scrollPos, getScrollY()); 1272 } 1273 } 1274 } 1275 1276 @Override 1277 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1278 mInLayout = true; 1279 populate(); 1280 mInLayout = false; 1281 1282 final int count = getChildCount(); 1283 int width = r - l; 1284 int height = b - t; 1285 int paddingLeft = getPaddingLeft(); 1286 int paddingTop = getPaddingTop(); 1287 int paddingRight = getPaddingRight(); 1288 int paddingBottom = getPaddingBottom(); 1289 final int scrollX = getScrollX(); 1290 1291 int decorCount = 0; 1292 1293 // First pass - decor views. We need to do this in two passes so that 1294 // we have the proper offsets for non-decor views later. 1295 for (int i = 0; i < count; i++) { 1296 final View child = getChildAt(i); 1297 if (child.getVisibility() != GONE) { 1298 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1299 int childLeft = 0; 1300 int childTop = 0; 1301 if (lp.isDecor) { 1302 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1303 final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1304 switch (hgrav) { 1305 default: 1306 childLeft = paddingLeft; 1307 break; 1308 case Gravity.LEFT: 1309 childLeft = paddingLeft; 1310 paddingLeft += child.getMeasuredWidth(); 1311 break; 1312 case Gravity.CENTER_HORIZONTAL: 1313 childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1314 paddingLeft); 1315 break; 1316 case Gravity.RIGHT: 1317 childLeft = width - paddingRight - child.getMeasuredWidth(); 1318 paddingRight += child.getMeasuredWidth(); 1319 break; 1320 } 1321 switch (vgrav) { 1322 default: 1323 childTop = paddingTop; 1324 break; 1325 case Gravity.TOP: 1326 childTop = paddingTop; 1327 paddingTop += child.getMeasuredHeight(); 1328 break; 1329 case Gravity.CENTER_VERTICAL: 1330 childTop = Math.max((height - child.getMeasuredHeight()) / 2, 1331 paddingTop); 1332 break; 1333 case Gravity.BOTTOM: 1334 childTop = height - paddingBottom - child.getMeasuredHeight(); 1335 paddingBottom += child.getMeasuredHeight(); 1336 break; 1337 } 1338 childLeft += scrollX; 1339 child.layout(childLeft, childTop, 1340 childLeft + child.getMeasuredWidth(), 1341 childTop + child.getMeasuredHeight()); 1342 decorCount++; 1343 } 1344 } 1345 } 1346 1347 // Page views. Do this once we have the right padding offsets from above. 1348 for (int i = 0; i < count; i++) { 1349 final View child = getChildAt(i); 1350 if (child.getVisibility() != GONE) { 1351 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1352 ItemInfo ii; 1353 if (!lp.isDecor && (ii = infoForChild(child)) != null) { 1354 int loff = (int) (width * ii.offset); 1355 int childLeft = paddingLeft + loff; 1356 int childTop = paddingTop; 1357 if (lp.needsMeasure) { 1358 // This was added during layout and needs measurement. 1359 // Do it now that we know what we're working with. 1360 lp.needsMeasure = false; 1361 final int widthSpec = MeasureSpec.makeMeasureSpec( 1362 (int) ((width - paddingLeft - paddingRight) * lp.widthFactor), 1363 MeasureSpec.EXACTLY); 1364 final int heightSpec = MeasureSpec.makeMeasureSpec( 1365 (int) (height - paddingTop - paddingBottom), 1366 MeasureSpec.EXACTLY); 1367 child.measure(widthSpec, heightSpec); 1368 } 1369 if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object 1370 + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() 1371 + "x" + child.getMeasuredHeight()); 1372 child.layout(childLeft, childTop, 1373 childLeft + child.getMeasuredWidth(), 1374 childTop + child.getMeasuredHeight()); 1375 } 1376 } 1377 } 1378 mTopPageBounds = paddingTop; 1379 mBottomPageBounds = height - paddingBottom; 1380 mDecorChildCount = decorCount; 1381 mFirstLayout = false; 1382 } 1383 1384 @Override 1385 public void computeScroll() { 1386 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { 1387 int oldX = getScrollX(); 1388 int oldY = getScrollY(); 1389 int x = mScroller.getCurrX(); 1390 int y = mScroller.getCurrY(); 1391 1392 if (oldX != x || oldY != y) { 1393 scrollTo(x, y); 1394 if (!pageScrolled(x)) { 1395 mScroller.abortAnimation(); 1396 scrollTo(0, y); 1397 } 1398 } 1399 1400 // Keep on drawing until the animation has finished. 1401 ViewCompat.postInvalidateOnAnimation(this); 1402 return; 1403 } 1404 1405 // Done with scroll, clean up state. 1406 completeScroll(); 1407 } 1408 1409 private boolean pageScrolled(int xpos) { 1410 if (mItems.size() == 0) { 1411 mCalledSuper = false; 1412 onPageScrolled(0, 0, 0); 1413 if (!mCalledSuper) { 1414 throw new IllegalStateException( 1415 "onPageScrolled did not call superclass implementation"); 1416 } 1417 return false; 1418 } 1419 final ItemInfo ii = infoForCurrentScrollPosition(); 1420 final int width = getWidth(); 1421 final int widthWithMargin = width + mPageMargin; 1422 final float marginOffset = (float) mPageMargin / width; 1423 final int currentPage = ii.position; 1424 final float pageOffset = (((float) xpos / width) - ii.offset) / 1425 (ii.widthFactor + marginOffset); 1426 final int offsetPixels = (int) (pageOffset * widthWithMargin); 1427 1428 mCalledSuper = false; 1429 onPageScrolled(currentPage, pageOffset, offsetPixels); 1430 if (!mCalledSuper) { 1431 throw new IllegalStateException( 1432 "onPageScrolled did not call superclass implementation"); 1433 } 1434 return true; 1435 } 1436 1437 /** 1438 * This method will be invoked when the current page is scrolled, either as part 1439 * of a programmatically initiated smooth scroll or a user initiated touch scroll. 1440 * If you override this method you must call through to the superclass implementation 1441 * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled 1442 * returns. 1443 * 1444 * @param position Position index of the first page currently being displayed. 1445 * Page position+1 will be visible if positionOffset is nonzero. 1446 * @param offset Value from [0, 1) indicating the offset from the page at position. 1447 * @param offsetPixels Value in pixels indicating the offset from position. 1448 */ 1449 protected void onPageScrolled(int position, float offset, int offsetPixels) { 1450 // Offset any decor views if needed - keep them on-screen at all times. 1451 if (mDecorChildCount > 0) { 1452 final int scrollX = getScrollX(); 1453 int paddingLeft = getPaddingLeft(); 1454 int paddingRight = getPaddingRight(); 1455 final int width = getWidth(); 1456 final int childCount = getChildCount(); 1457 for (int i = 0; i < childCount; i++) { 1458 final View child = getChildAt(i); 1459 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1460 if (!lp.isDecor) continue; 1461 1462 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1463 int childLeft = 0; 1464 switch (hgrav) { 1465 default: 1466 childLeft = paddingLeft; 1467 break; 1468 case Gravity.LEFT: 1469 childLeft = paddingLeft; 1470 paddingLeft += child.getWidth(); 1471 break; 1472 case Gravity.CENTER_HORIZONTAL: 1473 childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1474 paddingLeft); 1475 break; 1476 case Gravity.RIGHT: 1477 childLeft = width - paddingRight - child.getMeasuredWidth(); 1478 paddingRight += child.getMeasuredWidth(); 1479 break; 1480 } 1481 childLeft += scrollX; 1482 1483 final int childOffset = childLeft - child.getLeft(); 1484 if (childOffset != 0) { 1485 child.offsetLeftAndRight(childOffset); 1486 } 1487 } 1488 } 1489 1490 if (mOnPageChangeListener != null) { 1491 mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1492 } 1493 if (mInternalPageChangeListener != null) { 1494 mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1495 } 1496 mCalledSuper = true; 1497 } 1498 1499 private void completeScroll() { 1500 boolean needPopulate = mScrolling; 1501 if (needPopulate) { 1502 // Done with scroll, no longer want to cache view drawing. 1503 setScrollingCacheEnabled(false); 1504 mScroller.abortAnimation(); 1505 int oldX = getScrollX(); 1506 int oldY = getScrollY(); 1507 int x = mScroller.getCurrX(); 1508 int y = mScroller.getCurrY(); 1509 if (oldX != x || oldY != y) { 1510 scrollTo(x, y); 1511 } 1512 setScrollState(SCROLL_STATE_IDLE); 1513 } 1514 mPopulatePending = false; 1515 mScrolling = false; 1516 for (int i=0; i<mItems.size(); i++) { 1517 ItemInfo ii = mItems.get(i); 1518 if (ii.scrolling) { 1519 needPopulate = true; 1520 ii.scrolling = false; 1521 } 1522 } 1523 if (needPopulate) { 1524 populate(); 1525 } 1526 } 1527 1528 @Override 1529 public boolean onInterceptTouchEvent(MotionEvent ev) { 1530 /* 1531 * This method JUST determines whether we want to intercept the motion. 1532 * If we return true, onMotionEvent will be called and we do the actual 1533 * scrolling there. 1534 */ 1535 1536 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 1537 1538 // Always take care of the touch gesture being complete. 1539 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 1540 // Release the drag. 1541 if (DEBUG) Log.v(TAG, "Intercept done!"); 1542 mIsBeingDragged = false; 1543 mIsUnableToDrag = false; 1544 mActivePointerId = INVALID_POINTER; 1545 if (mVelocityTracker != null) { 1546 mVelocityTracker.recycle(); 1547 mVelocityTracker = null; 1548 } 1549 return false; 1550 } 1551 1552 // Nothing more to do here if we have decided whether or not we 1553 // are dragging. 1554 if (action != MotionEvent.ACTION_DOWN) { 1555 if (mIsBeingDragged) { 1556 if (DEBUG) Log.v(TAG, "Intercept returning true!"); 1557 return true; 1558 } 1559 if (mIsUnableToDrag) { 1560 if (DEBUG) Log.v(TAG, "Intercept returning false!"); 1561 return false; 1562 } 1563 } 1564 1565 switch (action) { 1566 case MotionEvent.ACTION_MOVE: { 1567 /* 1568 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1569 * whether the user has moved far enough from his original down touch. 1570 */ 1571 1572 /* 1573 * Locally do absolute value. mLastMotionY is set to the y value 1574 * of the down event. 1575 */ 1576 final int activePointerId = mActivePointerId; 1577 if (activePointerId == INVALID_POINTER) { 1578 // If we don't have a valid id, the touch down wasn't on content. 1579 break; 1580 } 1581 1582 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 1583 final float x = MotionEventCompat.getX(ev, pointerIndex); 1584 final float dx = x - mLastMotionX; 1585 final float xDiff = Math.abs(dx); 1586 final float y = MotionEventCompat.getY(ev, pointerIndex); 1587 final float yDiff = Math.abs(y - mLastMotionY); 1588 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1589 1590 if (canScroll(this, false, (int) dx, (int) x, (int) y)) { 1591 // Nested view has scrollable area under this point. Let it be handled there. 1592 mInitialMotionX = mLastMotionX = x; 1593 mLastMotionY = y; 1594 mIsUnableToDrag = true; 1595 return false; 1596 } 1597 if (xDiff > mTouchSlop && xDiff > yDiff) { 1598 if (DEBUG) Log.v(TAG, "Starting drag!"); 1599 mIsBeingDragged = true; 1600 setScrollState(SCROLL_STATE_DRAGGING); 1601 mLastMotionX = x; 1602 setScrollingCacheEnabled(true); 1603 } else { 1604 if (yDiff > mTouchSlop) { 1605 // The finger has moved enough in the vertical 1606 // direction to be counted as a drag... abort 1607 // any attempt to drag horizontally, to work correctly 1608 // with children that have scrolling containers. 1609 if (DEBUG) Log.v(TAG, "Starting unable to drag!"); 1610 mIsUnableToDrag = true; 1611 } 1612 } 1613 break; 1614 } 1615 1616 case MotionEvent.ACTION_DOWN: { 1617 /* 1618 * Remember location of down touch. 1619 * ACTION_DOWN always refers to pointer index 0. 1620 */ 1621 mLastMotionX = mInitialMotionX = ev.getX(); 1622 mLastMotionY = ev.getY(); 1623 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1624 mIsUnableToDrag = false; 1625 1626 if (mScrollState == SCROLL_STATE_SETTLING && 1627 Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { 1628 // Let the user 'catch' the pager as it animates. 1629 mIsBeingDragged = true; 1630 setScrollState(SCROLL_STATE_DRAGGING); 1631 } else { 1632 completeScroll(); 1633 mIsBeingDragged = false; 1634 } 1635 1636 if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY 1637 + " mIsBeingDragged=" + mIsBeingDragged 1638 + "mIsUnableToDrag=" + mIsUnableToDrag); 1639 break; 1640 } 1641 1642 case MotionEventCompat.ACTION_POINTER_UP: 1643 onSecondaryPointerUp(ev); 1644 break; 1645 } 1646 1647 if (!mIsBeingDragged) { 1648 // Track the velocity as long as we aren't dragging. 1649 // Once we start a real drag we will track in onTouchEvent. 1650 if (mVelocityTracker == null) { 1651 mVelocityTracker = VelocityTracker.obtain(); 1652 } 1653 mVelocityTracker.addMovement(ev); 1654 } 1655 1656 /* 1657 * The only time we want to intercept motion events is if we are in the 1658 * drag mode. 1659 */ 1660 return mIsBeingDragged; 1661 } 1662 1663 @Override 1664 public boolean onTouchEvent(MotionEvent ev) { 1665 if (mFakeDragging) { 1666 // A fake drag is in progress already, ignore this real one 1667 // but still eat the touch events. 1668 // (It is likely that the user is multi-touching the screen.) 1669 return true; 1670 } 1671 1672 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 1673 // Don't handle edge touches immediately -- they may actually belong to one of our 1674 // descendants. 1675 return false; 1676 } 1677 1678 if (mAdapter == null || mAdapter.getCount() == 0) { 1679 // Nothing to present or scroll; nothing to touch. 1680 return false; 1681 } 1682 1683 if (mVelocityTracker == null) { 1684 mVelocityTracker = VelocityTracker.obtain(); 1685 } 1686 mVelocityTracker.addMovement(ev); 1687 1688 final int action = ev.getAction(); 1689 boolean needsInvalidate = false; 1690 1691 switch (action & MotionEventCompat.ACTION_MASK) { 1692 case MotionEvent.ACTION_DOWN: { 1693 /* 1694 * If being flinged and user touches, stop the fling. isFinished 1695 * will be false if being flinged. 1696 */ 1697 completeScroll(); 1698 1699 // Remember where the motion event started 1700 mLastMotionX = mInitialMotionX = ev.getX(); 1701 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1702 break; 1703 } 1704 case MotionEvent.ACTION_MOVE: 1705 if (!mIsBeingDragged) { 1706 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1707 final float x = MotionEventCompat.getX(ev, pointerIndex); 1708 final float xDiff = Math.abs(x - mLastMotionX); 1709 final float y = MotionEventCompat.getY(ev, pointerIndex); 1710 final float yDiff = Math.abs(y - mLastMotionY); 1711 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1712 if (xDiff > mTouchSlop && xDiff > yDiff) { 1713 if (DEBUG) Log.v(TAG, "Starting drag!"); 1714 mIsBeingDragged = true; 1715 mLastMotionX = x; 1716 setScrollState(SCROLL_STATE_DRAGGING); 1717 setScrollingCacheEnabled(true); 1718 } 1719 } 1720 // Not else! Note that mIsBeingDragged can be set above. 1721 if (mIsBeingDragged) { 1722 // Scroll to follow the motion event 1723 final int activePointerIndex = MotionEventCompat.findPointerIndex( 1724 ev, mActivePointerId); 1725 final float x = MotionEventCompat.getX(ev, activePointerIndex); 1726 final float deltaX = mLastMotionX - x; 1727 mLastMotionX = x; 1728 float oldScrollX = getScrollX(); 1729 float scrollX = oldScrollX + deltaX; 1730 final int width = getWidth(); 1731 1732 float leftBound = width * mFirstOffset; 1733 float rightBound = width * mLastOffset; 1734 boolean leftAbsolute = true; 1735 boolean rightAbsolute = true; 1736 1737 final ItemInfo firstItem = mItems.get(0); 1738 final ItemInfo lastItem = mItems.get(mItems.size() - 1); 1739 if (firstItem.position != 0) { 1740 leftAbsolute = false; 1741 leftBound = firstItem.offset * width; 1742 } 1743 if (lastItem.position != mAdapter.getCount() - 1) { 1744 rightAbsolute = false; 1745 rightBound = lastItem.offset * width; 1746 } 1747 1748 if (scrollX < leftBound) { 1749 if (leftAbsolute) { 1750 float over = leftBound - scrollX; 1751 needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); 1752 } 1753 scrollX = leftBound; 1754 } else if (scrollX > rightBound) { 1755 if (rightAbsolute) { 1756 float over = scrollX - rightBound; 1757 needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); 1758 } 1759 scrollX = rightBound; 1760 } 1761 // Don't lose the rounded component 1762 mLastMotionX += scrollX - (int) scrollX; 1763 scrollTo((int) scrollX, getScrollY()); 1764 pageScrolled((int) scrollX); 1765 } 1766 break; 1767 case MotionEvent.ACTION_UP: 1768 if (mIsBeingDragged) { 1769 final VelocityTracker velocityTracker = mVelocityTracker; 1770 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1771 int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( 1772 velocityTracker, mActivePointerId); 1773 mPopulatePending = true; 1774 final int width = getWidth(); 1775 final int scrollX = getScrollX(); 1776 final ItemInfo ii = infoForCurrentScrollPosition(); 1777 final int currentPage = ii.position; 1778 final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; 1779 final int activePointerIndex = 1780 MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1781 final float x = MotionEventCompat.getX(ev, activePointerIndex); 1782 final int totalDelta = (int) (x - mInitialMotionX); 1783 int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 1784 totalDelta); 1785 setCurrentItemInternal(nextPage, true, true, initialVelocity); 1786 1787 mActivePointerId = INVALID_POINTER; 1788 endDrag(); 1789 needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); 1790 } 1791 break; 1792 case MotionEvent.ACTION_CANCEL: 1793 if (mIsBeingDragged) { 1794 setCurrentItemInternal(mCurItem, true, true); 1795 mActivePointerId = INVALID_POINTER; 1796 endDrag(); 1797 needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); 1798 } 1799 break; 1800 case MotionEventCompat.ACTION_POINTER_DOWN: { 1801 final int index = MotionEventCompat.getActionIndex(ev); 1802 final float x = MotionEventCompat.getX(ev, index); 1803 mLastMotionX = x; 1804 mActivePointerId = MotionEventCompat.getPointerId(ev, index); 1805 break; 1806 } 1807 case MotionEventCompat.ACTION_POINTER_UP: 1808 onSecondaryPointerUp(ev); 1809 mLastMotionX = MotionEventCompat.getX(ev, 1810 MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 1811 break; 1812 } 1813 if (needsInvalidate) { 1814 ViewCompat.postInvalidateOnAnimation(this); 1815 } 1816 return true; 1817 } 1818 1819 /** 1820 * @return Info about the page at the current scroll position. 1821 * This can be synthetic for a missing middle page; the 'object' field can be null. 1822 */ 1823 private ItemInfo infoForCurrentScrollPosition() { 1824 final int width = getWidth(); 1825 final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; 1826 final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; 1827 int lastPos = -1; 1828 float lastOffset = 0.f; 1829 float lastWidth = 0.f; 1830 boolean first = true; 1831 1832 ItemInfo lastItem = null; 1833 for (int i = 0; i < mItems.size(); i++) { 1834 ItemInfo ii = mItems.get(i); 1835 float offset; 1836 if (!first && ii.position != lastPos + 1) { 1837 // Create a synthetic item for a missing page. 1838 ii = mTempItem; 1839 ii.offset = lastOffset + lastWidth + marginOffset; 1840 ii.position = lastPos + 1; 1841 ii.widthFactor = mAdapter.getPageWidth(ii.position); 1842 i--; 1843 } 1844 offset = ii.offset; 1845 1846 final float leftBound = offset - 0.0001f; 1847 final float rightBound = offset + ii.widthFactor + marginOffset + 0.0001f; 1848 if (first || scrollOffset >= leftBound) { 1849 if (scrollOffset < rightBound || i == mItems.size() - 1) { 1850 return ii; 1851 } 1852 } else { 1853 return lastItem; 1854 } 1855 first = false; 1856 lastPos = ii.position; 1857 lastOffset = offset; 1858 lastWidth = ii.widthFactor; 1859 lastItem = ii; 1860 } 1861 1862 return lastItem; 1863 } 1864 1865 private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { 1866 int targetPage; 1867 if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { 1868 targetPage = velocity > 0 ? currentPage : currentPage + 1; 1869 } else { 1870 targetPage = (int) (currentPage + pageOffset + 0.5f); 1871 } 1872 1873 if (mItems.size() > 0) { 1874 final ItemInfo firstItem = mItems.get(0); 1875 final ItemInfo lastItem = mItems.get(mItems.size() - 1); 1876 1877 // Only let the user target pages we have items for 1878 targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); 1879 } 1880 1881 return targetPage; 1882 } 1883 1884 @Override 1885 public void draw(Canvas canvas) { 1886 super.draw(canvas); 1887 boolean needsInvalidate = false; 1888 1889 final int overScrollMode = ViewCompat.getOverScrollMode(this); 1890 if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 1891 (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && 1892 mAdapter != null && mAdapter.getCount() > 1)) { 1893 if (!mLeftEdge.isFinished()) { 1894 final int restoreCount = canvas.save(); 1895 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 1896 final int width = getWidth(); 1897 1898 canvas.rotate(270); 1899 canvas.translate(-height + getPaddingTop(), mFirstOffset * width); 1900 mLeftEdge.setSize(height, width); 1901 needsInvalidate |= mLeftEdge.draw(canvas); 1902 canvas.restoreToCount(restoreCount); 1903 } 1904 if (!mRightEdge.isFinished()) { 1905 final int restoreCount = canvas.save(); 1906 final int width = getWidth(); 1907 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 1908 1909 canvas.rotate(90); 1910 canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); 1911 mRightEdge.setSize(height, width); 1912 needsInvalidate |= mRightEdge.draw(canvas); 1913 canvas.restoreToCount(restoreCount); 1914 } 1915 } else { 1916 mLeftEdge.finish(); 1917 mRightEdge.finish(); 1918 } 1919 1920 if (needsInvalidate) { 1921 // Keep animating 1922 ViewCompat.postInvalidateOnAnimation(this); 1923 } 1924 } 1925 1926 @Override 1927 protected void onDraw(Canvas canvas) { 1928 super.onDraw(canvas); 1929 1930 // Draw the margin drawable between pages if needed. 1931 if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { 1932 final int scrollX = getScrollX(); 1933 final int width = getWidth(); 1934 1935 final float marginOffset = (float) mPageMargin / width; 1936 int itemIndex = 0; 1937 ItemInfo ii = mItems.get(0); 1938 float offset = ii.offset; 1939 final int itemCount = mItems.size(); 1940 final int firstPos = ii.position; 1941 final int lastPos = mItems.get(itemCount - 1).position; 1942 for (int pos = firstPos; pos < lastPos; pos++) { 1943 while (pos > ii.position && itemIndex < itemCount) { 1944 ii = mItems.get(++itemIndex); 1945 } 1946 1947 float drawAt; 1948 if (pos == ii.position) { 1949 drawAt = (ii.offset + ii.widthFactor) * width; 1950 offset = ii.offset + ii.widthFactor + marginOffset; 1951 } else { 1952 float widthFactor = mAdapter.getPageWidth(pos); 1953 drawAt = (offset + widthFactor) * width; 1954 offset += widthFactor + marginOffset; 1955 } 1956 1957 if (drawAt + mPageMargin > scrollX) { 1958 mMarginDrawable.setBounds((int) (drawAt - 0.5f), mTopPageBounds, 1959 (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds); 1960 mMarginDrawable.draw(canvas); 1961 } 1962 1963 if (drawAt > scrollX + width) { 1964 break; // No more visible, no sense in continuing 1965 } 1966 } 1967 } 1968 } 1969 1970 /** 1971 * Start a fake drag of the pager. 1972 * 1973 * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager 1974 * with the touch scrolling of another view, while still letting the ViewPager 1975 * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) 1976 * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call 1977 * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. 1978 * 1979 * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag 1980 * is already in progress, this method will return false. 1981 * 1982 * @return true if the fake drag began successfully, false if it could not be started. 1983 * 1984 * @see #fakeDragBy(float) 1985 * @see #endFakeDrag() 1986 */ 1987 public boolean beginFakeDrag() { 1988 if (mIsBeingDragged) { 1989 return false; 1990 } 1991 mFakeDragging = true; 1992 setScrollState(SCROLL_STATE_DRAGGING); 1993 mInitialMotionX = mLastMotionX = 0; 1994 if (mVelocityTracker == null) { 1995 mVelocityTracker = VelocityTracker.obtain(); 1996 } else { 1997 mVelocityTracker.clear(); 1998 } 1999 final long time = SystemClock.uptimeMillis(); 2000 final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); 2001 mVelocityTracker.addMovement(ev); 2002 ev.recycle(); 2003 mFakeDragBeginTime = time; 2004 return true; 2005 } 2006 2007 /** 2008 * End a fake drag of the pager. 2009 * 2010 * @see #beginFakeDrag() 2011 * @see #fakeDragBy(float) 2012 */ 2013 public void endFakeDrag() { 2014 if (!mFakeDragging) { 2015 throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2016 } 2017 2018 final VelocityTracker velocityTracker = mVelocityTracker; 2019 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 2020 int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( 2021 velocityTracker, mActivePointerId); 2022 mPopulatePending = true; 2023 final int width = getWidth(); 2024 final int scrollX = getScrollX(); 2025 final ItemInfo ii = infoForCurrentScrollPosition(); 2026 final int currentPage = ii.position; 2027 final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; 2028 final int totalDelta = (int) (mLastMotionX - mInitialMotionX); 2029 int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 2030 totalDelta); 2031 setCurrentItemInternal(nextPage, true, true, initialVelocity); 2032 endDrag(); 2033 2034 mFakeDragging = false; 2035 } 2036 2037 /** 2038 * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. 2039 * 2040 * @param xOffset Offset in pixels to drag by. 2041 * @see #beginFakeDrag() 2042 * @see #endFakeDrag() 2043 */ 2044 public void fakeDragBy(float xOffset) { 2045 if (!mFakeDragging) { 2046 throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2047 } 2048 2049 mLastMotionX += xOffset; 2050 2051 float oldScrollX = getScrollX(); 2052 float scrollX = oldScrollX - xOffset; 2053 final int width = getWidth(); 2054 2055 float leftBound = width * mFirstOffset; 2056 float rightBound = width * mLastOffset; 2057 2058 final ItemInfo firstItem = mItems.get(0); 2059 final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2060 if (firstItem.position != 0) { 2061 leftBound = firstItem.offset * width; 2062 } 2063 if (lastItem.position != mAdapter.getCount() - 1) { 2064 rightBound = lastItem.offset * width; 2065 } 2066 2067 if (scrollX < leftBound) { 2068 scrollX = leftBound; 2069 } else if (scrollX > rightBound) { 2070 scrollX = rightBound; 2071 } 2072 // Don't lose the rounded component 2073 mLastMotionX += scrollX - (int) scrollX; 2074 scrollTo((int) scrollX, getScrollY()); 2075 pageScrolled((int) scrollX); 2076 2077 // Synthesize an event for the VelocityTracker. 2078 final long time = SystemClock.uptimeMillis(); 2079 final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, 2080 mLastMotionX, 0, 0); 2081 mVelocityTracker.addMovement(ev); 2082 ev.recycle(); 2083 } 2084 2085 /** 2086 * Returns true if a fake drag is in progress. 2087 * 2088 * @return true if currently in a fake drag, false otherwise. 2089 * 2090 * @see #beginFakeDrag() 2091 * @see #fakeDragBy(float) 2092 * @see #endFakeDrag() 2093 */ 2094 public boolean isFakeDragging() { 2095 return mFakeDragging; 2096 } 2097 2098 private void onSecondaryPointerUp(MotionEvent ev) { 2099 final int pointerIndex = MotionEventCompat.getActionIndex(ev); 2100 final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 2101 if (pointerId == mActivePointerId) { 2102 // This was our active pointer going up. Choose a new 2103 // active pointer and adjust accordingly. 2104 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 2105 mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); 2106 mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 2107 if (mVelocityTracker != null) { 2108 mVelocityTracker.clear(); 2109 } 2110 } 2111 } 2112 2113 private void endDrag() { 2114 mIsBeingDragged = false; 2115 mIsUnableToDrag = false; 2116 2117 if (mVelocityTracker != null) { 2118 mVelocityTracker.recycle(); 2119 mVelocityTracker = null; 2120 } 2121 } 2122 2123 private void setScrollingCacheEnabled(boolean enabled) { 2124 if (mScrollingCacheEnabled != enabled) { 2125 mScrollingCacheEnabled = enabled; 2126 if (USE_CACHE) { 2127 final int size = getChildCount(); 2128 for (int i = 0; i < size; ++i) { 2129 final View child = getChildAt(i); 2130 if (child.getVisibility() != GONE) { 2131 child.setDrawingCacheEnabled(enabled); 2132 } 2133 } 2134 } 2135 } 2136 } 2137 2138 /** 2139 * Tests scrollability within child views of v given a delta of dx. 2140 * 2141 * @param v View to test for horizontal scrollability 2142 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 2143 * or just its children (false). 2144 * @param dx Delta scrolled in pixels 2145 * @param x X coordinate of the active touch point 2146 * @param y Y coordinate of the active touch point 2147 * @return true if child views of v can be scrolled by delta of dx. 2148 */ 2149 protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { 2150 if (v instanceof ViewGroup) { 2151 final ViewGroup group = (ViewGroup) v; 2152 final int scrollX = v.getScrollX(); 2153 final int scrollY = v.getScrollY(); 2154 final int count = group.getChildCount(); 2155 // Count backwards - let topmost views consume scroll distance first. 2156 for (int i = count - 1; i >= 0; i--) { 2157 // TODO: Add versioned support here for transformed views. 2158 // This will not work for transformed views in Honeycomb+ 2159 final View child = group.getChildAt(i); 2160 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 2161 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 2162 canScroll(child, true, dx, x + scrollX - child.getLeft(), 2163 y + scrollY - child.getTop())) { 2164 return true; 2165 } 2166 } 2167 } 2168 2169 return checkV && ViewCompat.canScrollHorizontally(v, -dx); 2170 } 2171 2172 @Override 2173 public boolean dispatchKeyEvent(KeyEvent event) { 2174 // Let the focused view and/or our descendants get the key first 2175 return super.dispatchKeyEvent(event) || executeKeyEvent(event); 2176 } 2177 2178 /** 2179 * You can call this function yourself to have the scroll view perform 2180 * scrolling from a key event, just as if the event had been dispatched to 2181 * it by the view hierarchy. 2182 * 2183 * @param event The key event to execute. 2184 * @return Return true if the event was handled, else false. 2185 */ 2186 public boolean executeKeyEvent(KeyEvent event) { 2187 boolean handled = false; 2188 if (event.getAction() == KeyEvent.ACTION_DOWN) { 2189 switch (event.getKeyCode()) { 2190 case KeyEvent.KEYCODE_DPAD_LEFT: 2191 handled = arrowScroll(FOCUS_LEFT); 2192 break; 2193 case KeyEvent.KEYCODE_DPAD_RIGHT: 2194 handled = arrowScroll(FOCUS_RIGHT); 2195 break; 2196 case KeyEvent.KEYCODE_TAB: 2197 if (Build.VERSION.SDK_INT >= 11) { 2198 // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD 2199 // before Android 3.0. Ignore the tab key on those devices. 2200 if (KeyEventCompat.hasNoModifiers(event)) { 2201 handled = arrowScroll(FOCUS_FORWARD); 2202 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) { 2203 handled = arrowScroll(FOCUS_BACKWARD); 2204 } 2205 } 2206 break; 2207 } 2208 } 2209 return handled; 2210 } 2211 2212 public boolean arrowScroll(int direction) { 2213 View currentFocused = findFocus(); 2214 if (currentFocused == this) currentFocused = null; 2215 2216 boolean handled = false; 2217 2218 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, 2219 direction); 2220 if (nextFocused != null && nextFocused != currentFocused) { 2221 if (direction == View.FOCUS_LEFT) { 2222 // If there is nothing to the left, or this is causing us to 2223 // jump to the right, then what we really want to do is page left. 2224 if (currentFocused != null && nextFocused.getLeft() >= currentFocused.getLeft()) { 2225 handled = pageLeft(); 2226 } else { 2227 handled = nextFocused.requestFocus(); 2228 } 2229 } else if (direction == View.FOCUS_RIGHT) { 2230 // If there is nothing to the right, or this is causing us to 2231 // jump to the left, then what we really want to do is page right. 2232 if (currentFocused != null && nextFocused.getLeft() <= currentFocused.getLeft()) { 2233 handled = pageRight(); 2234 } else { 2235 handled = nextFocused.requestFocus(); 2236 } 2237 } 2238 } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { 2239 // Trying to move left and nothing there; try to page. 2240 handled = pageLeft(); 2241 } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { 2242 // Trying to move right and nothing there; try to page. 2243 handled = pageRight(); 2244 } 2245 if (handled) { 2246 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 2247 } 2248 return handled; 2249 } 2250 2251 boolean pageLeft() { 2252 if (mCurItem > 0) { 2253 setCurrentItem(mCurItem-1, true); 2254 return true; 2255 } 2256 return false; 2257 } 2258 2259 boolean pageRight() { 2260 if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) { 2261 setCurrentItem(mCurItem+1, true); 2262 return true; 2263 } 2264 return false; 2265 } 2266 2267 /** 2268 * We only want the current page that is being shown to be focusable. 2269 */ 2270 @Override 2271 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { 2272 final int focusableCount = views.size(); 2273 2274 final int descendantFocusability = getDescendantFocusability(); 2275 2276 if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { 2277 for (int i = 0; i < getChildCount(); i++) { 2278 final View child = getChildAt(i); 2279 if (child.getVisibility() == VISIBLE) { 2280 ItemInfo ii = infoForChild(child); 2281 if (ii != null && ii.position == mCurItem) { 2282 child.addFocusables(views, direction, focusableMode); 2283 } 2284 } 2285 } 2286 } 2287 2288 // we add ourselves (if focusable) in all cases except for when we are 2289 // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is 2290 // to avoid the focus search finding layouts when a more precise search 2291 // among the focusable children would be more interesting. 2292 if ( 2293 descendantFocusability != FOCUS_AFTER_DESCENDANTS || 2294 // No focusable descendants 2295 (focusableCount == views.size())) { 2296 // Note that we can't call the superclass here, because it will 2297 // add all views in. So we need to do the same thing View does. 2298 if (!isFocusable()) { 2299 return; 2300 } 2301 if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && 2302 isInTouchMode() && !isFocusableInTouchMode()) { 2303 return; 2304 } 2305 if (views != null) { 2306 views.add(this); 2307 } 2308 } 2309 } 2310 2311 /** 2312 * We only want the current page that is being shown to be touchable. 2313 */ 2314 @Override 2315 public void addTouchables(ArrayList<View> views) { 2316 // Note that we don't call super.addTouchables(), which means that 2317 // we don't call View.addTouchables(). This is okay because a ViewPager 2318 // is itself not touchable. 2319 for (int i = 0; i < getChildCount(); i++) { 2320 final View child = getChildAt(i); 2321 if (child.getVisibility() == VISIBLE) { 2322 ItemInfo ii = infoForChild(child); 2323 if (ii != null && ii.position == mCurItem) { 2324 child.addTouchables(views); 2325 } 2326 } 2327 } 2328 } 2329 2330 /** 2331 * We only want the current page that is being shown to be focusable. 2332 */ 2333 @Override 2334 protected boolean onRequestFocusInDescendants(int direction, 2335 Rect previouslyFocusedRect) { 2336 int index; 2337 int increment; 2338 int end; 2339 int count = getChildCount(); 2340 if ((direction & FOCUS_FORWARD) != 0) { 2341 index = 0; 2342 increment = 1; 2343 end = count; 2344 } else { 2345 index = count - 1; 2346 increment = -1; 2347 end = -1; 2348 } 2349 for (int i = index; i != end; i += increment) { 2350 View child = getChildAt(i); 2351 if (child.getVisibility() == VISIBLE) { 2352 ItemInfo ii = infoForChild(child); 2353 if (ii != null && ii.position == mCurItem) { 2354 if (child.requestFocus(direction, previouslyFocusedRect)) { 2355 return true; 2356 } 2357 } 2358 } 2359 } 2360 return false; 2361 } 2362 2363 @Override 2364 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 2365 // ViewPagers should only report accessibility info for the current page, 2366 // otherwise things get very confusing. 2367 2368 // TODO: Should this note something about the paging container? 2369 2370 final int childCount = getChildCount(); 2371 for (int i = 0; i < childCount; i++) { 2372 final View child = getChildAt(i); 2373 if (child.getVisibility() == VISIBLE) { 2374 final ItemInfo ii = infoForChild(child); 2375 if (ii != null && ii.position == mCurItem && 2376 child.dispatchPopulateAccessibilityEvent(event)) { 2377 return true; 2378 } 2379 } 2380 } 2381 2382 return false; 2383 } 2384 2385 @Override 2386 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 2387 return new LayoutParams(); 2388 } 2389 2390 @Override 2391 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 2392 return generateDefaultLayoutParams(); 2393 } 2394 2395 @Override 2396 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 2397 return p instanceof LayoutParams && super.checkLayoutParams(p); 2398 } 2399 2400 @Override 2401 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 2402 return new LayoutParams(getContext(), attrs); 2403 } 2404 2405 private class PagerObserver extends DataSetObserver { 2406 @Override 2407 public void onChanged() { 2408 dataSetChanged(); 2409 } 2410 @Override 2411 public void onInvalidated() { 2412 dataSetChanged(); 2413 } 2414 } 2415 2416 /** 2417 * Layout parameters that should be supplied for views added to a 2418 * ViewPager. 2419 */ 2420 public static class LayoutParams extends ViewGroup.LayoutParams { 2421 /** 2422 * true if this view is a decoration on the pager itself and not 2423 * a view supplied by the adapter. 2424 */ 2425 public boolean isDecor; 2426 2427 /** 2428 * Gravity setting for use on decor views only: 2429 * Where to position the view page within the overall ViewPager 2430 * container; constants are defined in {@link android.view.Gravity}. 2431 */ 2432 public int gravity; 2433 2434 /** 2435 * Width as a 0-1 multiplier of the measured pager width 2436 */ 2437 public float widthFactor = 0.f; 2438 2439 /** 2440 * true if this view was added during layout and needs to be measured 2441 * before being positioned. 2442 */ 2443 public boolean needsMeasure; 2444 2445 public LayoutParams() { 2446 super(FILL_PARENT, FILL_PARENT); 2447 } 2448 2449 public LayoutParams(Context context, AttributeSet attrs) { 2450 super(context, attrs); 2451 2452 final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 2453 gravity = a.getInteger(0, Gravity.TOP); 2454 a.recycle(); 2455 } 2456 } 2457} 2458