├── .gitignore
├── ParallaxDirectionalViewPager
├── .classpath
├── .project
├── .settings
│ └── org.eclipse.jdt.core.prefs
├── AndroidManifest.xml
├── libs
│ └── android-support-v4.jar
├── project.properties
├── res
│ ├── drawable-xhdpi
│ │ ├── bg_intro.png
│ │ ├── biz_ad_new_version1_img0.png
│ │ ├── biz_ad_new_version1_img1.png
│ │ ├── biz_ad_new_version1_img2.png
│ │ ├── biz_ad_new_version1_img3.png
│ │ ├── biz_ad_show_new_version_btn_normal.png
│ │ ├── biz_ad_show_new_version_btn_pressed.png
│ │ └── ic_launcher.png
│ ├── drawable
│ │ ├── btn_selector.xml
│ │ ├── shape_nor.xml
│ │ └── shape_sel.xml
│ ├── layout
│ │ ├── activity_main.xml
│ │ └── fragment_layout.xml
│ └── values
│ │ ├── attrs.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
└── src
│ └── net
│ └── fengg
│ └── parallaxdirectionalviewpager
│ ├── DirectionalViewPager.java
│ └── sample
│ ├── MainActivity.java
│ ├── TestFragment.java
│ └── TestFragmentAdapter.java
├── README.md
├── horizontal.png
├── record.gif
└── vertical.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 |
15 | # Gradle files
16 | .gradle/
17 | build/
18 |
19 | # Local configuration file (sdk path, etc)
20 | local.properties
21 |
22 | # Proguard folder generated by Eclipse
23 | proguard/
24 |
--------------------------------------------------------------------------------
/ParallaxDirectionalViewPager/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
Note this class is currently under early design and 76 | * development. The API will likely change in later updates of 77 | * the compatibility library, requiring changes to the source code 78 | * of apps when they are compiled against the newer version.
79 | * 80 | *ViewPager is most often used in conjunction with {@link android.app.Fragment}, 81 | * which is a convenient way to supply and manage the lifecycle of each page. 82 | * There are standard adapters implemented for using fragments with the ViewPager, 83 | * which cover the most common use cases. These are 84 | * {@link android.support.v4.app.FragmentPagerAdapter} and 85 | * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these 86 | * classes have simple code showing how to build a full user interface 87 | * with them. 88 | * 89 | *
Here is a more complicated example of ViewPager, using it in conjuction
90 | * with {@link android.app.ActionBar} tabs. You can find other examples of using
91 | * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code.
92 | *
93 | * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java
94 | * complete}
95 | */
96 | public class DirectionalViewPager extends ViewGroup {
97 | public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
98 | public static final int VERTICAL = LinearLayout.VERTICAL;
99 |
100 | private static final String TAG = "ViewPager";
101 | private static final boolean DEBUG = false;
102 |
103 | private static final boolean USE_CACHE = false;
104 |
105 | private static final int DEFAULT_OFFSCREEN_PAGES = 1;
106 | private static final int MAX_SETTLE_DURATION = 600; // ms
107 | private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
108 |
109 | private static final int DEFAULT_GUTTER_SIZE = 16; // dips
110 |
111 | private static final int MIN_FLING_VELOCITY = 400; // dips
112 |
113 | private static final int[] LAYOUT_ATTRS = new int[] {
114 | android.R.attr.layout_gravity
115 | };
116 |
117 |
118 |
119 |
120 | /**
121 | * Used to track what the expected number of items in the adapter should be.
122 | * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
123 | */
124 | private int mExpectedAdapterCount;
125 |
126 | static class ItemInfo {
127 | Object object;
128 | int position;
129 | boolean scrolling;
130 | float dimensionFactor;
131 | float offset;
132 | }
133 |
134 | private static final Comparator As property animation is only supported as of Android 3.0 and forward,
338 | * setting a PageTransformer on a ViewPager on earlier platform versions will
339 | * be ignored.
This is offered as an optimization. If you know in advance the number 746 | * of pages you will need to support or have lazy-loading mechanisms in place 747 | * on your pages, tweaking this setting can have benefits in perceived smoothness 748 | * of paging animations and interaction. If you have a small number of pages (3-4) 749 | * that you can keep active all at once, less time will be spent in layout for 750 | * newly created view subtrees as the user pages back and forth.
751 | * 752 | *You should keep this limit low, especially if your pages have complex layouts. 753 | * This setting defaults to 1.
754 | * 755 | * @param limit How many pages will be kept offscreen in an idle state. 756 | */ 757 | public void setOffscreenPageLimit(int limit) { 758 | if (limit < DEFAULT_OFFSCREEN_PAGES) { 759 | Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + 760 | DEFAULT_OFFSCREEN_PAGES); 761 | limit = DEFAULT_OFFSCREEN_PAGES; 762 | } 763 | if (limit != mOffscreenPageLimit) { 764 | mOffscreenPageLimit = limit; 765 | populate(); 766 | } 767 | } 768 | 769 | /** 770 | * Set the margin between pages. 771 | * 772 | * @param marginPixels Distance between adjacent pages in pixels 773 | * @see #getPageMargin() 774 | * @see #setPageMarginDrawable(Drawable) 775 | * @see #setPageMarginDrawable(int) 776 | */ 777 | public void setPageMargin(int marginPixels) { 778 | final int oldMargin = mPageMargin; 779 | mPageMargin = marginPixels; 780 | 781 | final int width = getWidth(); 782 | recomputeScrollPosition(width, width, marginPixels, oldMargin); 783 | 784 | requestLayout(); 785 | } 786 | 787 | /** 788 | * Return the margin between pages. 789 | * 790 | * @return The size of the margin in pixels 791 | */ 792 | public int getPageMargin() { 793 | return mPageMargin; 794 | } 795 | 796 | public int getScrollCoord() { 797 | return (mOrientation == HORIZONTAL ? getScrollX() : getScrollY()); 798 | } 799 | 800 | /** 801 | * Set a drawable that will be used to fill the margin between pages. 802 | * 803 | * @param d Drawable to display between pages 804 | */ 805 | public void setPageMarginDrawable(Drawable d) { 806 | mMarginDrawable = d; 807 | if (d != null) refreshDrawableState(); 808 | setWillNotDraw(d == null); 809 | invalidate(); 810 | } 811 | 812 | /** 813 | * Set a drawable that will be used to fill the margin between pages. 814 | * 815 | * @param resId Resource ID of a drawable to display between pages 816 | */ 817 | public void setPageMarginDrawable(int resId) { 818 | setPageMarginDrawable(getContext().getResources().getDrawable(resId)); 819 | } 820 | 821 | @Override 822 | protected boolean verifyDrawable(Drawable who) { 823 | return super.verifyDrawable(who) || who == mMarginDrawable; 824 | } 825 | 826 | @Override 827 | protected void drawableStateChanged() { 828 | super.drawableStateChanged(); 829 | final Drawable d = mMarginDrawable; 830 | if (d != null && d.isStateful()) { 831 | d.setState(getDrawableState()); 832 | } 833 | } 834 | 835 | // We want the duration of the page snap animation to be influenced by the distance that 836 | // the screen has to travel, however, we don't want this duration to be effected in a 837 | // purely linear fashion. Instead, we use this method to moderate the effect that the distance 838 | // of travel has on the overall snap duration. 839 | float distanceInfluenceForSnapDuration(float f) { 840 | f -= 0.5f; // center the values about 0. 841 | f *= 0.3f * Math.PI / 2.0f; 842 | return (float) Math.sin(f); 843 | } 844 | 845 | /** 846 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 847 | * 848 | * @param x the number of pixels to scroll by on the X axis 849 | * @param y the number of pixels to scroll by on the Y axis 850 | */ 851 | void smoothScrollTo(int x, int y) { 852 | smoothScrollTo(x, y, 0); 853 | } 854 | 855 | /** 856 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 857 | * 858 | * @param x the number of pixels to scroll by on the X axis 859 | * @param y the number of pixels to scroll by on the Y axis 860 | * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) 861 | */ 862 | void smoothScrollTo(int x, int y, int velocity) { 863 | if (getChildCount() == 0) { 864 | // Nothing to do. 865 | setScrollingCacheEnabled(false); 866 | return; 867 | } 868 | int sx = getScrollX(); 869 | int sy = getScrollY(); 870 | int dx = x - sx; 871 | int dy = y - sy; 872 | if (dx == 0 && dy == 0) { 873 | completeScroll(false); 874 | populate(); 875 | setScrollState(SCROLL_STATE_IDLE); 876 | return; 877 | } 878 | 879 | setScrollingCacheEnabled(true); 880 | setScrollState(SCROLL_STATE_SETTLING); 881 | 882 | final int width = getClientWidth(); 883 | final int halfWidth = width / 2; 884 | final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); 885 | final float distance = halfWidth + halfWidth * 886 | distanceInfluenceForSnapDuration(distanceRatio); 887 | 888 | int duration = 0; 889 | velocity = Math.abs(velocity); 890 | if (velocity > 0) { 891 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 892 | } else { 893 | final float pageWidth = width * mAdapter.getPageWidth(mCurItem); 894 | final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); 895 | duration = (int) ((pageDelta + 1) * 100); 896 | } 897 | duration = Math.min(duration, MAX_SETTLE_DURATION); 898 | 899 | mScroller.startScroll(sx, sy, dx, dy, duration); 900 | ViewCompat.postInvalidateOnAnimation(this); 901 | } 902 | 903 | ItemInfo addNewItem(int position, int index) { 904 | ItemInfo ii = new ItemInfo(); 905 | ii.position = position; 906 | ii.object = mAdapter.instantiateItem(this, position); 907 | 908 | // We call getPageWidth(), but a better name would be getPageFill() It doesn't matter, the default 909 | // implementation returns 1.f anyway... 910 | ii.dimensionFactor = mAdapter.getPageWidth(position); 911 | if (index < 0 || index >= mItems.size()) { 912 | mItems.add(ii); 913 | } else { 914 | mItems.add(index, ii); 915 | } 916 | return ii; 917 | } 918 | 919 | void dataSetChanged() { 920 | // This method only gets called if our observer is attached, so mAdapter is non-null. 921 | 922 | final int adapterCount = mAdapter.getCount(); 923 | mExpectedAdapterCount = adapterCount; 924 | boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && 925 | mItems.size() < adapterCount; 926 | int newCurrItem = mCurItem; 927 | 928 | boolean isUpdating = false; 929 | for (int i = 0; i < mItems.size(); i++) { 930 | final ItemInfo ii = mItems.get(i); 931 | final int newPos = mAdapter.getItemPosition(ii.object); 932 | 933 | if (newPos == PagerAdapter.POSITION_UNCHANGED) { 934 | continue; 935 | } 936 | 937 | if (newPos == PagerAdapter.POSITION_NONE) { 938 | mItems.remove(i); 939 | i--; 940 | 941 | if (!isUpdating) { 942 | mAdapter.startUpdate(this); 943 | isUpdating = true; 944 | } 945 | 946 | mAdapter.destroyItem(this, ii.position, ii.object); 947 | needPopulate = true; 948 | 949 | if (mCurItem == ii.position) { 950 | // Keep the current item in the valid range 951 | newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); 952 | needPopulate = true; 953 | } 954 | continue; 955 | } 956 | 957 | if (ii.position != newPos) { 958 | if (ii.position == mCurItem) { 959 | // Our current item changed position. Follow it. 960 | newCurrItem = newPos; 961 | } 962 | 963 | ii.position = newPos; 964 | needPopulate = true; 965 | } 966 | } 967 | 968 | if (isUpdating) { 969 | mAdapter.finishUpdate(this); 970 | } 971 | 972 | Collections.sort(mItems, COMPARATOR); 973 | 974 | if (needPopulate) { 975 | // Reset our known page widths; populate will recompute them. 976 | final int childCount = getChildCount(); 977 | for (int i = 0; i < childCount; i++) { 978 | final View child = getChildAt(i); 979 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 980 | if (!lp.isDecor) { 981 | lp.dimensionFactor = 0.f; 982 | } 983 | } 984 | 985 | setCurrentItemInternal(newCurrItem, false, true); 986 | requestLayout(); 987 | } 988 | } 989 | 990 | void populate() { 991 | populate(mCurItem); 992 | } 993 | 994 | void populate(int newCurrentItem) { 995 | ItemInfo oldCurInfo = null; 996 | int focusDirection = View.FOCUS_FORWARD; 997 | if (mCurItem != newCurrentItem) { 998 | focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 999 | oldCurInfo = infoForPosition(mCurItem); 1000 | mCurItem = newCurrentItem; 1001 | } 1002 | 1003 | if (mAdapter == null) { 1004 | sortChildDrawingOrder(); 1005 | return; 1006 | } 1007 | 1008 | // Bail now if we are waiting to populate. This is to hold off 1009 | // on creating views from the time the user releases their finger to 1010 | // fling to a new position until we have finished the scroll to 1011 | // that position, avoiding glitches from happening at that point. 1012 | if (mPopulatePending) { 1013 | if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 1014 | sortChildDrawingOrder(); 1015 | return; 1016 | } 1017 | 1018 | // Also, don't populate until we are attached to a window. This is to 1019 | // avoid trying to populate before we have restored our view hierarchy 1020 | // state and conflicting with what is restored. 1021 | if (getWindowToken() == null) { 1022 | return; 1023 | } 1024 | 1025 | mAdapter.startUpdate(this); 1026 | 1027 | final int pageLimit = mOffscreenPageLimit; 1028 | final int startPos = Math.max(0, mCurItem - pageLimit); 1029 | final int N = mAdapter.getCount(); 1030 | final int endPos = Math.min(N-1, mCurItem + pageLimit); 1031 | 1032 | if (N != mExpectedAdapterCount) { 1033 | String resName; 1034 | try { 1035 | resName = getResources().getResourceName(getId()); 1036 | } catch (Resources.NotFoundException e) { 1037 | resName = Integer.toHexString(getId()); 1038 | } 1039 | throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + 1040 | " contents without calling PagerAdapter#notifyDataSetChanged!" + 1041 | " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + 1042 | " Pager id: " + resName + 1043 | " Pager class: " + getClass() + 1044 | " Problematic adapter: " + mAdapter.getClass()); 1045 | } 1046 | 1047 | // Locate the currently focused item or add it if needed. 1048 | int curIndex = -1; 1049 | ItemInfo curItem = null; 1050 | for (curIndex = 0; curIndex < mItems.size(); curIndex++) { 1051 | final ItemInfo ii = mItems.get(curIndex); 1052 | if (ii.position >= mCurItem) { 1053 | if (ii.position == mCurItem) curItem = ii; 1054 | break; 1055 | } 1056 | } 1057 | 1058 | if (curItem == null && N > 0) { 1059 | curItem = addNewItem(mCurItem, curIndex); 1060 | } 1061 | 1062 | // Fill 3x the available space or up to the number of offscreen 1063 | // pages requested to either side, whichever is larger. 1064 | // If we have no current item we have no work to do. 1065 | if (curItem != null) { 1066 | float extraDimensionStart = 0.f; 1067 | int itemIndex = curIndex - 1; 1068 | ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1069 | final int clientDimension = getClientDimension(); 1070 | final float paddingStart = mOrientation == HORIZONTAL ? getPaddingLeft() : getPaddingTop(); 1071 | final float startDimensionNeeded = clientDimension <= 0 ? 0 : 1072 | 2.f - curItem.dimensionFactor + paddingStart / (float) clientDimension; 1073 | for (int pos = mCurItem - 1; pos >= 0; pos--) { 1074 | if (extraDimensionStart >= startDimensionNeeded && pos < startPos) { 1075 | if (ii == null) { 1076 | break; 1077 | } 1078 | if (pos == ii.position && !ii.scrolling) { 1079 | mItems.remove(itemIndex); 1080 | mAdapter.destroyItem(this, pos, ii.object); 1081 | if (DEBUG) { 1082 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 1083 | " view: " + ((View) ii.object)); 1084 | } 1085 | itemIndex--; 1086 | curIndex--; 1087 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1088 | } 1089 | } else if (ii != null && pos == ii.position) { 1090 | extraDimensionStart += ii.dimensionFactor; 1091 | itemIndex--; 1092 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1093 | } else { 1094 | ii = addNewItem(pos, itemIndex + 1); 1095 | extraDimensionStart += ii.dimensionFactor; 1096 | curIndex++; 1097 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1098 | } 1099 | } 1100 | 1101 | float extraDimensionEnd = curItem.dimensionFactor; 1102 | itemIndex = curIndex + 1; 1103 | if (extraDimensionEnd < 2.f) { 1104 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1105 | final float endDimensionNeeded = clientDimension <= 0 ? 0 : 1106 | (float) getPaddingRight() / (float) clientDimension + 2.f; 1107 | for (int pos = mCurItem + 1; pos < N; pos++) { 1108 | if (extraDimensionEnd >= endDimensionNeeded && pos > endPos) { 1109 | if (ii == null) { 1110 | break; 1111 | } 1112 | if (pos == ii.position && !ii.scrolling) { 1113 | mItems.remove(itemIndex); 1114 | mAdapter.destroyItem(this, pos, ii.object); 1115 | if (DEBUG) { 1116 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 1117 | " view: " + ((View) ii.object)); 1118 | } 1119 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1120 | } 1121 | } else if (ii != null && pos == ii.position) { 1122 | extraDimensionEnd += ii.dimensionFactor; 1123 | itemIndex++; 1124 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1125 | } else { 1126 | ii = addNewItem(pos, itemIndex); 1127 | itemIndex++; 1128 | extraDimensionEnd += ii.dimensionFactor; 1129 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1130 | } 1131 | } 1132 | } 1133 | 1134 | calculatePageOffsets(curItem, curIndex, oldCurInfo); 1135 | } 1136 | 1137 | if (DEBUG) { 1138 | Log.i(TAG, "Current page list:"); 1139 | for (int i=0; iA fake drag can be useful if you want to synchronize the motion of the ViewPager 2494 | * with the touch scrolling of another view, while still letting the ViewPager 2495 | * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) 2496 | * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call 2497 | * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. 2498 | * 2499 | *
During a fake drag the ViewPager will ignore all touch events. If a real drag
2500 | * is already in progress, this method will return false.
2501 | *
2502 | * @return true if the fake drag began successfully, false if it could not be started.
2503 | *
2504 | * @see #fakeDragBy(float)
2505 | * @see #endFakeDrag()
2506 | */
2507 | public boolean beginFakeDrag() {
2508 | if (mIsBeingDragged) {
2509 | return false;
2510 | }
2511 | mFakeDragging = true;
2512 | setScrollState(SCROLL_STATE_DRAGGING);
2513 | mInitialMotionX = mLastMotionX = 0;
2514 | mInitialMotionY = mLastMotionY = 0;
2515 | if (mVelocityTracker == null) {
2516 | mVelocityTracker = VelocityTracker.obtain();
2517 | } else {
2518 | mVelocityTracker.clear();
2519 | }
2520 | final long time = SystemClock.uptimeMillis();
2521 | final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
2522 | mVelocityTracker.addMovement(ev);
2523 | ev.recycle();
2524 | mFakeDragBeginTime = time;
2525 | return true;
2526 | }
2527 |
2528 | /**
2529 | * End a fake drag of the pager.
2530 | *
2531 | * @see #beginFakeDrag()
2532 | * @see #fakeDragBy(float)
2533 | */
2534 | public void endFakeDrag() {
2535 | if (!mFakeDragging) {
2536 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2537 | }
2538 |
2539 | final VelocityTracker velocityTracker = mVelocityTracker;
2540 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
2541 | int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
2542 | velocityTracker, mActivePointerId);
2543 | mPopulatePending = true;
2544 | final int dimension = getClientDimension();
2545 | final int scrollCoord = getScrollCoord();
2546 | final ItemInfo ii = infoForCurrentScrollPosition();
2547 | final int currentPage = ii.position;
2548 | final float pageOffset = (((float) scrollCoord / dimension) - ii.offset) / ii.dimensionFactor;
2549 | final int totalDelta = mOrientation == HORIZONTAL ?
2550 | (int) (mLastMotionX - mInitialMotionX) :
2551 | (int) (mLastMotionY - mInitialMotionY);
2552 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
2553 | totalDelta);
2554 | setCurrentItemInternal(nextPage, true, true, initialVelocity);
2555 | endDrag();
2556 |
2557 | mFakeDragging = false;
2558 | }
2559 |
2560 | /**
2561 | * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
2562 | *
2563 | * @param xOffset Offset in pixels to drag by.
2564 | * @see #beginFakeDrag()
2565 | * @see #endFakeDrag()
2566 | */
2567 | public void fakeDragBy(float offset) {
2568 | if (!mFakeDragging) {
2569 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2570 | }
2571 |
2572 | if (mOrientation == HORIZONTAL) {
2573 | mLastMotionX += offset;
2574 | } else {
2575 | mLastMotionY += offset;
2576 | }
2577 |
2578 | float oldScrollCoord = getScrollCoord();
2579 | float scrollCoord = oldScrollCoord - offset;
2580 | final int dimension = getClientDimension();
2581 |
2582 | float startBound = dimension * mFirstOffset;
2583 | float endBound = dimension * mLastOffset;
2584 |
2585 | final ItemInfo firstItem = mItems.get(0);
2586 | final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2587 | if (firstItem.position != 0) {
2588 | startBound = firstItem.offset * dimension;
2589 | }
2590 | if (lastItem.position != mAdapter.getCount() - 1) {
2591 | endBound = lastItem.offset * dimension;
2592 | }
2593 |
2594 | if (scrollCoord < startBound) {
2595 | scrollCoord = startBound;
2596 | } else if (scrollCoord > endBound) {
2597 | scrollCoord = endBound;
2598 | }
2599 | // Don't lose the rounded component
2600 | if (mOrientation == HORIZONTAL) {
2601 | mLastMotionX += scrollCoord - (int) scrollCoord;
2602 | scrollTo((int) scrollCoord, getScrollY());
2603 | } else {
2604 | mLastMotionY += scrollCoord - (int) scrollCoord;
2605 | scrollTo(getScrollX(), (int) scrollCoord);
2606 | }
2607 |
2608 | pageScrolled((int) scrollCoord);
2609 |
2610 |
2611 | // Synthesize an event for the VelocityTracker.
2612 | final long time = SystemClock.uptimeMillis();
2613 | final MotionEvent ev = mOrientation == HORIZONTAL ?
2614 | MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, mLastMotionX, 0, 0) :
2615 | MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, 0, mLastMotionY, 0);
2616 | mVelocityTracker.addMovement(ev);
2617 | ev.recycle();
2618 | }
2619 |
2620 | /**
2621 | * Returns true if a fake drag is in progress.
2622 | *
2623 | * @return true if currently in a fake drag, false otherwise.
2624 | *
2625 | * @see #beginFakeDrag()
2626 | * @see #fakeDragBy(float)
2627 | * @see #endFakeDrag()
2628 | */
2629 | public boolean isFakeDragging() {
2630 | return mFakeDragging;
2631 | }
2632 |
2633 | private void onSecondaryPointerUp(MotionEvent ev) {
2634 | final int pointerIndex = MotionEventCompat.getActionIndex(ev);
2635 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
2636 | if (pointerId == mActivePointerId) {
2637 | // This was our active pointer going up. Choose a new
2638 | // active pointer and adjust accordingly.
2639 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
2640 | mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
2641 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
2642 | if (mVelocityTracker != null) {
2643 | mVelocityTracker.clear();
2644 | }
2645 | }
2646 | }
2647 |
2648 | private void endDrag() {
2649 | mIsBeingDragged = false;
2650 | mIsUnableToDrag = false;
2651 |
2652 | if (mVelocityTracker != null) {
2653 | mVelocityTracker.recycle();
2654 | mVelocityTracker = null;
2655 | }
2656 | }
2657 |
2658 | private void setScrollingCacheEnabled(boolean enabled) {
2659 | if (mScrollingCacheEnabled != enabled) {
2660 | mScrollingCacheEnabled = enabled;
2661 | if (USE_CACHE) {
2662 | final int size = getChildCount();
2663 | for (int i = 0; i < size; ++i) {
2664 | final View child = getChildAt(i);
2665 | if (child.getVisibility() != GONE) {
2666 | child.setDrawingCacheEnabled(enabled);
2667 | }
2668 | }
2669 | }
2670 | }
2671 | }
2672 |
2673 | public boolean canScrollHorizontally(int direction) {
2674 | if (mAdapter == null) {
2675 | return false;
2676 | }
2677 |
2678 | final int width = getClientWidth();
2679 | final int scrollX = getScrollX();
2680 | if (direction < 0) {
2681 | return (scrollX > (int) (width * mFirstOffset));
2682 | } else if (direction > 0) {
2683 | return (scrollX < (int) (width * mLastOffset));
2684 | } else {
2685 | return false;
2686 | }
2687 | }
2688 |
2689 | public boolean canScrollVertically(int direction) {
2690 | if (mAdapter == null) {
2691 | return false;
2692 | }
2693 |
2694 | final int height = getClientHeight();
2695 | final int scrollY = getScrollY();
2696 | if (direction < 0) {
2697 | return (scrollY > (int) (height * mFirstOffset));
2698 | } else if (direction > 0) {
2699 | return (scrollY < (int) (height * mLastOffset));
2700 | } else {
2701 | return false;
2702 | }
2703 | }
2704 |
2705 | /**
2706 | * Tests scrollability within child views of v given a delta of dx.
2707 | *
2708 | * @param v View to test for horizontal scrollability
2709 | * @param checkV Whether the view v passed should itself be checked for scrollability (true),
2710 | * or just its children (false).
2711 | * @param dx Delta scrolled in pixels
2712 | * @param x X coordinate of the active touch point
2713 | * @param y Y coordinate of the active touch point
2714 | * @return true if child views of v can be scrolled by delta of dx.
2715 | */
2716 | protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) {
2717 | if (v instanceof ViewGroup) {
2718 | final ViewGroup group = (ViewGroup) v;
2719 | final int scrollX = v.getScrollX();
2720 | final int scrollY = v.getScrollY();
2721 | final int count = group.getChildCount();
2722 | // Count backwards - let topmost views consume scroll distance first.
2723 | for (int i = count - 1; i >= 0; i--) {
2724 | // TODO: Add versioned support here for transformed views.
2725 | // This will not work for transformed views in Honeycomb+
2726 | final View child = group.getChildAt(i);
2727 | if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
2728 | y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
2729 | canScroll(child, true, dx, dy,
2730 | x + scrollX - child.getLeft(), y + scrollY - child.getTop())) {
2731 | return true;
2732 | }
2733 | }
2734 | }
2735 |
2736 | return checkV && ViewCompat.canScrollHorizontally(v, -dx);
2737 | }
2738 |
2739 | @Override
2740 | public boolean dispatchKeyEvent(KeyEvent event) {
2741 | // Let the focused view and/or our descendants get the key first
2742 | return super.dispatchKeyEvent(event) || executeKeyEvent(event);
2743 | }
2744 |
2745 | /**
2746 | * You can call this function yourself to have the scroll view perform
2747 | * scrolling from a key event, just as if the event had been dispatched to
2748 | * it by the view hierarchy.
2749 | *
2750 | * @param event The key event to execute.
2751 | * @return Return true if the event was handled, else false.
2752 | */
2753 | public boolean executeKeyEvent(KeyEvent event) {
2754 | boolean handled = false;
2755 | if (event.getAction() == KeyEvent.ACTION_DOWN) {
2756 | switch (event.getKeyCode()) {
2757 | case KeyEvent.KEYCODE_DPAD_LEFT:
2758 | handled = arrowScroll(FOCUS_LEFT);
2759 | break;
2760 | case KeyEvent.KEYCODE_DPAD_RIGHT:
2761 | handled = arrowScroll(FOCUS_RIGHT);
2762 | break;
2763 | case KeyEvent.KEYCODE_TAB:
2764 | if (Build.VERSION.SDK_INT >= 11) {
2765 | // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
2766 | // before Android 3.0. Ignore the tab key on those devices.
2767 | if (KeyEventCompat.hasNoModifiers(event)) {
2768 | handled = arrowScroll(FOCUS_FORWARD);
2769 | } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
2770 | handled = arrowScroll(FOCUS_BACKWARD);
2771 | }
2772 | }
2773 | break;
2774 | }
2775 | }
2776 | return handled;
2777 | }
2778 |
2779 | public boolean arrowScroll(int direction) {
2780 | View currentFocused = findFocus();
2781 | if (currentFocused == this) {
2782 | currentFocused = null;
2783 | } else if (currentFocused != null) {
2784 | boolean isChild = false;
2785 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2786 | parent = parent.getParent()) {
2787 | if (parent == this) {
2788 | isChild = true;
2789 | break;
2790 | }
2791 | }
2792 | if (!isChild) {
2793 | // This would cause the focus search down below to fail in fun ways.
2794 | final StringBuilder sb = new StringBuilder();
2795 | sb.append(currentFocused.getClass().getSimpleName());
2796 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2797 | parent = parent.getParent()) {
2798 | sb.append(" => ").append(parent.getClass().getSimpleName());
2799 | }
2800 | Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
2801 | "current focused view " + sb.toString());
2802 | currentFocused = null;
2803 | }
2804 | }
2805 |
2806 | boolean handled = false;
2807 |
2808 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
2809 | direction);
2810 | if (nextFocused != null && nextFocused != currentFocused) {
2811 | if (direction == View.FOCUS_LEFT) {
2812 | // If there is nothing to the left, or this is causing us to
2813 | // jump to the right, then what we really want to do is page left.
2814 | final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
2815 | final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
2816 | if (currentFocused != null && nextLeft >= currLeft) {
2817 | handled = pageLeft();
2818 | } else {
2819 | handled = nextFocused.requestFocus();
2820 | }
2821 | } else if (direction == View.FOCUS_RIGHT) {
2822 | // If there is nothing to the right, or this is causing us to
2823 | // jump to the left, then what we really want to do is page right.
2824 | final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
2825 | final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
2826 | if (currentFocused != null && nextLeft <= currLeft) {
2827 | handled = pageRight();
2828 | } else {
2829 | handled = nextFocused.requestFocus();
2830 | }
2831 | }
2832 | } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
2833 | // Trying to move left and nothing there; try to page.
2834 | handled = pageLeft();
2835 | } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
2836 | // Trying to move right and nothing there; try to page.
2837 | handled = pageRight();
2838 | }
2839 | if (handled) {
2840 | playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
2841 | }
2842 | return handled;
2843 | }
2844 |
2845 | private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
2846 | if (outRect == null) {
2847 | outRect = new Rect();
2848 | }
2849 | if (child == null) {
2850 | outRect.set(0, 0, 0, 0);
2851 | return outRect;
2852 | }
2853 | outRect.left = child.getLeft();
2854 | outRect.right = child.getRight();
2855 | outRect.top = child.getTop();
2856 | outRect.bottom = child.getBottom();
2857 |
2858 | ViewParent parent = child.getParent();
2859 | while (parent instanceof ViewGroup && parent != this) {
2860 | final ViewGroup group = (ViewGroup) parent;
2861 | outRect.left += group.getLeft();
2862 | outRect.right += group.getRight();
2863 | outRect.top += group.getTop();
2864 | outRect.bottom += group.getBottom();
2865 |
2866 | parent = group.getParent();
2867 | }
2868 | return outRect;
2869 | }
2870 |
2871 | boolean pageLeft() {
2872 | if (mCurItem > 0) {
2873 | setCurrentItem(mCurItem-1, true);
2874 | return true;
2875 | }
2876 | return false;
2877 | }
2878 |
2879 | boolean pageRight() {
2880 | if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) {
2881 | setCurrentItem(mCurItem+1, true);
2882 | return true;
2883 | }
2884 | return false;
2885 | }
2886 |
2887 | /**
2888 | * We only want the current page that is being shown to be focusable.
2889 | */
2890 | @Override
2891 | public void addFocusables(ArrayList