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