OSDN Git Service

Added OverScroller and overscroll effects for ScrollView and HorizontalScrollView.
authorAdam Powell <adamp@google.com>
Tue, 26 Jan 2010 02:38:22 +0000 (18:38 -0800)
committerAdam Powell <adamp@google.com>
Thu, 28 Jan 2010 05:15:58 +0000 (21:15 -0800)
core/java/android/widget/HorizontalScrollView.java
core/java/android/widget/OverScroller.java [new file with mode: 0644]
core/java/android/widget/ScrollView.java

index 52f56a7..4cc3b9e 100644 (file)
@@ -63,7 +63,7 @@ public class HorizontalScrollView extends FrameLayout {
     private long mLastScroll;
 
     private final Rect mTempRect = new Rect();
-    private Scroller mScroller;
+    private OverScroller mScroller;
 
     /**
      * Flag to indicate that we are moving focus ourselves. This is so the
@@ -177,7 +177,7 @@ public class HorizontalScrollView extends FrameLayout {
 
 
     private void initScrollView() {
-        mScroller = new Scroller(getContext());
+        mScroller = new OverScroller(getContext());
         setFocusable(true);
         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
         setWillNotDraw(false);
@@ -380,11 +380,6 @@ public class HorizontalScrollView extends FrameLayout {
             return true;
         }
 
-        if (!canScroll()) {
-            mIsBeingDragged = false;
-            return false;
-        }
-
         final float x = ev.getX();
 
         switch (action) {
@@ -440,10 +435,6 @@ public class HorizontalScrollView extends FrameLayout {
             return false;
         }
 
-        if (!canScroll()) {
-            return false;
-        }
-
         if (mVelocityTracker == null) {
             mVelocityTracker = VelocityTracker.obtain();
         }
@@ -470,25 +461,23 @@ public class HorizontalScrollView extends FrameLayout {
                 final int deltaX = (int) (mLastMotionX - x);
                 mLastMotionX = x;
 
-                if (deltaX < 0) {
-                    if (mScrollX > 0) {
-                        scrollBy(deltaX, 0);
-                    }
-                } else if (deltaX > 0) {
-                    final int rightEdge = getWidth() - mPaddingRight;
-                    final int availableToScroll = getChildAt(0).getRight() - mScrollX - rightEdge;
-                    if (availableToScroll > 0) {
-                        scrollBy(Math.min(availableToScroll, deltaX), 0);
-                    }
-                }
+                super.scrollTo(mScrollX + deltaX, mScrollY);
                 break;
             case MotionEvent.ACTION_UP:
                 final VelocityTracker velocityTracker = mVelocityTracker;
                 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                 int initialVelocity = (int) velocityTracker.getXVelocity();
 
-                if ((Math.abs(initialVelocity) > mMinimumVelocity) && getChildCount() > 0) {
-                    fling(-initialVelocity);
+                if (getChildCount() > 0) {
+                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+                        fling(-initialVelocity);
+                    } else {
+                        final int right = Math.max(0, getChildAt(0).getHeight() - 
+                                (getHeight() - mPaddingRight - mPaddingLeft));
+                        if (mScroller.springback(mScrollX, mScrollY, 0, 0, right, 0)) {
+                            invalidate();
+                        }
+                    }
                 }
 
                 if (mVelocityTracker != null) {
@@ -913,14 +902,10 @@ public class HorizontalScrollView extends FrameLayout {
             int oldY = mScrollY;
             int x = mScroller.getCurrX();
             int y = mScroller.getCurrY();
-            if (getChildCount() > 0) {
-                View child = getChildAt(0);
-                mScrollX = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
-                mScrollY = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
-            } else {
-                mScrollX = x;
-                mScrollY = y;
-            }
+
+            mScrollX = x;
+            mScrollY = y;
+
             if (oldX != mScrollX || oldY != mScrollY) {
                 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
             }
@@ -1156,7 +1141,8 @@ public class HorizontalScrollView extends FrameLayout {
             int width = getWidth() - mPaddingRight - mPaddingLeft;
             int right = getChildAt(0).getWidth();
     
-            mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0, right - width, 0, 0);
+            mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0, 
+                    Math.max(0, right - width), 0, 0, width/2, 0);
     
             final boolean movingRight = velocityX > 0;
     
diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java
new file mode 100644 (file)
index 0000000..3fd5dcc
--- /dev/null
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2006 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.widget;
+
+import android.content.Context;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+
+/**
+ * This class encapsulates scrolling with the ability to overshoot the bounds
+ * of a scrolling operation. This class attempts to be a drop-in replacement
+ * for {@link android.widget.Scroller} in most cases.
+ * 
+ * @hide Pending API approval
+ */
+public class OverScroller {
+    private static final int SPRINGBACK_DURATION = 150;
+    private static final int OVERFLING_DURATION = 150;
+    
+    private static final int MODE_DEFAULT = 0;
+    private static final int MODE_OVERFLING = 1;
+    private static final int MODE_SPRINGBACK = 2;
+    
+    private Scroller mDefaultScroller;
+    private Scroller mDecelScroller;
+    private Scroller mAccelDecelScroller;
+    private Scroller mCurrScroller;
+    
+    private int mScrollMode = MODE_DEFAULT;
+    
+    private int mMinimumX;
+    private int mMinimumY;
+    private int mMaximumX;
+    private int mMaximumY;
+    
+    public OverScroller(Context context) {
+        mDefaultScroller = new Scroller(context);
+        mDecelScroller = new Scroller(context, new DecelerateInterpolator(3.f));
+        mAccelDecelScroller = new Scroller(context, new AccelerateDecelerateInterpolator());
+        mCurrScroller = mDefaultScroller;
+    }
+    
+    /**
+     * Call this when you want to know the new location.  If it returns true,
+     * the animation is not yet finished.  loc will be altered to provide the
+     * new location.
+     */ 
+    public boolean computeScrollOffset() {
+        boolean inProgress = mCurrScroller.computeScrollOffset();
+        
+        switch (mScrollMode) {
+        case MODE_OVERFLING:
+            if (!inProgress) {
+                // Overfling ended
+                if (springback(mCurrScroller.getCurrX(), mCurrScroller.getCurrY(),
+                        mMinimumX, mMaximumX, mMinimumY, mMaximumY, mAccelDecelScroller)) {
+                    return mCurrScroller.computeScrollOffset();
+                } else {
+                    mCurrScroller = mDefaultScroller;
+                    mScrollMode = MODE_DEFAULT;
+                }
+            }
+            break;
+            
+        case MODE_SPRINGBACK:
+            if (!inProgress) {
+                mCurrScroller = mDefaultScroller;
+                mScrollMode = MODE_DEFAULT;
+            }
+            break;
+            
+        case MODE_DEFAULT:
+            // Fling/autoscroll - did we go off the edge?
+            if (inProgress) {
+                Scroller scroller = mCurrScroller;
+                final int x = scroller.getCurrX();
+                final int y = scroller.getCurrY();
+                final int minX = mMinimumX;
+                final int maxX = mMaximumX;
+                final int minY = mMinimumY;
+                final int maxY = mMaximumY;
+                if (x < minX || x > maxX || y < minY || y > maxY) {
+                    final int startx = scroller.getStartX();
+                    final int starty = scroller.getStartY();
+                    final int time = scroller.timePassed();
+                    final float timeSecs = time / 1000.f;
+                    final float xvel = ((x - startx) / timeSecs);
+                    final float yvel = ((y - starty) / timeSecs);
+                    
+                    if ((x < minX && xvel > 0) || (y < minY && yvel > 0) ||
+                            (x > maxX && xvel < 0) || (y > maxY && yvel < 0)) {
+                        // If our velocity would take us back into valid areas,
+                        // try to springback rather than overfling.
+                        if (springback(x, y, minX, maxX, minY, maxY)) {
+                            return mCurrScroller.computeScrollOffset();
+                        }
+                    } else {
+                        overfling(x, y, xvel, yvel);
+                        return mCurrScroller.computeScrollOffset();
+                    }
+                }
+            }
+            break;
+        }
+        
+        return inProgress;
+    }
+    
+    private void overfling(int startx, int starty, float xvel, float yvel) {
+        Scroller scroller = mDecelScroller;
+        final float durationSecs = (OVERFLING_DURATION / 1000.f);
+        int dx = (int)(xvel * durationSecs) / 8;
+        int dy = (int)(yvel * durationSecs) / 8;
+        scroller.startScroll(startx, starty, dx, dy, OVERFLING_DURATION);
+        mCurrScroller.abortAnimation();
+        mCurrScroller = scroller;
+        mScrollMode = MODE_OVERFLING;
+    }
+    
+    /**
+     * Call this when you want to 'spring back' into a valid coordinate range.
+     *
+     * @param startX Starting X coordinate
+     * @param startY Starting Y coordinate
+     * @param minX Minimum valid X value
+     * @param maxX Maximum valid X value
+     * @param minY Minimum valid Y value
+     * @param maxY Minimum valid Y value
+     * @return true if a springback was initiated, false if startX/startY was
+     *          already within the valid range.
+     */
+    public boolean springback(int startX, int startY, int minX, int maxX,
+            int minY, int maxY) {
+        return springback(startX, startY, minX, maxX, minY, maxY, mDecelScroller);
+    }
+    
+    private boolean springback(int startX, int startY, int minX, int maxX,
+            int minY, int maxY, Scroller scroller) {
+        int xoff = 0;
+        int yoff = 0;
+        if (startX < minX) {
+            xoff = minX - startX;
+        } else if (startX > maxX) {
+            xoff = maxX - startX;
+        }
+        if (startY < minY) {
+            yoff = minY - startY;
+        } else if (startY > maxY) {
+            yoff = maxY - startY;
+        }
+        
+        if (xoff != 0 || yoff != 0) {
+            scroller.startScroll(startX, startY, xoff, yoff, SPRINGBACK_DURATION);
+            mCurrScroller.abortAnimation();
+            mCurrScroller = scroller;
+            mScrollMode = MODE_SPRINGBACK;
+            return true;
+        }
+        
+        return false;
+    }
+
+    /**
+     * 
+     * Returns whether the scroller has finished scrolling.
+     * 
+     * @return True if the scroller has finished scrolling, false otherwise.
+     */
+    public final boolean isFinished() {
+        return mCurrScroller.isFinished();
+    }
+
+    /**
+     * Returns the current X offset in the scroll. 
+     * 
+     * @return The new X offset as an absolute distance from the origin.
+     */
+    public final int getCurrX() {
+        return mCurrScroller.getCurrX();
+    }
+    
+    /**
+     * Returns the current Y offset in the scroll. 
+     * 
+     * @return The new Y offset as an absolute distance from the origin.
+     */
+    public final int getCurrY() {
+        return mCurrScroller.getCurrY();
+    }
+    
+    /**
+     * Stops the animation, resets any springback/overfling and completes
+     * any standard flings/scrolls in progress.
+     */
+    public void abortAnimation() {
+        mCurrScroller.abortAnimation();
+        mCurrScroller = mDefaultScroller;
+        mScrollMode = MODE_DEFAULT;
+        mCurrScroller.abortAnimation();
+    }
+    
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     * The scroll will use the default value of 250 milliseconds for the
+     * duration.
+     * 
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy) {
+        mCurrScroller.abortAnimation();
+        mCurrScroller = mDefaultScroller;
+        mScrollMode = MODE_DEFAULT;
+        mMinimumX = Math.min(startX, startX + dx);
+        mMinimumY = Math.min(startY, startY + dy);
+        mMaximumX = Math.max(startX, startX + dx);
+        mMaximumY = Math.max(startY, startY + dy);
+        mCurrScroller.startScroll(startX, startY, dx, dy);
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     * 
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     * @param duration Duration of the scroll in milliseconds.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+        mCurrScroller.abortAnimation();
+        mCurrScroller = mDefaultScroller;
+        mScrollMode = MODE_DEFAULT;
+        mMinimumX = Math.min(startX, startX + dx);
+        mMinimumY = Math.min(startY, startY + dy);
+        mMaximumX = Math.max(startX, startX + dx);
+        mMaximumY = Math.max(startY, startY + dy);
+        mCurrScroller.startScroll(startX, startY, dx, dy, duration);
+    }
+    
+    /**
+     * Returns the duration of the active scroll in progress; standard, fling,
+     * springback, or overfling. Does not account for any overflings or springback
+     * that may result.
+     */
+    public int getDuration() {
+        return mCurrScroller.getDuration();
+    }
+
+    /**
+     * Start scrolling based on a fling gesture. The distance travelled will
+     * depend on the initial velocity of the fling.
+     * 
+     * @param startX Starting point of the scroll (X)
+     * @param startY Starting point of the scroll (Y)
+     * @param velocityX Initial velocity of the fling (X) measured in pixels per
+     *        second.
+     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
+     *        second
+     * @param minX Minimum X value. The scroller will not scroll past this
+     *        point.
+     * @param maxX Maximum X value. The scroller will not scroll past this
+     *        point.
+     * @param minY Minimum Y value. The scroller will not scroll past this
+     *        point.
+     * @param maxY Maximum Y value. The scroller will not scroll past this
+     *        point.
+     */
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY) {
+        this.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
+    }
+
+    /**
+     * Start scrolling based on a fling gesture. The distance travelled will
+     * depend on the initial velocity of the fling.
+     * 
+     * @param startX Starting point of the scroll (X)
+     * @param startY Starting point of the scroll (Y)
+     * @param velocityX Initial velocity of the fling (X) measured in pixels per
+     *        second.
+     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
+     *        second
+     * @param minX Minimum X value. The scroller will not scroll past this
+     *        point unless overX > 0. If overfling is allowed, it will use minX
+     *        as a springback boundary.
+     * @param maxX Maximum X value. The scroller will not scroll past this
+     *        point unless overX > 0. If overfling is allowed, it will use maxX
+     *        as a springback boundary.
+     * @param minY Minimum Y value. The scroller will not scroll past this
+     *        point unless overY > 0. If overfling is allowed, it will use minY
+     *        as a springback boundary.
+     * @param maxY Maximum Y value. The scroller will not scroll past this
+     *        point unless overY > 0. If overfling is allowed, it will use maxY
+     *        as a springback boundary.
+     * @param overX Overfling range. If > 0, horizontal overfling in either
+     *        direction will be possible.
+     * @param overY Overfling range. If > 0, vertical overfling in either
+     *        direction will be possible.
+     */
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY, int overX, int overY) {
+        mCurrScroller = mDefaultScroller;
+        mScrollMode = MODE_DEFAULT;
+        mMinimumX = minX;
+        mMaximumX = maxX;
+        mMinimumY = minY;
+        mMaximumY = maxY;
+        mCurrScroller.fling(startX, startY, velocityX, velocityY, 
+                minX - overX, maxX + overX, minY - overY, maxY + overY);
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     * 
+     * @return The final X offset as an absolute distance from the origin.
+     */
+    public int getFinalX() {
+        return mCurrScroller.getFinalX();
+    }
+    
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     * 
+     * @return The final Y offset as an absolute distance from the origin.
+     */
+    public int getFinalY() {
+        return mCurrScroller.getFinalY();
+    }
+}
index bf16e28..62797f3 100644 (file)
@@ -16,6 +16,8 @@
 
 package android.widget;
 
+import com.android.internal.R;
+
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Rect;
@@ -30,8 +32,6 @@ import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.view.animation.AnimationUtils;
 
-import com.android.internal.R;
-
 import java.util.List;
 
 /**
@@ -59,7 +59,7 @@ public class ScrollView extends FrameLayout {
     private long mLastScroll;
 
     private final Rect mTempRect = new Rect();
-    private Scroller mScroller;
+    private OverScroller mScroller;
 
     /**
      * Flag to indicate that we are moving focus ourselves. This is so the
@@ -173,7 +173,7 @@ public class ScrollView extends FrameLayout {
 
 
     private void initScrollView() {
-        mScroller = new Scroller(getContext());
+        mScroller = new OverScroller(getContext());
         setFocusable(true);
         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
         setWillNotDraw(false);
@@ -378,11 +378,6 @@ public class ScrollView extends FrameLayout {
             return true;
         }
 
-        if (!canScroll()) {
-            mIsBeingDragged = false;
-            return false;
-        }
-
         final float y = ev.getY();
 
         switch (action) {
@@ -437,10 +432,6 @@ public class ScrollView extends FrameLayout {
             // descendants.
             return false;
         }
-        
-        if (!canScroll()) {
-            return false;
-        }
 
         if (mVelocityTracker == null) {
             mVelocityTracker = VelocityTracker.obtain();
@@ -468,25 +459,23 @@ public class ScrollView extends FrameLayout {
                 final int deltaY = (int) (mLastMotionY - y);
                 mLastMotionY = y;
 
-                if (deltaY < 0) {
-                    if (mScrollY > 0) {
-                        scrollBy(0, deltaY);
-                    }
-                } else if (deltaY > 0) {
-                    final int bottomEdge = getHeight() - mPaddingBottom;
-                    final int availableToScroll = getChildAt(0).getBottom() - mScrollY - bottomEdge;
-                    if (availableToScroll > 0) {
-                        scrollBy(0, Math.min(availableToScroll, deltaY));
-                    }
-                }
+                super.scrollTo(mScrollX, mScrollY + deltaY);
                 break;
             case MotionEvent.ACTION_UP:
                 final VelocityTracker velocityTracker = mVelocityTracker;
                 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                 int initialVelocity = (int) velocityTracker.getYVelocity();
 
-                if ((Math.abs(initialVelocity) > mMinimumVelocity) && getChildCount() > 0) {
-                    fling(-initialVelocity);
+                if (getChildCount() > 0) {
+                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+                        fling(-initialVelocity);
+                    } else {
+                        final int bottom = Math.max(0, getChildAt(0).getHeight() - 
+                                (getHeight() - mPaddingBottom - mPaddingTop));
+                        if (mScroller.springback(mScrollX, mScrollY, 0, 0, 0, bottom)) {
+                            invalidate();
+                        }
+                    }
                 }
 
                 if (mVelocityTracker != null) {
@@ -915,14 +904,10 @@ public class ScrollView extends FrameLayout {
             int oldY = mScrollY;
             int x = mScroller.getCurrX();
             int y = mScroller.getCurrY();
-            if (getChildCount() > 0) {
-                View child = getChildAt(0);
-                mScrollX = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
-                mScrollY = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
-            } else {
-                mScrollX = x;
-                mScrollY = y;
-            }            
+
+            mScrollX = x;
+            mScrollY = y;
+
             if (oldX != mScrollX || oldY != mScrollY) {
                 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
             }
@@ -1159,7 +1144,8 @@ public class ScrollView extends FrameLayout {
             int height = getHeight() - mPaddingBottom - mPaddingTop;
             int bottom = getChildAt(0).getHeight();
     
-            mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, bottom - height);
+            mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, 
+                    Math.max(0, bottom - height), 0, height/2);
     
             final boolean movingDown = velocityY > 0;