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