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