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