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);
+ }
+}