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