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