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