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