Note: Prior to Android 3.0 the property animation APIs did not exist. 497 | * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.
498 | * 499 | * @param reverseDrawingOrder true if the supplied PageTransformer requires page views 500 | * to be drawn from last to first instead of first to last. 501 | * @param transformer PageTransformer that will modify each page's animation properties 502 | */ 503 | public void setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer) { 504 | if (Build.VERSION.SDK_INT >= 11) { 505 | final boolean hasTransformer = transformer != null; 506 | final boolean needsPopulate = hasTransformer != (mPageTransformer != null); 507 | mPageTransformer = transformer; 508 | setChildrenDrawingOrderEnabledCompat(hasTransformer); 509 | if (hasTransformer) { 510 | mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; 511 | } else { 512 | mDrawingOrder = DRAW_ORDER_DEFAULT; 513 | } 514 | if (needsPopulate) populate(); 515 | } 516 | } 517 | 518 | void setChildrenDrawingOrderEnabledCompat(boolean enable) { 519 | if (Build.VERSION.SDK_INT >= 7) { 520 | if (mSetChildrenDrawingOrderEnabled == null) { 521 | try { 522 | mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod( 523 | "setChildrenDrawingOrderEnabled", new Class[]{Boolean.TYPE}); 524 | } catch (NoSuchMethodException e) { 525 | Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e); 526 | } 527 | } 528 | try { 529 | mSetChildrenDrawingOrderEnabled.invoke(this, enable); 530 | } catch (Exception e) { 531 | Log.e(TAG, "Error changing children drawing order", e); 532 | } 533 | } 534 | } 535 | 536 | @Override 537 | protected int getChildDrawingOrder(int childCount, int i) { 538 | final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; 539 | final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; 540 | return result; 541 | } 542 | 543 | /** 544 | * Set a separate OnPageChangeListener for internal use by the support library. 545 | * 546 | * @param listener Listener to set 547 | * @return The old listener that was set, if any. 548 | */ 549 | ViewPager.OnPageChangeListener setInternalPageChangeListener(ViewPager.OnPageChangeListener listener) { 550 | ViewPager.OnPageChangeListener oldListener = mInternalPageChangeListener; 551 | mInternalPageChangeListener = listener; 552 | return oldListener; 553 | } 554 | 555 | /** 556 | * Returns the number of pages that will be retained to either side of the 557 | * current page in the view hierarchy in an idle state. Defaults to 1. 558 | * 559 | * @return How many pages will be kept offscreen on either side 560 | * @see #setOffscreenPageLimit(int) 561 | */ 562 | public int getOffscreenPageLimit() { 563 | return mOffscreenPageLimit; 564 | } 565 | 566 | /** 567 | * Set the number of pages that should be retained to either side of the 568 | * current page in the view hierarchy in an idle state. Pages beyond this 569 | * limit will be recreated from the adapter when needed. 570 | * 571 | *This is offered as an optimization. If you know in advance the number 572 | * of pages you will need to support or have lazy-loading mechanisms in place 573 | * on your pages, tweaking this setting can have benefits in perceived smoothness 574 | * of paging animations and interaction. If you have a small number of pages (3-4) 575 | * that you can keep active all at once, less time will be spent in layout for 576 | * newly created view subtrees as the user pages back and forth.
577 | * 578 | *You should keep this limit low, especially if your pages have complex layouts. 579 | * This setting defaults to 1.
580 | * 581 | * @param limit How many pages will be kept offscreen in an idle state. 582 | */ 583 | public void setOffscreenPageLimit(int limit) { 584 | if (limit < DEFAULT_OFFSCREEN_PAGES) { 585 | Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + 586 | DEFAULT_OFFSCREEN_PAGES); 587 | limit = DEFAULT_OFFSCREEN_PAGES; 588 | } 589 | if (limit != mOffscreenPageLimit) { 590 | mOffscreenPageLimit = limit; 591 | populate(); 592 | } 593 | } 594 | 595 | /** 596 | * Set the margin between pages. 597 | * 598 | * @param marginPixels Distance between adjacent pages in pixels 599 | * @see #getPageMargin() 600 | * @see #setPageMarginDrawable(Drawable) 601 | * @see #setPageMarginDrawable(int) 602 | */ 603 | public void setPageMargin(int marginPixels) { 604 | final int oldMargin = mPageMargin; 605 | mPageMargin = marginPixels; 606 | 607 | final int height = getHeight(); 608 | recomputeScrollPosition(height, height, marginPixels, oldMargin); 609 | 610 | requestLayout(); 611 | } 612 | 613 | /** 614 | * Return the margin between pages. 615 | * 616 | * @return The size of the margin in pixels 617 | */ 618 | public int getPageMargin() { 619 | return mPageMargin; 620 | } 621 | 622 | /** 623 | * Set a drawable that will be used to fill the margin between pages. 624 | * 625 | * @param d Drawable to display between pages 626 | */ 627 | public void setPageMarginDrawable(Drawable d) { 628 | mMarginDrawable = d; 629 | if (d != null) refreshDrawableState(); 630 | setWillNotDraw(d == null); 631 | invalidate(); 632 | } 633 | 634 | /** 635 | * Set a drawable that will be used to fill the margin between pages. 636 | * 637 | * @param resId Resource ID of a drawable to display between pages 638 | */ 639 | public void setPageMarginDrawable(int resId) { 640 | setPageMarginDrawable(getContext().getResources().getDrawable(resId)); 641 | } 642 | 643 | @Override 644 | protected boolean verifyDrawable(Drawable who) { 645 | return super.verifyDrawable(who) || who == mMarginDrawable; 646 | } 647 | 648 | @Override 649 | protected void drawableStateChanged() { 650 | super.drawableStateChanged(); 651 | final Drawable d = mMarginDrawable; 652 | if (d != null && d.isStateful()) { 653 | d.setState(getDrawableState()); 654 | } 655 | } 656 | 657 | // We want the duration of the page snap animation to be influenced by the distance that 658 | // the screen has to travel, however, we don't want this duration to be effected in a 659 | // purely linear fashion. Instead, we use this method to moderate the effect that the distance 660 | // of travel has on the overall snap duration. 661 | float distanceInfluenceForSnapDuration(float f) { 662 | f -= 0.5f; // center the values about 0. 663 | f *= 0.3f * Math.PI / 2.0f; 664 | return (float) Math.sin(f); 665 | } 666 | 667 | /** 668 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 669 | * 670 | * @param x the number of pixels to scroll by on the X axis 671 | * @param y the number of pixels to scroll by on the Y axis 672 | */ 673 | void smoothScrollTo(int x, int y) { 674 | smoothScrollTo(x, y, 0); 675 | } 676 | 677 | /** 678 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 679 | * 680 | * @param x the number of pixels to scroll by on the X axis 681 | * @param y the number of pixels to scroll by on the Y axis 682 | * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) 683 | */ 684 | void smoothScrollTo(int x, int y, int velocity) { 685 | if (getChildCount() == 0) { 686 | // Nothing to do. 687 | setScrollingCacheEnabled(false); 688 | return; 689 | } 690 | int sx = getScrollX(); 691 | int sy = getScrollY(); 692 | int dx = x - sx; 693 | int dy = y - sy; 694 | if (dx == 0 && dy == 0) { 695 | completeScroll(false); 696 | populate(); 697 | setScrollState(SCROLL_STATE_IDLE); 698 | return; 699 | } 700 | 701 | setScrollingCacheEnabled(true); 702 | setScrollState(SCROLL_STATE_SETTLING); 703 | 704 | final int height = getClientHeight(); 705 | final int halfHeight = height / 2; 706 | final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / height); 707 | final float distance = halfHeight + halfHeight * 708 | distanceInfluenceForSnapDuration(distanceRatio); 709 | 710 | int duration = 0; 711 | velocity = Math.abs(velocity); 712 | if (velocity > 0) { 713 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 714 | } else { 715 | final float pageHeight = height * mAdapter.getPageWidth(mCurItem); 716 | final float pageDelta = (float) Math.abs(dx) / (pageHeight + mPageMargin); 717 | duration = (int) ((pageDelta + 1) * 100); 718 | } 719 | duration = Math.min(duration, MAX_SETTLE_DURATION); 720 | 721 | mScroller.startScroll(sx, sy, dx, dy, duration); 722 | ViewCompat.postInvalidateOnAnimation(this); 723 | } 724 | 725 | ItemInfo addNewItem(int position, int index) { 726 | ItemInfo ii = new ItemInfo(); 727 | ii.position = position; 728 | ii.object = mAdapter.instantiateItem(this, position); 729 | ii.heightFactor = mAdapter.getPageWidth(position); 730 | if (index < 0 || index >= mItems.size()) { 731 | mItems.add(ii); 732 | } else { 733 | mItems.add(index, ii); 734 | } 735 | return ii; 736 | } 737 | 738 | void dataSetChanged() { 739 | // This method only gets called if our observer is attached, so mAdapter is non-null. 740 | 741 | final int adapterCount = mAdapter.getCount(); 742 | mExpectedAdapterCount = adapterCount; 743 | boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && 744 | mItems.size() < adapterCount; 745 | int newCurrItem = mCurItem; 746 | 747 | boolean isUpdating = false; 748 | for (int i = 0; i < mItems.size(); i++) { 749 | final ItemInfo ii = mItems.get(i); 750 | final int newPos = mAdapter.getItemPosition(ii.object); 751 | 752 | if (newPos == PagerAdapter.POSITION_UNCHANGED) { 753 | continue; 754 | } 755 | 756 | if (newPos == PagerAdapter.POSITION_NONE) { 757 | mItems.remove(i); 758 | i--; 759 | 760 | if (!isUpdating) { 761 | mAdapter.startUpdate(this); 762 | isUpdating = true; 763 | } 764 | 765 | mAdapter.destroyItem(this, ii.position, ii.object); 766 | needPopulate = true; 767 | 768 | if (mCurItem == ii.position) { 769 | // Keep the current item in the valid range 770 | newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); 771 | needPopulate = true; 772 | } 773 | continue; 774 | } 775 | 776 | if (ii.position != newPos) { 777 | if (ii.position == mCurItem) { 778 | // Our current item changed position. Follow it. 779 | newCurrItem = newPos; 780 | } 781 | 782 | ii.position = newPos; 783 | needPopulate = true; 784 | } 785 | } 786 | 787 | if (isUpdating) { 788 | mAdapter.finishUpdate(this); 789 | } 790 | 791 | Collections.sort(mItems, COMPARATOR); 792 | 793 | if (needPopulate) { 794 | // Reset our known page widths; populate will recompute them. 795 | final int childCount = getChildCount(); 796 | for (int i = 0; i < childCount; i++) { 797 | final View child = getChildAt(i); 798 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 799 | if (!lp.isDecor) { 800 | lp.heightFactor = 0.f; 801 | } 802 | } 803 | 804 | setCurrentItemInternal(newCurrItem, false, true); 805 | requestLayout(); 806 | } 807 | } 808 | 809 | void populate() { 810 | populate(mCurItem); 811 | } 812 | 813 | void populate(int newCurrentItem) { 814 | ItemInfo oldCurInfo = null; 815 | int focusDirection = View.FOCUS_FORWARD; 816 | if (mCurItem != newCurrentItem) { 817 | focusDirection = mCurItem < newCurrentItem ? View.FOCUS_DOWN : View.FOCUS_UP; 818 | oldCurInfo = infoForPosition(mCurItem); 819 | mCurItem = newCurrentItem; 820 | } 821 | 822 | if (mAdapter == null) { 823 | sortChildDrawingOrder(); 824 | return; 825 | } 826 | 827 | // Bail now if we are waiting to populate. This is to hold off 828 | // on creating views from the time the user releases their finger to 829 | // fling to a new position until we have finished the scroll to 830 | // that position, avoiding glitches from happening at that point. 831 | if (mPopulatePending) { 832 | if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 833 | sortChildDrawingOrder(); 834 | return; 835 | } 836 | 837 | // Also, don't populate until we are attached to a window. This is to 838 | // avoid trying to populate before we have restored our view hierarchy 839 | // state and conflicting with what is restored. 840 | if (getWindowToken() == null) { 841 | return; 842 | } 843 | 844 | mAdapter.startUpdate(this); 845 | 846 | final int pageLimit = mOffscreenPageLimit; 847 | final int startPos = Math.max(0, mCurItem - pageLimit); 848 | final int N = mAdapter.getCount(); 849 | final int endPos = Math.min(N - 1, mCurItem + pageLimit); 850 | 851 | if (N != mExpectedAdapterCount) { 852 | String resName; 853 | try { 854 | resName = getResources().getResourceName(getId()); 855 | } catch (Resources.NotFoundException e) { 856 | resName = Integer.toHexString(getId()); 857 | } 858 | throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + 859 | " contents without calling PagerAdapter#notifyDataSetChanged!" + 860 | " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + 861 | " Pager id: " + resName + 862 | " Pager class: " + getClass() + 863 | " Problematic adapter: " + mAdapter.getClass()); 864 | } 865 | 866 | // Locate the currently focused item or add it if needed. 867 | int curIndex = -1; 868 | ItemInfo curItem = null; 869 | for (curIndex = 0; curIndex < mItems.size(); curIndex++) { 870 | final ItemInfo ii = mItems.get(curIndex); 871 | if (ii.position >= mCurItem) { 872 | if (ii.position == mCurItem) curItem = ii; 873 | break; 874 | } 875 | } 876 | 877 | if (curItem == null && N > 0) { 878 | curItem = addNewItem(mCurItem, curIndex); 879 | } 880 | 881 | // Fill 3x the available width or up to the number of offscreen 882 | // pages requested to either side, whichever is larger. 883 | // If we have no current item we have no work to do. 884 | if (curItem != null) { 885 | float extraHeightTop = 0.f; 886 | int itemIndex = curIndex - 1; 887 | ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 888 | final int clientHeight = getClientHeight(); 889 | final float topHeightNeeded = clientHeight <= 0 ? 0 : 890 | 2.f - curItem.heightFactor + (float) getPaddingLeft() / (float) clientHeight; 891 | for (int pos = mCurItem - 1; pos >= 0; pos--) { 892 | if (extraHeightTop >= topHeightNeeded && pos < startPos) { 893 | if (ii == null) { 894 | break; 895 | } 896 | if (pos == ii.position && !ii.scrolling) { 897 | mItems.remove(itemIndex); 898 | mAdapter.destroyItem(this, pos, ii.object); 899 | if (DEBUG) { 900 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 901 | " view: " + ((View) ii.object)); 902 | } 903 | itemIndex--; 904 | curIndex--; 905 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 906 | } 907 | } else if (ii != null && pos == ii.position) { 908 | extraHeightTop += ii.heightFactor; 909 | itemIndex--; 910 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 911 | } else { 912 | ii = addNewItem(pos, itemIndex + 1); 913 | extraHeightTop += ii.heightFactor; 914 | curIndex++; 915 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 916 | } 917 | } 918 | 919 | float extraHeightBottom = curItem.heightFactor; 920 | itemIndex = curIndex + 1; 921 | if (extraHeightBottom < 2.f) { 922 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 923 | final float bottomHeightNeeded = clientHeight <= 0 ? 0 : 924 | (float) getPaddingRight() / (float) clientHeight + 2.f; 925 | for (int pos = mCurItem + 1; pos < N; pos++) { 926 | if (extraHeightBottom >= bottomHeightNeeded && pos > endPos) { 927 | if (ii == null) { 928 | break; 929 | } 930 | if (pos == ii.position && !ii.scrolling) { 931 | mItems.remove(itemIndex); 932 | mAdapter.destroyItem(this, pos, ii.object); 933 | if (DEBUG) { 934 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 935 | " view: " + ((View) ii.object)); 936 | } 937 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 938 | } 939 | } else if (ii != null && pos == ii.position) { 940 | extraHeightBottom += ii.heightFactor; 941 | itemIndex++; 942 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 943 | } else { 944 | ii = addNewItem(pos, itemIndex); 945 | itemIndex++; 946 | extraHeightBottom += ii.heightFactor; 947 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 948 | } 949 | } 950 | } 951 | 952 | calculatePageOffsets(curItem, curIndex, oldCurInfo); 953 | } 954 | 955 | if (DEBUG) { 956 | Log.i(TAG, "Current page list:"); 957 | for (int i = 0; i < mItems.size(); i++) { 958 | Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); 959 | } 960 | } 961 | 962 | mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); 963 | 964 | mAdapter.finishUpdate(this); 965 | 966 | // Check width measurement of current pages and drawing sort order. 967 | // Update LayoutParams as needed. 968 | final int childCount = getChildCount(); 969 | for (int i = 0; i < childCount; i++) { 970 | final View child = getChildAt(i); 971 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 972 | lp.childIndex = i; 973 | if (!lp.isDecor && lp.heightFactor == 0.f) { 974 | // 0 means requery the adapter for this, it doesn't have a valid width. 975 | final ItemInfo ii = infoForChild(child); 976 | if (ii != null) { 977 | lp.heightFactor = ii.heightFactor; 978 | lp.position = ii.position; 979 | } 980 | } 981 | } 982 | sortChildDrawingOrder(); 983 | 984 | if (hasFocus()) { 985 | View currentFocused = findFocus(); 986 | ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; 987 | if (ii == null || ii.position != mCurItem) { 988 | for (int i = 0; i < getChildCount(); i++) { 989 | View child = getChildAt(i); 990 | ii = infoForChild(child); 991 | if (ii != null && ii.position == mCurItem) { 992 | if (child.requestFocus(focusDirection)) { 993 | break; 994 | } 995 | } 996 | } 997 | } 998 | } 999 | } 1000 | 1001 | private void sortChildDrawingOrder() { 1002 | if (mDrawingOrder != DRAW_ORDER_DEFAULT) { 1003 | if (mDrawingOrderedChildren == null) { 1004 | mDrawingOrderedChildren = new ArrayListA fake drag can be useful if you want to synchronize the motion of the ViewPager 2148 | * with the touch scrolling of another view, while still letting the ViewPager 2149 | * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) 2150 | * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call 2151 | * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. 2152 | *
2153 | *During a fake drag the ViewPager will ignore all touch events. If a real drag
2154 | * is already in progress, this method will return false.
2155 | *
2156 | * @return true if the fake drag began successfully, false if it could not be started.
2157 | * @see #fakeDragBy(float)
2158 | * @see #endFakeDrag()
2159 | */
2160 | public boolean beginFakeDrag() {
2161 | if (mIsBeingDragged) {
2162 | return false;
2163 | }
2164 | mFakeDragging = true;
2165 | setScrollState(SCROLL_STATE_DRAGGING);
2166 | mInitialMotionY = mLastMotionY = 0;
2167 | if (mVelocityTracker == null) {
2168 | mVelocityTracker = VelocityTracker.obtain();
2169 | } else {
2170 | mVelocityTracker.clear();
2171 | }
2172 | final long time = SystemClock.uptimeMillis();
2173 | final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
2174 | mVelocityTracker.addMovement(ev);
2175 | ev.recycle();
2176 | mFakeDragBeginTime = time;
2177 | return true;
2178 | }
2179 |
2180 | /**
2181 | * End a fake drag of the pager.
2182 | *
2183 | * @see #beginFakeDrag()
2184 | * @see #fakeDragBy(float)
2185 | */
2186 | public void endFakeDrag() {
2187 | if (!mFakeDragging) {
2188 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2189 | }
2190 |
2191 | final VelocityTracker velocityTracker = mVelocityTracker;
2192 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
2193 | int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
2194 | velocityTracker, mActivePointerId);
2195 | mPopulatePending = true;
2196 | final int height = getClientHeight();
2197 | final int scrollY = getScrollY();
2198 | final ItemInfo ii = infoForCurrentScrollPosition();
2199 | final int currentPage = ii.position;
2200 | final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor;
2201 | final int totalDelta = (int) (mLastMotionY - mInitialMotionY);
2202 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
2203 | totalDelta);
2204 | setCurrentItemInternal(nextPage, true, true, initialVelocity);
2205 | endDrag();
2206 |
2207 | mFakeDragging = false;
2208 | }
2209 |
2210 | /**
2211 | * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
2212 | *
2213 | * @param yOffset Offset in pixels to drag by.
2214 | * @see #beginFakeDrag()
2215 | * @see #endFakeDrag()
2216 | */
2217 | public void fakeDragBy(float yOffset) {
2218 | if (!mFakeDragging) {
2219 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2220 | }
2221 |
2222 | mLastMotionY += yOffset;
2223 |
2224 | float oldScrollY = getScrollY();
2225 | float scrollY = oldScrollY - yOffset;
2226 | final int height = getClientHeight();
2227 |
2228 | float topBound = height * mFirstOffset;
2229 | float bottomBound = height * mLastOffset;
2230 |
2231 | final ItemInfo firstItem = mItems.get(0);
2232 | final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2233 | if (firstItem.position != 0) {
2234 | topBound = firstItem.offset * height;
2235 | }
2236 | if (lastItem.position != mAdapter.getCount() - 1) {
2237 | bottomBound = lastItem.offset * height;
2238 | }
2239 |
2240 | if (scrollY < topBound) {
2241 | scrollY = topBound;
2242 | } else if (scrollY > bottomBound) {
2243 | scrollY = bottomBound;
2244 | }
2245 | // Don't lose the rounded component
2246 | mLastMotionY += scrollY - (int) scrollY;
2247 | scrollTo(getScrollX(), (int) scrollY);
2248 | pageScrolled((int) scrollY);
2249 |
2250 | // Synthesize an event for the VelocityTracker.
2251 | final long time = SystemClock.uptimeMillis();
2252 | final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
2253 | 0, mLastMotionY, 0);
2254 | mVelocityTracker.addMovement(ev);
2255 | ev.recycle();
2256 | }
2257 |
2258 | /**
2259 | * Returns true if a fake drag is in progress.
2260 | *
2261 | * @return true if currently in a fake drag, false otherwise.
2262 | * @see #beginFakeDrag()
2263 | * @see #fakeDragBy(float)
2264 | * @see #endFakeDrag()
2265 | */
2266 | public boolean isFakeDragging() {
2267 | return mFakeDragging;
2268 | }
2269 |
2270 | private void onSecondaryPointerUp(MotionEvent ev) {
2271 | final int pointerIndex = MotionEventCompat.getActionIndex(ev);
2272 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
2273 | if (pointerId == mActivePointerId) {
2274 | // This was our active pointer going up. Choose a new
2275 | // active pointer and adjust accordingly.
2276 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
2277 | mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
2278 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
2279 | if (mVelocityTracker != null) {
2280 | mVelocityTracker.clear();
2281 | }
2282 | }
2283 | }
2284 |
2285 | private void endDrag() {
2286 | mIsBeingDragged = false;
2287 | mIsUnableToDrag = false;
2288 |
2289 | if (mVelocityTracker != null) {
2290 | mVelocityTracker.recycle();
2291 | mVelocityTracker = null;
2292 | }
2293 | }
2294 |
2295 | private void setScrollingCacheEnabled(boolean enabled) {
2296 | if (mScrollingCacheEnabled != enabled) {
2297 | mScrollingCacheEnabled = enabled;
2298 | if (USE_CACHE) {
2299 | final int size = getChildCount();
2300 | for (int i = 0; i < size; ++i) {
2301 | final View child = getChildAt(i);
2302 | if (child.getVisibility() != GONE) {
2303 | child.setDrawingCacheEnabled(enabled);
2304 | }
2305 | }
2306 | }
2307 | }
2308 | }
2309 |
2310 | public boolean internalCanScrollVertically(int direction) {
2311 | if (mAdapter == null) {
2312 | return false;
2313 | }
2314 |
2315 | final int height = getClientHeight();
2316 | final int scrollY = getScrollY();
2317 | if (direction < 0) {
2318 | return (scrollY > (int) (height * mFirstOffset));
2319 | } else if (direction > 0) {
2320 | return (scrollY < (int) (height * mLastOffset));
2321 | } else {
2322 | return false;
2323 | }
2324 | }
2325 |
2326 | /**
2327 | * Tests scrollability within child views of v given a delta of dx.
2328 | *
2329 | * @param v View to test for horizontal scrollability
2330 | * @param checkV Whether the view v passed should itself be checked for scrollability (true),
2331 | * or just its children (false).
2332 | * @param dy Delta scrolled in pixels
2333 | * @param x X coordinate of the active touch point
2334 | * @param y Y coordinate of the active touch point
2335 | * @return true if child views of v can be scrolled by delta of dx.
2336 | */
2337 | protected boolean canScroll(View v, boolean checkV, int dy, int x, int y) {
2338 | if (v instanceof ViewGroup) {
2339 | final ViewGroup group = (ViewGroup) v;
2340 | final int scrollX = v.getScrollX();
2341 | final int scrollY = v.getScrollY();
2342 | final int count = group.getChildCount();
2343 | // Count backwards - let topmost views consume scroll distance first.
2344 | for (int i = count - 1; i >= 0; i--) {
2345 | // TODO: Add versioned support here for transformed views.
2346 | // This will not work for transformed views in Honeycomb+
2347 | final View child = group.getChildAt(i);
2348 | if (y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
2349 | x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
2350 | canScroll(child, true, dy, x + scrollX - child.getLeft(),
2351 | y + scrollY - child.getTop())) {
2352 | return true;
2353 | }
2354 | }
2355 | }
2356 |
2357 | return checkV && ViewCompat.canScrollVertically(v, -dy);
2358 | }
2359 |
2360 | /**
2361 | * Return false if:
2362 | * If the adapter is in the first row
2363 | * & (If the child is a viewgroup) If the child is in the first row
2364 | * & If the scroll is trying to go up
2365 | */
2366 | protected boolean allowDragDown(float dy) {
2367 | return mCurItem != 0 && dy > 0;
2368 | }
2369 |
2370 | protected boolean allowDragUp(float dy) {
2371 | return mCurItem != getAdapter().getCount() - 1 && dy < 0;
2372 | }
2373 |
2374 | @Override
2375 | public boolean dispatchKeyEvent(KeyEvent event) {
2376 | // Let the focused view and/or our descendants get the key first
2377 | return super.dispatchKeyEvent(event) || executeKeyEvent(event);
2378 | }
2379 |
2380 | /**
2381 | * You can call this function yourself to have the scroll view perform
2382 | * scrolling from a key event, just as if the event had been dispatched to
2383 | * it by the view hierarchy.
2384 | *
2385 | * @param event The key event to execute.
2386 | * @return Return true if the event was handled, else false.
2387 | */
2388 | public boolean executeKeyEvent(KeyEvent event) {
2389 | boolean handled = false;
2390 | if (event.getAction() == KeyEvent.ACTION_DOWN) {
2391 | switch (event.getKeyCode()) {
2392 | case KeyEvent.KEYCODE_DPAD_LEFT:
2393 | handled = arrowScroll(FOCUS_LEFT);
2394 | break;
2395 | case KeyEvent.KEYCODE_DPAD_RIGHT:
2396 | handled = arrowScroll(FOCUS_RIGHT);
2397 | break;
2398 | case KeyEvent.KEYCODE_TAB:
2399 | if (Build.VERSION.SDK_INT >= 11) {
2400 | // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
2401 | // before Android 3.0. Ignore the tab key on those devices.
2402 | if (KeyEventCompat.hasNoModifiers(event)) {
2403 | handled = arrowScroll(FOCUS_FORWARD);
2404 | } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
2405 | handled = arrowScroll(FOCUS_BACKWARD);
2406 | }
2407 | }
2408 | break;
2409 | }
2410 | }
2411 | return handled;
2412 | }
2413 |
2414 | public boolean arrowScroll(int direction) {
2415 | View currentFocused = findFocus();
2416 | if (currentFocused == this) {
2417 | currentFocused = null;
2418 | } else if (currentFocused != null) {
2419 | boolean isChild = false;
2420 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2421 | parent = parent.getParent()) {
2422 | if (parent == this) {
2423 | isChild = true;
2424 | break;
2425 | }
2426 | }
2427 | if (!isChild) {
2428 | // This would cause the focus search down below to fail in fun ways.
2429 | final StringBuilder sb = new StringBuilder();
2430 | sb.append(currentFocused.getClass().getSimpleName());
2431 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2432 | parent = parent.getParent()) {
2433 | sb.append(" => ").append(parent.getClass().getSimpleName());
2434 | }
2435 | Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
2436 | "current focused view " + sb.toString());
2437 | currentFocused = null;
2438 | }
2439 | }
2440 |
2441 | boolean handled = false;
2442 |
2443 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
2444 | direction);
2445 | if (nextFocused != null && nextFocused != currentFocused) {
2446 | if (direction == View.FOCUS_UP) {
2447 | // If there is nothing to the left, or this is causing us to
2448 | // jump to the right, then what we really want to do is page left.
2449 | final int nextTop = getChildRectInPagerCoordinates(mTempRect, nextFocused).top;
2450 | final int currTop = getChildRectInPagerCoordinates(mTempRect, currentFocused).top;
2451 | if (currentFocused != null && nextTop >= currTop) {
2452 | handled = pageUp();
2453 | } else {
2454 | handled = nextFocused.requestFocus();
2455 | }
2456 | } else if (direction == View.FOCUS_DOWN) {
2457 | // If there is nothing to the right, or this is causing us to
2458 | // jump to the left, then what we really want to do is page right.
2459 | final int nextDown = getChildRectInPagerCoordinates(mTempRect, nextFocused).bottom;
2460 | final int currDown = getChildRectInPagerCoordinates(mTempRect, currentFocused).bottom;
2461 | if (currentFocused != null && nextDown <= currDown) {
2462 | handled = pageDown();
2463 | } else {
2464 | handled = nextFocused.requestFocus();
2465 | }
2466 | }
2467 | } else if (direction == FOCUS_UP || direction == FOCUS_BACKWARD) {
2468 | // Trying to move left and nothing there; try to page.
2469 | handled = pageUp();
2470 | } else if (direction == FOCUS_DOWN || direction == FOCUS_FORWARD) {
2471 | // Trying to move right and nothing there; try to page.
2472 | handled = pageDown();
2473 | }
2474 | if (handled) {
2475 | playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
2476 | }
2477 | return handled;
2478 | }
2479 |
2480 | private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
2481 | if (outRect == null) {
2482 | outRect = new Rect();
2483 | }
2484 | if (child == null) {
2485 | outRect.set(0, 0, 0, 0);
2486 | return outRect;
2487 | }
2488 | outRect.left = child.getLeft();
2489 | outRect.right = child.getRight();
2490 | outRect.top = child.getTop();
2491 | outRect.bottom = child.getBottom();
2492 |
2493 | ViewParent parent = child.getParent();
2494 | while (parent instanceof ViewGroup && parent != this) {
2495 | final ViewGroup group = (ViewGroup) parent;
2496 | outRect.left += group.getLeft();
2497 | outRect.right += group.getRight();
2498 | outRect.top += group.getTop();
2499 | outRect.bottom += group.getBottom();
2500 |
2501 | parent = group.getParent();
2502 | }
2503 | return outRect;
2504 | }
2505 |
2506 | boolean pageUp() {
2507 | if (mCurItem > 0) {
2508 | setCurrentItem(mCurItem - 1, true);
2509 | return true;
2510 | }
2511 | return false;
2512 | }
2513 |
2514 | boolean pageDown() {
2515 | if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) {
2516 | setCurrentItem(mCurItem + 1, true);
2517 | return true;
2518 | }
2519 | return false;
2520 | }
2521 |
2522 | /**
2523 | * We only want the current page that is being shown to be focusable.
2524 | */
2525 | @Override
2526 | public void addFocusables(ArrayList