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