├── .gitignore
├── .idea
├── .name
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── encodings.xml
├── gradle.xml
├── libraries
│ ├── ComAndroidSupportAppcompatV71800_aar.xml
│ ├── ViewPagerSample_build_exploded_bundles_ComAndroidSupportAppcompatV71800_aar_classes_jar_ComAndroidSupportAppcompatV71800_aar.xml
│ ├── ViewPager_build_exploded_bundles_ComAndroidSupportAppcompatV71800_aar_classes_jar_ComAndroidSupportAppcompatV71800_aar.xml
│ └── support_v4_18_0_0.xml
├── misc.xml
├── modules.xml
├── scopes
│ └── scope_settings.xml
└── vcs.xml
├── LICENSE.md
├── README.md
├── ViewPager
├── .gitignore
├── ViewPager-ViewPager.iml
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── ryanharter
│ │ └── viewpager
│ │ ├── PagerAdapter.java
│ │ └── ViewPager.java
│ └── res
│ ├── drawable-hdpi
│ └── ic_launcher.png
│ ├── drawable-mdpi
│ └── ic_launcher.png
│ ├── drawable-xhdpi
│ └── ic_launcher.png
│ ├── values-v11
│ └── styles.xml
│ ├── values-v14
│ └── styles.xml
│ └── values
│ ├── attrs.xml
│ ├── strings.xml
│ └── styles.xml
├── ViewPagerSample
├── .gitignore
├── ViewPagerSample.iml
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── ryanharter
│ │ └── viewpager
│ │ └── sample
│ │ ├── HorizontalPagingFragment.java
│ │ ├── MainActivity.java
│ │ └── VerticalPagingFragment.java
│ └── res
│ ├── drawable-hdpi
│ └── ic_launcher.png
│ ├── drawable-mdpi
│ └── ic_launcher.png
│ ├── drawable-xhdpi
│ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ └── ic_launcher.png
│ ├── layout
│ ├── activity_main.xml
│ ├── fragment_horizontal.xml
│ ├── fragment_main.xml
│ ├── fragment_vertical.xml
│ └── page.xml
│ ├── menu
│ └── main.xml
│ ├── values-w820dp
│ └── dimens.xml
│ └── values
│ ├── arrays.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/workspace.xml
4 | .DS_Store
5 | *.iml
6 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | ViewPagerSample
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
56 |
58 |
Note this class is currently under early design and 70 | * development. The API will likely change in later updates of 71 | * the compatibility library, requiring changes to the source code 72 | * of apps when they are compiled against the newer version.
73 | * 74 | *ViewPager is most often used in conjunction with {@link android.app.Fragment}, 75 | * which is a convenient way to supply and manage the lifecycle of each page. 76 | * There are standard adapters implemented for using fragments with the ViewPager, 77 | * which cover the most common use cases. These are 78 | * {@link android.support.v4.app.FragmentPagerAdapter} and 79 | * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these 80 | * classes have simple code showing how to build a full user interface 81 | * with them. 82 | * 83 | *
Here is a more complicated example of ViewPager, using it in conjuction
84 | * with {@link android.app.ActionBar} tabs. You can find other examples of using
85 | * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code.
86 | *
87 | * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java
88 | * complete}
89 | */
90 | public class ViewPager extends ViewGroup {
91 | private static final String TAG = "ViewPager";
92 | private static final boolean DEBUG = false;
93 |
94 | private static final boolean USE_CACHE = false;
95 |
96 | private static final int DEFAULT_OFFSCREEN_PAGES = 1;
97 | private static final int MAX_SETTLE_DURATION = 600; // ms
98 | private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
99 |
100 | private static final int DEFAULT_GUTTER_SIZE = 16; // dips
101 |
102 | private static final int MIN_FLING_VELOCITY = 400; // dips
103 |
104 | private static final int[] LAYOUT_ATTRS = new int[] {
105 | android.R.attr.layout_gravity
106 | };
107 |
108 | private static final int ORIENTATION_HORIZONTAL = 0;
109 | private static final int ORIENTATION_VERTICAL = 1;
110 |
111 | /**
112 | * Used to track what the expected number of items in the adapter should be.
113 | * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
114 | */
115 | private int mExpectedAdapterCount;
116 |
117 | static class ItemInfo {
118 | Object object;
119 | int position;
120 | boolean scrolling;
121 | float sizeFactor;
122 | float offset;
123 | }
124 |
125 | private static final Comparator As property animation is only supported as of Android 3.0 and forward,
328 | * setting a PageTransformer on a ViewPager on earlier platform versions will
329 | * be ignored.
This is offered as an optimization. If you know in advance the number 717 | * of pages you will need to support or have lazy-loading mechanisms in place 718 | * on your pages, tweaking this setting can have benefits in perceived smoothness 719 | * of paging animations and interaction. If you have a small number of pages (3-4) 720 | * that you can keep active all at once, less time will be spent in layout for 721 | * newly created view subtrees as the user pages back and forth.
722 | * 723 | *You should keep this limit low, especially if your pages have complex layouts. 724 | * This setting defaults to 1.
725 | * 726 | * @param limit How many pages will be kept offscreen in an idle state. 727 | */ 728 | public void setOffscreenPageLimit(int limit) { 729 | if (limit < DEFAULT_OFFSCREEN_PAGES) { 730 | Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + 731 | DEFAULT_OFFSCREEN_PAGES); 732 | limit = DEFAULT_OFFSCREEN_PAGES; 733 | } 734 | if (limit != mOffscreenPageLimit) { 735 | mOffscreenPageLimit = limit; 736 | populate(); 737 | } 738 | } 739 | 740 | /** 741 | * Set the margin between pages. 742 | * 743 | * @param marginPixels Distance between adjacent pages in pixels 744 | * @see #getPageMargin() 745 | * @see #setPageMarginDrawable(Drawable) 746 | * @see #setPageMarginDrawable(int) 747 | */ 748 | public void setPageMargin(int marginPixels) { 749 | final int oldMargin = mPageMargin; 750 | mPageMargin = marginPixels; 751 | 752 | int spacing = 0; 753 | if (isOrientationHorizontal()) { 754 | spacing = getWidth(); 755 | } else { 756 | spacing = getHeight(); 757 | } 758 | recomputeScrollPosition(spacing, spacing, spacing, spacing, marginPixels, oldMargin); 759 | 760 | requestLayout(); 761 | } 762 | 763 | /** 764 | * Return the margin between pages. 765 | * 766 | * @return The size of the margin in pixels 767 | */ 768 | public int getPageMargin() { 769 | return mPageMargin; 770 | } 771 | 772 | /** 773 | * Set a drawable that will be used to fill the margin between pages. 774 | * 775 | * @param d Drawable to display between pages 776 | */ 777 | public void setPageMarginDrawable(Drawable d) { 778 | mMarginDrawable = d; 779 | if (d != null) refreshDrawableState(); 780 | setWillNotDraw(d == null); 781 | invalidate(); 782 | } 783 | 784 | /** 785 | * Set a drawable that will be used to fill the margin between pages. 786 | * 787 | * @param resId Resource ID of a drawable to display between pages 788 | */ 789 | public void setPageMarginDrawable(int resId) { 790 | setPageMarginDrawable(getContext().getResources().getDrawable(resId)); 791 | } 792 | 793 | @Override 794 | protected boolean verifyDrawable(Drawable who) { 795 | return super.verifyDrawable(who) || who == mMarginDrawable; 796 | } 797 | 798 | @Override 799 | protected void drawableStateChanged() { 800 | super.drawableStateChanged(); 801 | final Drawable d = mMarginDrawable; 802 | if (d != null && d.isStateful()) { 803 | d.setState(getDrawableState()); 804 | } 805 | } 806 | 807 | // We want the duration of the page snap animation to be influenced by the distance that 808 | // the screen has to travel, however, we don't want this duration to be effected in a 809 | // purely linear fashion. Instead, we use this method to moderate the effect that the distance 810 | // of travel has on the overall snap duration. 811 | float distanceInfluenceForSnapDuration(float f) { 812 | f -= 0.5f; // center the values about 0. 813 | f *= 0.3f * Math.PI / 2.0f; 814 | return (float) Math.sin(f); 815 | } 816 | 817 | /** 818 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 819 | * 820 | * @param x the number of pixels to scroll by on the X axis 821 | * @param y the number of pixels to scroll by on the Y axis 822 | */ 823 | void smoothScrollTo(int x, int y) { 824 | smoothScrollTo(x, y, 0); 825 | } 826 | 827 | /** 828 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 829 | * 830 | * @param x the number of pixels to scroll by on the X axis 831 | * @param y the number of pixels to scroll by on the Y axis 832 | * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) 833 | */ 834 | void smoothScrollTo(int x, int y, int velocity) { 835 | if (getChildCount() == 0) { 836 | // Nothing to do. 837 | setScrollingCacheEnabled(false); 838 | return; 839 | } 840 | int sx = getScrollX(); 841 | int sy = getScrollY(); 842 | int dx = x - sx; 843 | int dy = y - sy; 844 | if (dx == 0 && dy == 0) { 845 | completeScroll(false); 846 | populate(); 847 | setScrollState(SCROLL_STATE_IDLE); 848 | return; 849 | } 850 | 851 | setScrollingCacheEnabled(true); 852 | setScrollState(SCROLL_STATE_SETTLING); 853 | 854 | final int size = isOrientationHorizontal() ? getClientWidth() : getClientHeight(); 855 | final int halfSize = size / 2; 856 | final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / size); 857 | final float distance = halfSize + halfSize * 858 | distanceInfluenceForSnapDuration(distanceRatio); 859 | 860 | int duration = 0; 861 | velocity = Math.abs(velocity); 862 | if (velocity > 0) { 863 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 864 | } else { 865 | final float pageSize = size * mAdapter.getPageSize(mCurItem); 866 | final float pageDelta = (float) Math.abs(dx) / (pageSize + mPageMargin); 867 | duration = (int) ((pageDelta + 1) * 100); 868 | } 869 | duration = Math.min(duration, MAX_SETTLE_DURATION); 870 | 871 | mScroller.startScroll(sx, sy, dx, dy, duration); 872 | ViewCompat.postInvalidateOnAnimation(this); 873 | } 874 | 875 | ItemInfo addNewItem(int position, int index) { 876 | ItemInfo ii = new ItemInfo(); 877 | ii.position = position; 878 | ii.object = mAdapter.instantiateItem(this, position); 879 | ii.sizeFactor = mAdapter.getPageSize(position); 880 | if (index < 0 || index >= mItems.size()) { 881 | mItems.add(ii); 882 | } else { 883 | mItems.add(index, ii); 884 | } 885 | return ii; 886 | } 887 | 888 | void dataSetChanged() { 889 | // This method only gets called if our observer is attached, so mAdapter is non-null. 890 | 891 | final int adapterCount = mAdapter.getCount(); 892 | mExpectedAdapterCount = adapterCount; 893 | boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && 894 | mItems.size() < adapterCount; 895 | int newCurrItem = mCurItem; 896 | 897 | boolean isUpdating = false; 898 | for (int i = 0; i < mItems.size(); i++) { 899 | final ItemInfo ii = mItems.get(i); 900 | final int newPos = mAdapter.getItemPosition(ii.object); 901 | 902 | if (newPos == PagerAdapter.POSITION_UNCHANGED) { 903 | continue; 904 | } 905 | 906 | if (newPos == PagerAdapter.POSITION_NONE) { 907 | mItems.remove(i); 908 | i--; 909 | 910 | if (!isUpdating) { 911 | mAdapter.startUpdate(this); 912 | isUpdating = true; 913 | } 914 | 915 | mAdapter.destroyItem(this, ii.position, ii.object); 916 | needPopulate = true; 917 | 918 | if (mCurItem == ii.position) { 919 | // Keep the current item in the valid range 920 | newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); 921 | needPopulate = true; 922 | } 923 | continue; 924 | } 925 | 926 | if (ii.position != newPos) { 927 | if (ii.position == mCurItem) { 928 | // Our current item changed position. Follow it. 929 | newCurrItem = newPos; 930 | } 931 | 932 | ii.position = newPos; 933 | needPopulate = true; 934 | } 935 | } 936 | 937 | if (isUpdating) { 938 | mAdapter.finishUpdate(this); 939 | } 940 | 941 | Collections.sort(mItems, COMPARATOR); 942 | 943 | if (needPopulate) { 944 | // Reset our known page widths; populate will recompute them. 945 | final int childCount = getChildCount(); 946 | for (int i = 0; i < childCount; i++) { 947 | final View child = getChildAt(i); 948 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 949 | if (!lp.isDecor) { 950 | lp.sizeFactor = 0.f; 951 | } 952 | } 953 | 954 | setCurrentItemInternal(newCurrItem, false, true); 955 | requestLayout(); 956 | } 957 | } 958 | 959 | void populate() { 960 | populate(mCurItem); 961 | } 962 | 963 | void populate(int newCurrentItem) { 964 | ItemInfo oldCurInfo = null; 965 | int focusDirection = View.FOCUS_FORWARD; 966 | if (mCurItem != newCurrentItem) { 967 | if (isOrientationHorizontal()) { 968 | focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 969 | } else { 970 | focusDirection = mCurItem < newCurrentItem ? View.FOCUS_DOWN : View.FOCUS_UP; 971 | } 972 | oldCurInfo = infoForPosition(mCurItem); 973 | mCurItem = newCurrentItem; 974 | } 975 | 976 | if (mAdapter == null) { 977 | sortChildDrawingOrder(); 978 | return; 979 | } 980 | 981 | // Bail now if we are waiting to populate. This is to hold off 982 | // on creating views from the time the user releases their finger to 983 | // fling to a new position until we have finished the scroll to 984 | // that position, avoiding glitches from happening at that point. 985 | if (mPopulatePending) { 986 | if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 987 | sortChildDrawingOrder(); 988 | return; 989 | } 990 | 991 | // Also, don't populate until we are attached to a window. This is to 992 | // avoid trying to populate before we have restored our view hierarchy 993 | // state and conflicting with what is restored. 994 | if (getWindowToken() == null) { 995 | return; 996 | } 997 | 998 | mAdapter.startUpdate(this); 999 | 1000 | final int pageLimit = mOffscreenPageLimit; 1001 | final int startPos = Math.max(0, mCurItem - pageLimit); 1002 | final int N = mAdapter.getCount(); 1003 | final int endPos = Math.min(N-1, mCurItem + pageLimit); 1004 | 1005 | if (N != mExpectedAdapterCount) { 1006 | String resName; 1007 | try { 1008 | resName = getResources().getResourceName(getId()); 1009 | } catch (Resources.NotFoundException e) { 1010 | resName = Integer.toHexString(getId()); 1011 | } 1012 | throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + 1013 | " contents without calling PagerAdapter#notifyDataSetChanged!" + 1014 | " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + 1015 | " Pager id: " + resName + 1016 | " Pager class: " + getClass() + 1017 | " Problematic adapter: " + mAdapter.getClass()); 1018 | } 1019 | 1020 | // Locate the currently focused item or add it if needed. 1021 | int curIndex = -1; 1022 | ItemInfo curItem = null; 1023 | for (curIndex = 0; curIndex < mItems.size(); curIndex++) { 1024 | final ItemInfo ii = mItems.get(curIndex); 1025 | if (ii.position >= mCurItem) { 1026 | if (ii.position == mCurItem) curItem = ii; 1027 | break; 1028 | } 1029 | } 1030 | 1031 | if (curItem == null && N > 0) { 1032 | curItem = addNewItem(mCurItem, curIndex); 1033 | } 1034 | 1035 | // Fill 3x the available width or up to the number of offscreen 1036 | // pages requested to either side, whichever is larger. 1037 | // If we have no current item we have no work to do. 1038 | if (curItem != null) { 1039 | float extraSizeStart = 0.f; 1040 | int itemIndex = curIndex - 1; 1041 | ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1042 | final int paddingStart = isOrientationHorizontal() ? getPaddingLeft() : getPaddingTop(); 1043 | final int clientSize = isOrientationHorizontal() ? getClientWidth() : getClientHeight(); 1044 | final float startSizeNeeded = clientSize <= 0 ? 0 : 1045 | 2.f - curItem.sizeFactor + (float) paddingStart / (float) clientSize; 1046 | for (int pos = mCurItem - 1; pos >= 0; pos--) { 1047 | if (extraSizeStart >= startSizeNeeded && pos < startPos) { 1048 | if (ii == null) { 1049 | break; 1050 | } 1051 | if (pos == ii.position && !ii.scrolling) { 1052 | mItems.remove(itemIndex); 1053 | mAdapter.destroyItem(this, pos, ii.object); 1054 | if (DEBUG) { 1055 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 1056 | " view: " + ((View) ii.object)); 1057 | } 1058 | itemIndex--; 1059 | curIndex--; 1060 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1061 | } 1062 | } else if (ii != null && pos == ii.position) { 1063 | extraSizeStart += ii.sizeFactor; 1064 | itemIndex--; 1065 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1066 | } else { 1067 | ii = addNewItem(pos, itemIndex + 1); 1068 | extraSizeStart += ii.sizeFactor; 1069 | curIndex++; 1070 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1071 | } 1072 | } 1073 | 1074 | float extraSizeEnd = curItem.sizeFactor; 1075 | itemIndex = curIndex + 1; 1076 | if (extraSizeEnd < 2.f) { 1077 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1078 | final int paddingEnd = isOrientationHorizontal() ? getPaddingRight() : getPaddingBottom(); 1079 | final float endSizeNeeded = clientSize <= 0 ? 0 : 1080 | (float) paddingEnd / (float) clientSize + 2.f; 1081 | for (int pos = mCurItem + 1; pos < N; pos++) { 1082 | if (extraSizeEnd >= endSizeNeeded && pos > endPos) { 1083 | if (ii == null) { 1084 | break; 1085 | } 1086 | if (pos == ii.position && !ii.scrolling) { 1087 | mItems.remove(itemIndex); 1088 | mAdapter.destroyItem(this, pos, ii.object); 1089 | if (DEBUG) { 1090 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 1091 | " view: " + ((View) ii.object)); 1092 | } 1093 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1094 | } 1095 | } else if (ii != null && pos == ii.position) { 1096 | extraSizeEnd += ii.sizeFactor; 1097 | itemIndex++; 1098 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1099 | } else { 1100 | ii = addNewItem(pos, itemIndex); 1101 | itemIndex++; 1102 | extraSizeEnd += ii.sizeFactor; 1103 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1104 | } 1105 | } 1106 | } 1107 | 1108 | calculatePageOffsets(curItem, curIndex, oldCurInfo); 1109 | } 1110 | 1111 | if (DEBUG) { 1112 | Log.i(TAG, "Current page list:"); 1113 | for (int i=0; iA fake drag can be useful if you want to synchronize the motion of the ViewPager 2558 | * with the touch scrolling of another view, while still letting the ViewPager 2559 | * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) 2560 | * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call 2561 | * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. 2562 | * 2563 | *
During a fake drag the ViewPager will ignore all touch events. If a real drag
2564 | * is already in progress, this method will return false.
2565 | *
2566 | * @return true if the fake drag began successfully, false if it could not be started.
2567 | *
2568 | * @see #fakeDragBy(float)
2569 | * @see #endFakeDrag()
2570 | */
2571 | public boolean beginFakeDrag() {
2572 | if (mIsBeingDragged) {
2573 | return false;
2574 | }
2575 | mFakeDragging = true;
2576 | setScrollState(SCROLL_STATE_DRAGGING);
2577 | if (isOrientationHorizontal()) {
2578 | mInitialMotionX = mLastMotionX = 0;
2579 | } else {
2580 | mInitialMotionY = mLastMotionY = 0;
2581 | }
2582 | if (mVelocityTracker == null) {
2583 | mVelocityTracker = VelocityTracker.obtain();
2584 | } else {
2585 | mVelocityTracker.clear();
2586 | }
2587 | final long time = SystemClock.uptimeMillis();
2588 | final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
2589 | mVelocityTracker.addMovement(ev);
2590 | ev.recycle();
2591 | mFakeDragBeginTime = time;
2592 | return true;
2593 | }
2594 |
2595 | /**
2596 | * End a fake drag of the pager.
2597 | *
2598 | * @see #beginFakeDrag()
2599 | * @see #fakeDragBy(float)
2600 | */
2601 | public void endFakeDrag() {
2602 | if (!mFakeDragging) {
2603 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2604 | }
2605 |
2606 | final VelocityTracker velocityTracker = mVelocityTracker;
2607 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
2608 |
2609 | final ItemInfo ii = infoForCurrentScrollPosition();
2610 | final int currentPage = ii.position;
2611 |
2612 | int initialVelocity, totalDelta;
2613 | float pageOffset;
2614 | if (isOrientationHorizontal()) {
2615 | initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
2616 | velocityTracker, mActivePointerId);
2617 | mPopulatePending = true;
2618 | final int width = getClientWidth();
2619 | final int scrollX = getScrollX();
2620 | pageOffset = (((float) scrollX / width) - ii.offset) / ii.sizeFactor;
2621 | totalDelta = (int) (mLastMotionX - mInitialMotionX);
2622 | } else {
2623 | initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
2624 | velocityTracker, mActivePointerId);
2625 | mPopulatePending = true;
2626 | final int height = getClientHeight();
2627 | final int scrollY = getScrollY();
2628 | pageOffset = (((float) scrollY / height) - ii.offset) / ii.sizeFactor;
2629 | totalDelta = (int) (mLastMotionY - mInitialMotionY);
2630 | }
2631 |
2632 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
2633 | totalDelta);
2634 | setCurrentItemInternal(nextPage, true, true, initialVelocity);
2635 | endDrag();
2636 |
2637 | mFakeDragging = false;
2638 | }
2639 |
2640 | /**
2641 | * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
2642 | *
2643 | * @param offset Offset in pixels to drag by.
2644 | * @see #beginFakeDrag()
2645 | * @see #endFakeDrag()
2646 | */
2647 | public void fakeDragBy(float offset) {
2648 | if (!mFakeDragging) {
2649 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2650 | }
2651 |
2652 | if (isOrientationHorizontal()) {
2653 | mLastMotionX += offset;
2654 |
2655 | float oldScrollX = getScrollX();
2656 | float scrollX = oldScrollX - offset;
2657 | final int width = getClientWidth();
2658 |
2659 | float leftBound = width * mFirstOffset;
2660 | float rightBound = width * mLastOffset;
2661 |
2662 | final ItemInfo firstItem = mItems.get(0);
2663 | final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2664 | if (firstItem.position != 0) {
2665 | leftBound = firstItem.offset * width;
2666 | }
2667 | if (lastItem.position != mAdapter.getCount() - 1) {
2668 | rightBound = lastItem.offset * width;
2669 | }
2670 |
2671 | if (scrollX < leftBound) {
2672 | scrollX = leftBound;
2673 | } else if (scrollX > rightBound) {
2674 | scrollX = rightBound;
2675 | }
2676 | // Don't lose the rounded component
2677 | mLastMotionX += scrollX - (int) scrollX;
2678 | scrollTo((int) scrollX, getScrollY());
2679 | pageScrolled((int) scrollX);
2680 |
2681 | // Synthesize an event for the VelocityTracker.
2682 | final long time = SystemClock.uptimeMillis();
2683 | final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
2684 | mLastMotionX, 0, 0);
2685 | mVelocityTracker.addMovement(ev);
2686 | ev.recycle();
2687 | } else {
2688 | mLastMotionY += offset;
2689 |
2690 | float oldScrollY = getScrollY();
2691 | float scrollY = oldScrollY - offset;
2692 | final int height = getClientHeight();
2693 |
2694 | float topBound = height * mFirstOffset;
2695 | float bottomBound = height * mLastOffset;
2696 |
2697 | final ItemInfo firstItem = mItems.get(0);
2698 | final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2699 | if (firstItem.position != 0) {
2700 | topBound = firstItem.offset * height;
2701 | }
2702 | if (lastItem.position != mAdapter.getCount() - 1) {
2703 | bottomBound = lastItem.offset * height;
2704 | }
2705 |
2706 | if (scrollY < topBound) {
2707 | scrollY = topBound;
2708 | } else if (scrollY > bottomBound) {
2709 | scrollY = bottomBound;
2710 | }
2711 | // Don't lose the rounded component
2712 | mLastMotionY += scrollY - (int) scrollY;
2713 | scrollTo(getScrollX(), (int) scrollY);
2714 | pageScrolled((int) scrollY);
2715 |
2716 | // Synthesize an event for the VelocityTracker.
2717 | final long time = SystemClock.uptimeMillis();
2718 | final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
2719 | 0, mLastMotionY, 0);
2720 | mVelocityTracker.addMovement(ev);
2721 | ev.recycle();
2722 | }
2723 | }
2724 |
2725 | /**
2726 | * Returns true if a fake drag is in progress.
2727 | *
2728 | * @return true if currently in a fake drag, false otherwise.
2729 | *
2730 | * @see #beginFakeDrag()
2731 | * @see #fakeDragBy(float)
2732 | * @see #endFakeDrag()
2733 | */
2734 | public boolean isFakeDragging() {
2735 | return mFakeDragging;
2736 | }
2737 |
2738 | private void onSecondaryPointerUp(MotionEvent ev) {
2739 | final int pointerIndex = MotionEventCompat.getActionIndex(ev);
2740 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
2741 | if (pointerId == mActivePointerId) {
2742 | // This was our active pointer going up. Choose a new
2743 | // active pointer and adjust accordingly.
2744 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
2745 | mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
2746 | mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
2747 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
2748 | if (mVelocityTracker != null) {
2749 | mVelocityTracker.clear();
2750 | }
2751 | }
2752 | }
2753 |
2754 | private void endDrag() {
2755 | mIsBeingDragged = false;
2756 | mIsUnableToDrag = false;
2757 |
2758 | if (mVelocityTracker != null) {
2759 | mVelocityTracker.recycle();
2760 | mVelocityTracker = null;
2761 | }
2762 | }
2763 |
2764 | private void setScrollingCacheEnabled(boolean enabled) {
2765 | if (mScrollingCacheEnabled != enabled) {
2766 | mScrollingCacheEnabled = enabled;
2767 | if (USE_CACHE) {
2768 | final int size = getChildCount();
2769 | for (int i = 0; i < size; ++i) {
2770 | final View child = getChildAt(i);
2771 | if (child.getVisibility() != GONE) {
2772 | child.setDrawingCacheEnabled(enabled);
2773 | }
2774 | }
2775 | }
2776 | }
2777 | }
2778 |
2779 | public boolean canScrollHorizontally(int direction) {
2780 | if (mAdapter == null) {
2781 | return false;
2782 | }
2783 |
2784 | final int width = getClientWidth();
2785 | final int scrollX = getScrollX();
2786 | if (direction < 0) {
2787 | return (scrollX > (int) (width * mFirstOffset));
2788 | } else if (direction > 0) {
2789 | return (scrollX < (int) (width * mLastOffset));
2790 | } else {
2791 | return false;
2792 | }
2793 | }
2794 |
2795 | public boolean canScrollVertically(int direction) {
2796 | if (mAdapter == null) {
2797 | return false;
2798 | }
2799 |
2800 | final int height = getClientHeight();
2801 | final int scrollY = getScrollY();
2802 | if (direction < 0) {
2803 | return (scrollY > (int) (height * mFirstOffset));
2804 | } else if (direction > 0) {
2805 | return (scrollY < (int) (height * mLastOffset));
2806 | } else {
2807 | return false;
2808 | }
2809 | }
2810 |
2811 | /**
2812 | * Tests scrollability within child views of v given a delta of dx.
2813 | *
2814 | * @param v View to test for horizontal scrollability
2815 | * @param checkV Whether the view v passed should itself be checked for scrollability (true),
2816 | * or just its children (false).
2817 | * @param dx Delta scrolled in pixels
2818 | * @param x X coordinate of the active touch point
2819 | * @param y Y coordinate of the active touch point
2820 | * @return true if child views of v can be scrolled by delta of dx.
2821 | */
2822 | protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
2823 | if (v instanceof ViewGroup) {
2824 | final ViewGroup group = (ViewGroup) v;
2825 | final int scrollX = v.getScrollX();
2826 | final int scrollY = v.getScrollY();
2827 | final int count = group.getChildCount();
2828 | // Count backwards - let topmost views consume scroll distance first.
2829 | for (int i = count - 1; i >= 0; i--) {
2830 | // TODO: Add versioned support here for transformed views.
2831 | // This will not work for transformed views in Honeycomb+
2832 | final View child = group.getChildAt(i);
2833 | if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
2834 | y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
2835 | canScroll(child, true, dx, x + scrollX - child.getLeft(),
2836 | y + scrollY - child.getTop())) {
2837 | return true;
2838 | }
2839 | }
2840 | }
2841 |
2842 | boolean canScroll = false;
2843 | if (isOrientationHorizontal()) {
2844 | canScroll = ViewCompat.canScrollHorizontally(v, -dx);
2845 | } else {
2846 | canScroll = ViewCompat.canScrollVertically(v, -dx);
2847 | }
2848 | return checkV && canScroll;
2849 | }
2850 |
2851 | @Override
2852 | public boolean dispatchKeyEvent(KeyEvent event) {
2853 | // Let the focused view and/or our descendants get the key first
2854 | return super.dispatchKeyEvent(event) || executeKeyEvent(event);
2855 | }
2856 |
2857 | /**
2858 | * You can call this function yourself to have the scroll view perform
2859 | * scrolling from a key event, just as if the event had been dispatched to
2860 | * it by the view hierarchy.
2861 | *
2862 | * @param event The key event to execute.
2863 | * @return Return true if the event was handled, else false.
2864 | */
2865 | public boolean executeKeyEvent(KeyEvent event) {
2866 | boolean handled = false;
2867 | if (event.getAction() == KeyEvent.ACTION_DOWN) {
2868 | switch (event.getKeyCode()) {
2869 | case KeyEvent.KEYCODE_DPAD_LEFT:
2870 | if (isOrientationHorizontal())
2871 | handled = arrowScroll(FOCUS_LEFT);
2872 | break;
2873 | case KeyEvent.KEYCODE_DPAD_RIGHT:
2874 | if (isOrientationHorizontal())
2875 | handled = arrowScroll(FOCUS_RIGHT);
2876 | break;
2877 | case KeyEvent.KEYCODE_DPAD_UP:
2878 | if (!isOrientationHorizontal())
2879 | handled = arrowScroll(FOCUS_UP);
2880 | break;
2881 | case KeyEvent.KEYCODE_DPAD_DOWN:
2882 | if (!isOrientationHorizontal())
2883 | handled = arrowScroll(FOCUS_DOWN);
2884 | break;
2885 | case KeyEvent.KEYCODE_TAB:
2886 | if (Build.VERSION.SDK_INT >= 11) {
2887 | // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
2888 | // before Android 3.0. Ignore the tab key on those devices.
2889 | if (KeyEventCompat.hasNoModifiers(event)) {
2890 | handled = arrowScroll(FOCUS_FORWARD);
2891 | } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
2892 | handled = arrowScroll(FOCUS_BACKWARD);
2893 | }
2894 | }
2895 | break;
2896 | }
2897 | }
2898 | return handled;
2899 | }
2900 |
2901 | public boolean arrowScroll(int direction) {
2902 | View currentFocused = findFocus();
2903 | if (currentFocused == this) {
2904 | currentFocused = null;
2905 | } else if (currentFocused != null) {
2906 | boolean isChild = false;
2907 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2908 | parent = parent.getParent()) {
2909 | if (parent == this) {
2910 | isChild = true;
2911 | break;
2912 | }
2913 | }
2914 | if (!isChild) {
2915 | // This would cause the focus search down below to fail in fun ways.
2916 | final StringBuilder sb = new StringBuilder();
2917 | sb.append(currentFocused.getClass().getSimpleName());
2918 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2919 | parent = parent.getParent()) {
2920 | sb.append(" => ").append(parent.getClass().getSimpleName());
2921 | }
2922 | Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
2923 | "current focused view " + sb.toString());
2924 | currentFocused = null;
2925 | }
2926 | }
2927 |
2928 | boolean handled = false;
2929 |
2930 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
2931 | direction);
2932 | if (nextFocused != null && nextFocused != currentFocused) {
2933 | if (direction == View.FOCUS_LEFT) {
2934 | // If there is nothing to the left, or this is causing us to
2935 | // jump to the right, then what we really want to do is page left.
2936 | final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
2937 | final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
2938 | if (currentFocused != null && nextLeft >= currLeft) {
2939 | handled = pageLeft();
2940 | } else {
2941 | handled = nextFocused.requestFocus();
2942 | }
2943 | } else if (direction == View.FOCUS_RIGHT) {
2944 | // If there is nothing to the right, or this is causing us to
2945 | // jump to the left, then what we really want to do is page right.
2946 | final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
2947 | final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
2948 | if (currentFocused != null && nextLeft <= currLeft) {
2949 | handled = pageRight();
2950 | } else {
2951 | handled = nextFocused.requestFocus();
2952 | }
2953 | } else if (direction == View.FOCUS_UP) {
2954 | final int nextUp = getChildRectInPagerCoordinates(mTempRect, nextFocused).top;
2955 | final int currUp = getChildRectInPagerCoordinates(mTempRect, currentFocused).top;
2956 | if (currentFocused != null && nextUp >= currUp) {
2957 | handled = pageLeft();
2958 | } else {
2959 | handled = nextFocused.requestFocus();
2960 | }
2961 | } else if (direction == View.FOCUS_DOWN) {
2962 | final int nextUp = getChildRectInPagerCoordinates(mTempRect, nextFocused).top;
2963 | final int currUp = getChildRectInPagerCoordinates(mTempRect, currentFocused).top;
2964 | if (currentFocused != null && nextUp <= currUp) {
2965 | handled = pageRight();
2966 | } else {
2967 | handled = nextFocused.requestFocus();
2968 | }
2969 | }
2970 | } else if (direction == FOCUS_LEFT || direction == FOCUS_UP || direction == FOCUS_BACKWARD) {
2971 | // Trying to move left and nothing there; try to page.
2972 | handled = pageLeft();
2973 | } else if (direction == FOCUS_RIGHT || direction == FOCUS_DOWN || direction == FOCUS_FORWARD) {
2974 | // Trying to move right and nothing there; try to page.
2975 | handled = pageRight();
2976 | }
2977 | if (handled) {
2978 | playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
2979 | }
2980 | return handled;
2981 | }
2982 |
2983 | private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
2984 | if (outRect == null) {
2985 | outRect = new Rect();
2986 | }
2987 | if (child == null) {
2988 | outRect.set(0, 0, 0, 0);
2989 | return outRect;
2990 | }
2991 | outRect.left = child.getLeft();
2992 | outRect.right = child.getRight();
2993 | outRect.top = child.getTop();
2994 | outRect.bottom = child.getBottom();
2995 |
2996 | ViewParent parent = child.getParent();
2997 | while (parent instanceof ViewGroup && parent != this) {
2998 | final ViewGroup group = (ViewGroup) parent;
2999 | outRect.left += group.getLeft();
3000 | outRect.right += group.getRight();
3001 | outRect.top += group.getTop();
3002 | outRect.bottom += group.getBottom();
3003 |
3004 | parent = group.getParent();
3005 | }
3006 | return outRect;
3007 | }
3008 |
3009 | boolean pageLeft() {
3010 | if (mCurItem > 0) {
3011 | setCurrentItem(mCurItem-1, true);
3012 | return true;
3013 | }
3014 | return false;
3015 | }
3016 |
3017 | boolean pageRight() {
3018 | if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) {
3019 | setCurrentItem(mCurItem+1, true);
3020 | return true;
3021 | }
3022 | return false;
3023 | }
3024 |
3025 | /**
3026 | * We only want the current page that is being shown to be focusable.
3027 | */
3028 | @Override
3029 | public void addFocusables(ArrayList