Make ViewPager aware of EdgeEffect on ICS devices.

Add EdgeEffectCompat for apps that also want to selectively show the
ICS-style edge effect.

Add ViewCompat methods for checking/changing over scroll modes on GB+

Change-Id: If0de62c389c9eaef4593f2321ee99787b13b2418
diff --git a/v4/Android.mk b/v4/Android.mk
index e35e207..7f7c74a 100644
--- a/v4/Android.mk
+++ b/v4/Android.mk
@@ -35,6 +35,15 @@
 
 # -----------------------------------------------------------------------
 
+# A helper sub-library that makes direct use of Gingerbread APIs.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v4-gingerbread
+LOCAL_SDK_VERSION := 9
+LOCAL_SRC_FILES := $(call all-java-files-under, gingerbread)
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# -----------------------------------------------------------------------
+
 # A helper sub-library that makes direct use of Honeycomb APIs.
 include $(CLEAR_VARS)
 LOCAL_MODULE := android-support-v4-honeycomb
@@ -70,6 +79,7 @@
 LOCAL_STATIC_JAVA_LIBRARIES += \
         android-support-v4-eclair \
         android-support-v4-froyo \
+        android-support-v4-gingerbread \
         android-support-v4-honeycomb \
         android-support-v4-honeycomb-mr2 \
         android-support-v4-ics
diff --git a/v4/gingerbread/android/support/v4/view/ViewCompatGingerbread.java b/v4/gingerbread/android/support/v4/view/ViewCompatGingerbread.java
new file mode 100644
index 0000000..e063778
--- /dev/null
+++ b/v4/gingerbread/android/support/v4/view/ViewCompatGingerbread.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.view;
+
+import android.view.View;
+
+public class ViewCompatGingerbread {
+    public static int getOverScrollMode(View v) {
+        return v.getOverScrollMode();
+    }
+
+    public static void setOverScrollMode(View v, int mode) {
+        v.setOverScrollMode(mode);
+    }
+}
diff --git a/v4/ics/android/support/v4/widget/EdgeEffectCompatIcs.java b/v4/ics/android/support/v4/widget/EdgeEffectCompatIcs.java
new file mode 100644
index 0000000..c02eeb4
--- /dev/null
+++ b/v4/ics/android/support/v4/widget/EdgeEffectCompatIcs.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.widget.EdgeEffect;
+
+/**
+ * Stub implementation that contains a real EdgeEffect on ICS.
+ *
+ * This class is an implementation detail for EdgeEffectCompat
+ * and should not be used directly.
+ */
+class EdgeEffectCompatIcs {
+    public static Object newEdgeEffect(Context context) {
+        return new EdgeEffect(context);
+    }
+
+    public static void setSize(Object edgeEffect, int width, int height) {
+        ((EdgeEffect) edgeEffect).setSize(width, height);
+    }
+
+    public static boolean isFinished(Object edgeEffect) {
+        return ((EdgeEffect) edgeEffect).isFinished();
+    }
+
+    public static void finish(Object edgeEffect) {
+        ((EdgeEffect) edgeEffect).finish();
+    }
+
+    public static boolean onPull(Object edgeEffect, float deltaDistance) {
+        ((EdgeEffect) edgeEffect).onPull(deltaDistance);
+        return true;
+    }
+
+    public static boolean onRelease(Object edgeEffect) {
+        EdgeEffect eff = (EdgeEffect) edgeEffect;
+        eff.onRelease();
+        return eff.isFinished();
+    }
+
+    public static boolean onAbsorb(Object edgeEffect, int velocity) {
+        ((EdgeEffect) edgeEffect).onAbsorb(velocity);
+        return true;
+    }
+
+    public static boolean draw(Object edgeEffect, Canvas canvas) {
+        return ((EdgeEffect) edgeEffect).draw(canvas);
+    }
+}
\ No newline at end of file
diff --git a/v4/java/android/support/v4/view/ViewCompat.java b/v4/java/android/support/v4/view/ViewCompat.java
index 827d5f2..582e61f 100644
--- a/v4/java/android/support/v4/view/ViewCompat.java
+++ b/v4/java/android/support/v4/view/ViewCompat.java
@@ -22,9 +22,28 @@
  * Helper for accessing newer features in View.
  */
 public class ViewCompat {
+    /**
+     * Always allow a user to over-scroll this view, provided it is a
+     * view that can scroll.
+     */
+    public static final int OVER_SCROLL_ALWAYS = 0;
+
+    /**
+     * Allow a user to over-scroll this view only if the content is large
+     * enough to meaningfully scroll, provided it is a view that can scroll.
+     */
+    public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1;
+
+    /**
+     * Never allow a user to over-scroll this view.
+     */
+    public static final int OVER_SCROLL_NEVER = 2;
+
     interface ViewCompatImpl {
         public boolean canScrollHorizontally(View v, int direction);
         public boolean canScrollVertically(View v, int direction);
+        public int getOverScrollMode(View v);
+        public void setOverScrollMode(View v, int mode);
     }
 
     static class BaseViewCompatImpl implements ViewCompatImpl {
@@ -34,9 +53,24 @@
         public boolean canScrollVertically(View v, int direction) {
             return false;
         }
+        public int getOverScrollMode(View v) {
+            return OVER_SCROLL_NEVER;
+        }
+        public void setOverScrollMode(View v, int mode) {
+            // Do nothing; API doesn't exist
+        }
     }
 
-    static class ICSViewCompatImpl implements ViewCompatImpl {
+    static class GBViewCompatImpl extends BaseViewCompatImpl {
+        public int getOverScrollMode(View v) {
+            return ViewCompatGingerbread.getOverScrollMode(v);
+        }
+        public void setOverScrollMode(View v, int mode) {
+            ViewCompatGingerbread.setOverScrollMode(v, mode);
+        }
+    }
+
+    static class ICSViewCompatImpl extends GBViewCompatImpl {
         public boolean canScrollHorizontally(View v, int direction) {
             return ViewCompatICS.canScrollHorizontally(v, direction);
         }
@@ -47,8 +81,11 @@
 
     static final ViewCompatImpl IMPL;
     static {
-        if (android.os.Build.VERSION.SDK_INT >= 14) {
+        final int version = android.os.Build.VERSION.SDK_INT;
+        if (version >= 14) {
             IMPL = new ICSViewCompatImpl();
+        } else if (version >= 9) {
+            IMPL = new GBViewCompatImpl();
         } else {
             IMPL = new BaseViewCompatImpl();
         }
@@ -61,4 +98,12 @@
     public static boolean canScrollVertically(View v, int direction) {
         return IMPL.canScrollVertically(v, direction);
     }
+
+    public static int getOverScrollMode(View v) {
+        return IMPL.getOverScrollMode(v);
+    }
+
+    public static void setOverScrollMode(View v, int mode) {
+        IMPL.setOverScrollMode(v, mode);
+    }
 }
diff --git a/v4/java/android/support/v4/view/ViewPager.java b/v4/java/android/support/v4/view/ViewPager.java
index 10767f9..e38b257 100644
--- a/v4/java/android/support/v4/view/ViewPager.java
+++ b/v4/java/android/support/v4/view/ViewPager.java
@@ -17,12 +17,14 @@
 package android.support.v4.view;
 
 import android.content.Context;
+import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SystemClock;
 import android.support.v4.os.ParcelableCompat;
 import android.support.v4.os.ParcelableCompatCreatorCallbacks;
+import android.support.v4.widget.EdgeEffectCompat;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.FocusFinder;
@@ -118,6 +120,9 @@
     private boolean mFakeDragging;
     private long mFakeDragBeginTime;
 
+    private EdgeEffectCompat mLeftEdge;
+    private EdgeEffectCompat mRightEdge;
+
     private boolean mFirstLayout = true;
 
     private OnPageChangeListener mOnPageChangeListener;
@@ -213,11 +218,14 @@
         setWillNotDraw(false);
         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
         setFocusable(true);
-        mScroller = new Scroller(getContext());
+        final Context context = getContext();
+        mScroller = new Scroller(context);
         final ViewConfiguration configuration = ViewConfiguration.get(getContext());
         mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mLeftEdge = new EdgeEffectCompat(context);
+        mRightEdge = new EdgeEffectCompat(context);
     }
 
     private void setScrollState(int newState) {
@@ -869,7 +877,7 @@
                         scrollX >= (mAdapter.getCount() - 1) * getWidth() - 1);
                 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
 
-                if (atEdge || canScroll(this, false, (int) dx, (int) x, (int) y)) {
+                if (canScroll(this, false, (int) dx, (int) x, (int) y)) {
                     // Nested view has scrollable area under this point. Let it be handled there.
                     mInitialMotionX = mLastMotionX = x;
                     mLastMotionY = y;
@@ -958,6 +966,7 @@
         mVelocityTracker.addMovement(ev);
 
         final int action = ev.getAction();
+        boolean needsInvalidate = false;
 
         switch (action & MotionEventCompat.ACTION_MASK) {
             case MotionEvent.ACTION_DOWN: {
@@ -995,15 +1004,24 @@
                     final float x = MotionEventCompat.getX(ev, activePointerIndex);
                     final float deltaX = mLastMotionX - x;
                     mLastMotionX = x;
-                    float scrollX = getScrollX() + deltaX;
+                    float oldScrollX = getScrollX();
+                    float scrollX = oldScrollX + deltaX;
                     final int width = getWidth();
 
+                    final int lastItemIndex = mAdapter.getCount() - 1;
                     final float leftBound = Math.max(0, (mCurItem - 1) * width);
-                    final float rightBound =
-                            Math.min(mCurItem + 1, mAdapter.getCount() - 1) * width;
+                    final float rightBound = Math.min(mCurItem + 1, lastItemIndex) * width;
                     if (scrollX < leftBound) {
+                        if (leftBound == 0) {
+                            float over = -scrollX;
+                            needsInvalidate = mLeftEdge.onPull(over / width);
+                        }
                         scrollX = leftBound;
                     } else if (scrollX > rightBound) {
+                        if (rightBound == lastItemIndex * width) {
+                            float over = scrollX - rightBound;
+                            needsInvalidate = mRightEdge.onPull(over / width);
+                        }
                         scrollX = rightBound;
                     }
                     // Don't lose the rounded component
@@ -1038,6 +1056,7 @@
 
                     mActivePointerId = INVALID_POINTER;
                     endDrag();
+                    needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
                 }
                 break;
             case MotionEvent.ACTION_CANCEL:
@@ -1045,6 +1064,7 @@
                     setCurrentItemInternal(mCurItem, true, true);
                     mActivePointerId = INVALID_POINTER;
                     endDrag();
+                    needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
                 }
                 break;
             case MotionEventCompat.ACTION_POINTER_DOWN: {
@@ -1060,9 +1080,54 @@
                         MotionEventCompat.findPointerIndex(ev, mActivePointerId));
                 break;
         }
+        if (needsInvalidate) {
+            invalidate();
+        }
         return true;
     }
 
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+        boolean needsInvalidate = false;
+
+        final int overScrollMode = ViewCompat.getOverScrollMode(this);
+        if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+                (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
+                        mAdapter != null && mAdapter.getCount() > 1)) {
+            if (!mLeftEdge.isFinished()) {
+                final int restoreCount = canvas.save();
+                final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+                canvas.rotate(270);
+                canvas.translate(-height + getPaddingTop(), 0);
+                mLeftEdge.setSize(height, getWidth());
+                needsInvalidate |= mLeftEdge.draw(canvas);
+                canvas.restoreToCount(restoreCount);
+            }
+            if (!mRightEdge.isFinished()) {
+                final int restoreCount = canvas.save();
+                final int width = getWidth();
+                final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+                final int itemCount = mAdapter != null ? mAdapter.getCount() : 1;
+
+                canvas.rotate(90);
+                canvas.translate(-getPaddingTop(), -itemCount * width);
+                mRightEdge.setSize(height, width);
+                needsInvalidate |= mRightEdge.draw(canvas);
+                canvas.restoreToCount(restoreCount);
+            }
+        } else {
+            mLeftEdge.finish();
+            mRightEdge.finish();
+        }
+
+        if (needsInvalidate) {
+            // Keep animating
+            invalidate();
+        }
+    }
+
     /**
      * Start a fake drag of the pager.
      *
diff --git a/v4/java/android/support/v4/widget/EdgeEffectCompat.java b/v4/java/android/support/v4/widget/EdgeEffectCompat.java
new file mode 100644
index 0000000..0d12d81
--- /dev/null
+++ b/v4/java/android/support/v4/widget/EdgeEffectCompat.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Build;
+
+/**
+ * Helper for accessing EdgeEffects from newer platform versions.
+ *
+ * This class is used to access {@link android.widget.EdgeEffect} on platform versions
+ * that support it. When running on older platforms it will result in no-ops. It should
+ * be used by views that wish to use the standard Android visual effects at the edges
+ * of scrolling containers.
+ */
+public class EdgeEffectCompat {
+    private Object mEdgeEffect;
+
+    private static final EdgeEffectImpl IMPL;
+
+    static {
+        if (Build.VERSION.SDK_INT >= 14) { // ICS
+            IMPL = new EdgeEffectIcsImpl();
+        } else {
+            IMPL = new BaseEdgeEffectImpl();
+        }
+    }
+
+    interface EdgeEffectImpl {
+        public Object newEdgeEffect(Context context);
+        public void setSize(Object edgeEffect, int width, int height);
+        public boolean isFinished(Object edgeEffect);
+        public void finish(Object edgeEffect);
+        public boolean onPull(Object edgeEffect, float deltaDistance);
+        public boolean onRelease(Object edgeEffect);
+        public boolean onAbsorb(Object edgeEffect, int velocity);
+        public boolean draw(Object edgeEffect, Canvas canvas);
+    }
+
+    /**
+     * Null implementation to use pre-ICS
+     */
+    static class BaseEdgeEffectImpl implements EdgeEffectImpl {
+        public Object newEdgeEffect(Context context) {
+            return null;
+        }
+
+        public void setSize(Object edgeEffect, int width, int height) {
+        }
+
+        public boolean isFinished(Object edgeEffect) {
+            return true;
+        }
+
+        public void finish(Object edgeEffect) {
+        }
+
+        public boolean onPull(Object edgeEffect, float deltaDistance) {
+            return false;
+        }
+
+        public boolean onRelease(Object edgeEffect) {
+            return false;
+        }
+
+        public boolean onAbsorb(Object edgeEffect, int velocity) {
+            return false;
+        }
+
+        public boolean draw(Object edgeEffect, Canvas canvas) {
+            return false;
+        }
+    }
+
+    static class EdgeEffectIcsImpl implements EdgeEffectImpl {
+        public Object newEdgeEffect(Context context) {
+            return EdgeEffectCompatIcs.newEdgeEffect(context);
+        }
+
+        public void setSize(Object edgeEffect, int width, int height) {
+            EdgeEffectCompatIcs.setSize(edgeEffect, width, height);
+        }
+
+        public boolean isFinished(Object edgeEffect) {
+            return EdgeEffectCompatIcs.isFinished(edgeEffect);
+        }
+
+        public void finish(Object edgeEffect) {
+            EdgeEffectCompatIcs.finish(edgeEffect);
+        }
+
+        public boolean onPull(Object edgeEffect, float deltaDistance) {
+            return EdgeEffectCompatIcs.onPull(edgeEffect, deltaDistance);
+        }
+
+        public boolean onRelease(Object edgeEffect) {
+            return EdgeEffectCompatIcs.onRelease(edgeEffect);
+        }
+
+        public boolean onAbsorb(Object edgeEffect, int velocity) {
+            return EdgeEffectCompatIcs.onAbsorb(edgeEffect, velocity);
+        }
+
+        public boolean draw(Object edgeEffect, Canvas canvas) {
+            return EdgeEffectCompatIcs.draw(edgeEffect, canvas);
+        }
+    }
+
+    /**
+     * Construct a new EdgeEffect themed using the given context.
+     *
+     * <p>Note: On platform versions that do not support EdgeEffect, all operations
+     * on the newly constructed object will be mocked/no-ops.</p>
+     *
+     * @param context Context to use for theming the effect
+     */
+    public EdgeEffectCompat(Context context) {
+        mEdgeEffect = IMPL.newEdgeEffect(context);
+    }
+
+    /**
+     * Set the size of this edge effect in pixels.
+     *
+     * @param width Effect width in pixels
+     * @param height Effect height in pixels
+     */
+    public void setSize(int width, int height) {
+        IMPL.setSize(mEdgeEffect, width, height);
+    }
+
+    /**
+     * Reports if this EdgeEffectCompat's animation is finished. If this method returns false
+     * after a call to {@link #draw(Canvas)} the host widget should schedule another
+     * drawing pass to continue the animation.
+     *
+     * @return true if animation is finished, false if drawing should continue on the next frame.
+     */
+    public boolean isFinished() {
+        return IMPL.isFinished(mEdgeEffect);
+    }
+
+    /**
+     * Immediately finish the current animation.
+     * After this call {@link #isFinished()} will return true.
+     */
+    public void finish() {
+        IMPL.finish(mEdgeEffect);
+    }
+
+    /**
+     * A view should call this when content is pulled away from an edge by the user.
+     * This will update the state of the current visual effect and its associated animation.
+     * The host view should always {@link android.view.View#invalidate()} if this method
+     * returns true and draw the results accordingly.
+     *
+     * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+     *                      1.f (full length of the view) or negative values to express change
+     *                      back toward the edge reached to initiate the effect.
+     * @return true if the host view should call invalidate, false if it should not.
+     */
+    public boolean onPull(float deltaDistance) {
+        return IMPL.onPull(mEdgeEffect, deltaDistance);
+    }
+
+    /**
+     * Call when the object is released after being pulled.
+     * This will begin the "decay" phase of the effect. After calling this method
+     * the host view should {@link android.view.View#invalidate()} if this method
+     * returns true and thereby draw the results accordingly.
+     *
+     * @return true if the host view should invalidate, false if it should not.
+     */
+    public boolean onRelease() {
+        return IMPL.onRelease(mEdgeEffect);
+    }
+
+    /**
+     * Call when the effect absorbs an impact at the given velocity.
+     * Used when a fling reaches the scroll boundary.
+     *
+     * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
+     * the method <code>getCurrVelocity</code> will provide a reasonable approximation
+     * to use here.</p>
+     *
+     * @param velocity Velocity at impact in pixels per second.
+     * @return true if the host view should invalidate, false if it should not.
+     */
+    public boolean onAbsorb(int velocity) {
+        return IMPL.onAbsorb(mEdgeEffect, velocity);
+    }
+
+    /**
+     * Draw into the provided canvas. Assumes that the canvas has been rotated
+     * accordingly and the size has been set. The effect will be drawn the full
+     * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
+     * 1.f of height.
+     *
+     * @param canvas Canvas to draw into
+     * @return true if drawing should continue beyond this frame to continue the
+     *         animation
+     */
+    public boolean draw(Canvas canvas) {
+        return IMPL.draw(mEdgeEffect, canvas);
+    }
+}