Merge "Use autoscroller to handle drag scrolling" into jb-ub-gel-agar
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 231b776..3fe53d2 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -102,9 +102,6 @@
     <!--  When dragging an item, how much bigger (fixed dps) the dragged view
           should be. If 0, it will not be scaled at all. -->
     <dimen name="dragViewScale">12dp</dimen>
-    <dimen name="min_scroll_band_size">48dp</dimen>
-    <integer name="scroll_band_factor">20</integer>
-    <integer name="scroll_band_max_factor">30</integer>
 
     <!-- Padding applied to AppWidget previews -->
     <dimen name="app_widget_preview_padding_left">16dp</dimen>
diff --git a/src/com/android/launcher3/AutoScroller.java b/src/com/android/launcher3/AutoScroller.java
new file mode 100644
index 0000000..ac8e2e6
--- /dev/null
+++ b/src/com/android/launcher3/AutoScroller.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2013 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 com.android.launcher3;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.AbsListView;
+
+class AutoScroller implements View.OnTouchListener, Runnable {
+    private static final int SCALE_RELATIVE = 0;
+    private static final int SCALE_ABSOLUTE = 1;
+
+    private final View mTarget;
+    private final RampUpScroller mScroller;
+
+    /** Interpolator used to scale velocity with touch position, may be null. */
+    private Interpolator mEdgeInterpolator = new AccelerateInterpolator();
+
+    /**
+     * Type of maximum velocity scaling to use, one of:
+     * <ul>
+     * <li>{@link #SCALE_RELATIVE}
+     * <li>{@link #SCALE_ABSOLUTE}
+     * </ul>
+     */
+    private int mMaxVelocityScale = SCALE_RELATIVE;
+
+    /**
+     * Type of activation edge scaling to use, one of:
+     * <ul>
+     * <li>{@link #SCALE_RELATIVE}
+     * <li>{@link #SCALE_ABSOLUTE}
+     * </ul>
+     */
+    private int mActivationEdgeScale = SCALE_RELATIVE;
+
+    /** Edge insets used to activate auto-scrolling. */
+    private RectF mActivationEdges = new RectF(0.2f, 0.2f, 0.2f, 0.2f);
+
+    /** Delay after entering an activation edge before auto-scrolling begins. */
+    private int mActivationDelay;
+
+    /** Maximum horizontal scrolling velocity. */
+    private float mMaxVelocityX = 0.001f;
+
+    /** Maximum vertical scrolling velocity. */
+    private float mMaxVelocityY = 0.001f;
+
+    /**
+     * Whether positive insets should also extend beyond the view bounds when
+     * auto-scrolling is already active. This allows a user to start scrolling
+     * at an inside edge, then move beyond the edge and continue scrolling.
+     */
+    private boolean mExtendsBeyondEdges = true;
+
+    /** Whether to start activation immediately. */
+    private boolean mSkipDelay;
+
+    /** Whether to reset the scroller start time on the next animation. */
+    private boolean mResetScroller;
+
+    /** Whether the auto-scroller is active. */
+    private boolean mActive;
+    private long[] mScrollStart = new long[2];
+
+    /**
+     * If the event is within this percentage of the edge of the scrolling area,
+     * use accelerated scrolling.
+     */
+    private float mFastScrollingRange = 0.8f;
+
+    /**
+     * Duration of time spent in accelerated scrolling area before reaching
+     * maximum velocity
+     */
+    private float mDurationToMax = 2500f;
+
+    private static final int X = 0;
+    private static final int Y = 1;
+
+    public AutoScroller(View target) {
+        mTarget = target;
+        mScroller = new RampUpScroller(250);
+        mActivationDelay = ViewConfiguration.getTapTimeout();
+    }
+
+    /**
+     * Sets the maximum scrolling velocity as a fraction of the host view size
+     * per second. For example, a maximum Y velocity of 1 would scroll one
+     * vertical page per second. By default, both values are 1.
+     *
+     * @param x The maximum X velocity as a fraction of the host view width per
+     *            second.
+     * @param y The maximum Y velocity as a fraction of the host view height per
+     *            second.
+     */
+    public void setMaximumVelocityRelative(float x, float y) {
+        mMaxVelocityScale = SCALE_RELATIVE;
+        mMaxVelocityX = x / 1000f;
+        mMaxVelocityY = y / 1000f;
+    }
+
+    /**
+     * Sets the maximum scrolling velocity as an absolute pixel distance per
+     * second. For example, a maximum Y velocity of 100 would scroll one hundred
+     * pixels per second.
+     *
+     * @param x The maximum X velocity as a fraction of the host view width per
+     *            second.
+     * @param y The maximum Y velocity as a fraction of the host view height per
+     *            second.
+     */
+    public void setMaximumVelocityAbsolute(float x, float y) {
+        mMaxVelocityScale = SCALE_ABSOLUTE;
+        mMaxVelocityX = x / 1000f;
+        mMaxVelocityY = y / 1000f;
+    }
+
+    /**
+     * Sets the delay after entering an activation edge before activation of
+     * auto-scrolling. By default, the activation delay is set to
+     * {@link ViewConfiguration#getTapTimeout()}.
+     *
+     * @param delayMillis The delay in milliseconds.
+     */
+    public void setActivationDelay(int delayMillis) {
+        mActivationDelay = delayMillis;
+    }
+
+    /**
+     * Sets the activation edges in pixels. Edges are treated as insets, so
+     * positive values expand into the view bounds while negative values extend
+     * outside the bounds.
+     *
+     * @param l The left activation edge, in pixels.
+     * @param t The top activation edge, in pixels.
+     * @param r The right activation edge, in pixels.
+     * @param b The bottom activation edge, in pixels.
+     */
+    public void setEdgesAbsolute(int l, int t, int r, int b) {
+        mActivationEdgeScale = SCALE_ABSOLUTE;
+        mActivationEdges.set(l, t, r, b);
+    }
+
+    /**
+     * Whether positive insets should also extend beyond the view bounds when
+     * auto-scrolling is already active. This allows a user to start scrolling
+     * at an inside edge, then move beyond the edge and continue scrolling.
+     *
+     * @param e
+     */
+    public void setExtendsBeyondEdges(boolean e) {
+        mExtendsBeyondEdges = e;
+    }
+
+    /**
+     * Sets the activation edges as fractions of the host view size. Edges are
+     * treated as insets, so positive values expand into the view bounds while
+     * negative values extend outside the bounds. By default, all values are
+     * 0.25.
+     *
+     * @param l The left activation edge, as a fraction of view size.
+     * @param t The top activation edge, as a fraction of view size.
+     * @param r The right activation edge, as a fraction of view size.
+     * @param b The bottom activation edge, as a fraction of view size.
+     */
+    public void setEdgesRelative(float l, float t, float r, float b) {
+        mActivationEdgeScale = SCALE_RELATIVE;
+        mActivationEdges.set(l, t, r, b);
+    }
+
+    /**
+     * Sets the {@link Interpolator} used for scaling touches within activation
+     * edges. By default, uses the {@link AccelerateInterpolator} to gradually
+     * speed up scrolling.
+     *
+     * @param edgeInterpolator The interpolator to use for activation edges, or
+     *            {@code null} to use a fixed velocity during auto-scrolling.
+     */
+    public void setEdgeInterpolator(Interpolator edgeInterpolator) {
+        mEdgeInterpolator = edgeInterpolator;
+    }
+
+    /**
+     * Stop tracking scrolling.
+     */
+    public void stop() {
+        stop(true);
+    }
+
+    /**
+     * Pass the rectangle defining the drawing region for the object used to
+     * trigger drag scrolling.
+     *
+     * @param v View on which the scrolling regions are defined
+     * @param r Rect defining the drawing bounds of the object being dragged
+     * @return whether the event was handled
+     */
+    public boolean onTouch(View v, Rect r) {
+        MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
+                SystemClock.uptimeMillis(), MotionEvent.ACTION_MOVE, r.left, r.top, 0);
+        return onTouch(v, event);
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        final int action = event.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_MOVE:
+                final int sourceWidth = v.getWidth();
+                final int sourceHeight = v.getHeight();
+                final float x = event.getX();
+                final float y = event.getY();
+                final float l;
+                final float t;
+                final float r;
+                final float b;
+                final RectF activationEdges = mActivationEdges;
+                if (mActivationEdgeScale == SCALE_ABSOLUTE) {
+                    l = activationEdges.left;
+                    t = activationEdges.top;
+                    r = activationEdges.right;
+                    b = activationEdges.bottom;
+                } else {
+                    l = activationEdges.left * sourceWidth;
+                    t = activationEdges.top * sourceHeight;
+                    r = activationEdges.right * sourceWidth;
+                    b = activationEdges.bottom * sourceHeight;
+                }
+
+                final float maxVelX;
+                final float maxVelY;
+                if (mMaxVelocityScale == SCALE_ABSOLUTE) {
+                    maxVelX = mMaxVelocityX;
+                    maxVelY = mMaxVelocityY;
+                } else {
+                    maxVelX = mMaxVelocityX * mTarget.getWidth();
+                    maxVelY = mMaxVelocityY * mTarget.getHeight();
+                }
+
+                final float velocityX = getEdgeVelocity(X, l, r, x, sourceWidth, event);
+                final float velocityY = getEdgeVelocity(Y, t, b, y, sourceHeight, event);
+                mScroller.setTargetVelocity(velocityX * maxVelX, velocityY * maxVelY);
+
+                if ((velocityX != 0 || velocityY != 0) && !mActive) {
+                    mActive = true;
+                    mResetScroller = true;
+                    if (mSkipDelay) {
+                        mTarget.postOnAnimation(this);
+                    } else {
+                        mSkipDelay = true;
+                        mTarget.postOnAnimationDelayed(this, mActivationDelay);
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                stop(true);
+                break;
+        }
+
+        return false;
+    }
+
+    /**
+     * @param leading Size of the leading activation inset.
+     * @param trailing Size of the trailing activation inset.
+     * @param current Position within within the total area.
+     * @param size Size of the total area.
+     * @return The fraction of the activation area.
+     */
+    private float getEdgeVelocity(int dir, float leading, float trailing,
+            float current, float size, MotionEvent ev) {
+        float valueLeading = 0;
+        if (leading > 0) {
+            if (current < leading) {
+                if (current > 0) {
+                    // Movement up to the edge is scaled.
+                    valueLeading = 1f - current / leading;
+                } else if (mActive && mExtendsBeyondEdges) {
+                    // Movement beyond the edge is always maximum.
+                    valueLeading = 1f;
+                }
+            }
+        } else if (leading < 0) {
+            if (current < 0) {
+                // Movement beyond the edge is scaled.
+                valueLeading = current / leading;
+            }
+        }
+
+        float valueTrailing = 0;
+        if (trailing > 0) {
+            if (current > size - trailing) {
+                if (current < size) {
+                    // Movement up to the edge is scaled.
+                    valueTrailing = 1f - (size - current) / trailing;
+                } else if (mActive && mExtendsBeyondEdges) {
+                    // Movement beyond the edge is always maximum.
+                    valueTrailing = 1f;
+                }
+            }
+        } else if (trailing < 0) {
+            if (current > size) {
+                // Movement beyond the edge is scaled.
+                valueTrailing = (size - current) / trailing;
+            }
+        }
+
+        float value = (valueTrailing - valueLeading);
+        if ((value > mFastScrollingRange || value < -mFastScrollingRange)
+            && mScrollStart[dir] == 0) {
+            // within auto scrolling area
+            mScrollStart[dir] = ev.getEventTime();
+        } else {
+            // Outside fast scrolling area; reset duration
+            mScrollStart[dir] = 0;
+        }
+        final float duration = (ev.getEventTime() - mScrollStart[dir])/mDurationToMax;
+        final float interpolated;
+        if (value < 0) {
+            if (value < -mFastScrollingRange) {
+                // Close to top; use duration!
+                value += mEdgeInterpolator.getInterpolation(-duration);
+            }
+            interpolated = mEdgeInterpolator == null ? -1
+                    : -mEdgeInterpolator.getInterpolation(-value);
+        } else if (value > 0) {
+            // Close to bottom; use duration
+            if (value > mFastScrollingRange) {
+                // Close to bottom; use duration!
+                value += mEdgeInterpolator.getInterpolation(duration);
+            }
+            interpolated = mEdgeInterpolator == null ? 1
+                    : mEdgeInterpolator.getInterpolation(value);
+        } else {
+            mScrollStart[dir] = 0;
+            return 0;
+        }
+
+        return constrain(interpolated, -1, 1);
+    }
+
+    private static float constrain(float value, float min, float max) {
+        if (value > max) {
+            return max;
+        } else if (value < min) {
+            return min;
+        } else {
+            return value;
+        }
+    }
+
+    /**
+     * Stops auto-scrolling immediately, optionally reseting the auto-scrolling
+     * delay.
+     *
+     * @param reset Whether to reset the auto-scrolling delay.
+     */
+    private void stop(boolean reset) {
+        mActive = false;
+        mSkipDelay = !reset;
+        mTarget.removeCallbacks(this);
+    }
+
+    @Override
+    public void run() {
+        if (!mActive) {
+            return;
+        }
+
+        if (mResetScroller) {
+            mResetScroller = false;
+            mScroller.start();
+        }
+
+        final View target = mTarget;
+        final RampUpScroller scroller = mScroller;
+        final float targetVelocityX = scroller.getTargetVelocityX();
+        final float targetVelocityY = scroller.getTargetVelocityY();
+        if ((targetVelocityY == 0 || !target.canScrollVertically(targetVelocityY > 0 ? 1 : -1)
+                && (targetVelocityX == 0
+                        || !target.canScrollHorizontally(targetVelocityX > 0 ? 1 : -1)))) {
+            stop(false);
+            return;
+        }
+
+        scroller.computeScrollDelta();
+
+        final int deltaX = scroller.getDeltaX();
+        final int deltaY = scroller.getDeltaY();
+
+        if (target instanceof AbsListView) {
+            final AbsListView list = (AbsListView) target;
+            list.smoothScrollBy(deltaY, 0);
+        } else {
+            target.scrollBy(deltaX, deltaY);
+        }
+
+        target.postOnAnimation(this);
+    }
+}
diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java
index 54f7dbc..970ce26 100644
--- a/src/com/android/launcher3/DragController.java
+++ b/src/com/android/launcher3/DragController.java
@@ -123,17 +123,16 @@
      * Interface to receive notifications when a drag starts or stops
      */
     interface DragListener {
-        
         /**
          * A drag has begun
-         * 
+         *
          * @param source An object representing where the drag originated
          * @param info The data associated with the object that is being dragged
          * @param dragAction The drag action: either {@link DragController#DRAG_ACTION_MOVE}
          *        or {@link DragController#DRAG_ACTION_COPY}
          */
         void onDragStart(DragSource source, Object info, int dragAction);
-        
+
         /**
          * The drag has ended
          */
@@ -514,7 +513,6 @@
             if (delegate != null) {
                 dropTarget = delegate;
             }
-
             if (mLastDropTarget != dropTarget) {
                 if (mLastDropTarget != null) {
                     mLastDropTarget.onDragExit(mDragObject);
@@ -589,6 +587,7 @@
             } else {
                 mScrollState = SCROLL_OUTSIDE_ZONE;
             }
+            handleMoveEvent(dragLayerX, dragLayerY);
             break;
         case MotionEvent.ACTION_MOVE:
             handleMoveEvent(dragLayerX, dragLayerY);
diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java
index 3ecb8ff..fa364fa 100644
--- a/src/com/android/launcher3/DropTarget.java
+++ b/src/com/android/launcher3/DropTarget.java
@@ -123,7 +123,7 @@
      *          touch happened
      * @param dragView The DragView that's being dragged around on screen.
      * @param dragInfo Data associated with the object being dragged
-     * 
+     *
      */
     void onDrop(DragObject dragObject);
 
diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java
index 36b6bf4..cf1a432 100644
--- a/src/com/android/launcher3/Folder.java
+++ b/src/com/android/launcher3/Folder.java
@@ -107,22 +107,10 @@
     FolderEditText mFolderName;
     private float mFolderIconPivotX;
     private float mFolderIconPivotY;
-    private long mDragScrollStart;
-
-    /** Interpolator used to scale velocity with touch position, may be null. */
-    private Interpolator mEdgeInterpolator = new AccelerateInterpolator();
 
     private static final int SCROLL_CUT_OFF_AMOUNT = 60;
 
-    private final float mDurationToMax = 300f;
-    private final float mMaxVelocity = 100000f/1000f;
-
-    // Aim for this amount of target area to trigger scrolling
-    private static float sScrollBandFactor = -1f;
-    private static float sScrollBandMaxFactor = -1f;
-
-    // Min size for the target area to trigger scrolling
-    private static int sMinScrollBandHeight = -1;
+    private static final float MAX_SCROLL_VELOCITY = 1500f;
 
     private boolean mIsEditingName = false;
     private InputMethodManager mInputMethodManager;
@@ -132,12 +120,12 @@
 
     private int DRAG_MODE_NONE = 0;
     private int DRAG_MODE_REORDER = 1;
-    private int DRAG_MODE_SCROLL_UP = 2;
-    private int DRAG_MODE_SCROLL_DOWN = 3;
     private int mDragMode = DRAG_MODE_NONE;
 
     private boolean mDestroyed;
 
+    private AutoScroller mAutoScroller;
+
     /**
      * Used to inflate the Workspace from XML.
      *
@@ -191,15 +179,6 @@
         if (sHintText == null) {
             sHintText = res.getString(R.string.folder_hint_text);
         }
-        if (sMinScrollBandHeight == -1) {
-            sMinScrollBandHeight = res.getDimensionPixelSize(R.dimen.min_scroll_band_size);
-        }
-        if (sScrollBandFactor == -1f) {
-            sScrollBandFactor = res.getInteger(R.integer.scroll_band_factor)/100f;
-        }
-        if (sScrollBandMaxFactor == -1f) {
-            sScrollBandMaxFactor = res.getInteger(R.integer.scroll_band_max_factor)/100f;
-        }
         mLauncher = (Launcher) context;
         // We need this view to be focusable in touch mode so that when text editing of the folder
         // name is complete, we have something to focus on, thus hiding the cursor and giving
@@ -236,6 +215,9 @@
         mFolderName.setSelectAllOnFocus(true);
         mFolderName.setInputType(mFolderName.getInputType() |
                 InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
+        mAutoScroller = new AutoScroller(mScrollView);
+        mAutoScroller.setMaximumVelocityAbsolute(MAX_SCROLL_VELOCITY, MAX_SCROLL_VELOCITY);
+        mAutoScroller.setExtendsBeyondEdges(false);
     }
 
     private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
@@ -673,83 +655,38 @@
         return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
     }
 
+    private Rect getDragObjectDrawingRect(View dragView, float[] r) {
+        final Rect drawingRect = mTempRect;
+        drawingRect.left = (int) r[0];
+        drawingRect.top = (int) r[1];
+        drawingRect.right = drawingRect.left + dragView.getMeasuredWidth();
+        drawingRect.bottom = drawingRect.top + dragView.getMeasuredHeight();
+        return drawingRect;
+    }
+
     public void onDragOver(DragObject d) {
         int scrollOffset = mScrollView.getScrollY();
-        int height = getMeasuredHeight();
-        int scrollBandHeight = Math.min((int) (height * sScrollBandMaxFactor),
-            (Math.max(sMinScrollBandHeight, (int) (height * sScrollBandFactor))));
         float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, null);
         r[0] -= getPaddingLeft();
         r[1] -= getPaddingTop();
+        if (!mAutoScroller.onTouch(this, getDragObjectDrawingRect(d.dragView, r))) {
+            mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1] + scrollOffset, 1, 1,
+                    mTargetCell);
 
-        mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1] + scrollOffset,
-                1, 1, mTargetCell);
-
-        if (isLayoutRtl()) {
-            mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1;
-        }
-
-        if (r[1] < scrollBandHeight && mScrollView.getScrollY() > 0) {
-            // Scroll up
-            if (mDragMode != DRAG_MODE_SCROLL_UP) {
-                mDragMode = DRAG_MODE_SCROLL_UP;
-                mDragScrollStart = System.currentTimeMillis();
-                scrollUp();
+            if (isLayoutRtl()) {
+                mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1;
             }
-            mReorderAlarm.cancelAlarm();
-        } else if (r[1] > (getFolderHeight() - scrollBandHeight) && mScrollView.getScrollY() <
-                (mContent.getMeasuredHeight() - mScrollView.getMeasuredHeight())) {
-            if (mDragMode != DRAG_MODE_SCROLL_DOWN) {
-                mDragMode = DRAG_MODE_SCROLL_DOWN;
-                mDragScrollStart = System.currentTimeMillis();
-                scrollDown();
+            if (mTargetCell[0] != mPreviousTargetCell[0]
+                    || mTargetCell[1] != mPreviousTargetCell[1]) {
+                mReorderAlarm.cancelAlarm();
+                mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
+                mReorderAlarm.setAlarm(REORDER_DELAY);
+                mPreviousTargetCell[0] = mTargetCell[0];
+                mPreviousTargetCell[1] = mTargetCell[1];
+                mDragMode = DRAG_MODE_REORDER;
+            } else {
+                mDragMode = DRAG_MODE_NONE;
             }
-            mReorderAlarm.cancelAlarm();
-        } else if (mTargetCell[0] != mPreviousTargetCell[0] || mTargetCell[1] != mPreviousTargetCell[1]) {
-            mReorderAlarm.cancelAlarm();
-            mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
-            mReorderAlarm.setAlarm(REORDER_DELAY);
-            mPreviousTargetCell[0] = mTargetCell[0];
-            mPreviousTargetCell[1] = mTargetCell[1];
-            mDragMode = DRAG_MODE_REORDER;
-        } else {
-            mDragMode = DRAG_MODE_NONE;
-        }
-    }
-
-    Runnable mScrollUpRunnable = new Runnable() {
-        @Override
-        public void run() {
-            scrollUp();
-        }
-    };
-
-    Runnable mScrollDownRunnable = new Runnable() {
-        @Override
-        public void run() {
-            scrollDown();
-        }
-    };
-
-    private int getVelocity() {
-        float duration = (System.currentTimeMillis() - mDragScrollStart)/mDurationToMax;
-        return (int) Math.min(mMaxVelocity,
-        		(int) (mEdgeInterpolator.getInterpolation(duration) * mMaxVelocity));
-    }
-
-    private void scrollUp() {
-        if (mDragMode == DRAG_MODE_SCROLL_UP) {
-            mScrollView.setScrollY(mScrollView.getScrollY() - getVelocity());
-            invalidate();
-            post(mScrollUpRunnable);
-        }
-    }
-
-    private void scrollDown() {
-        if (mDragMode == DRAG_MODE_SCROLL_DOWN) {
-            mScrollView.setScrollY(mScrollView.getScrollY() + getVelocity());
-            invalidate();
-            post(mScrollDownRunnable);
         }
     }
 
@@ -794,8 +731,10 @@
     }
 
     public void onDragExit(DragObject d) {
-        // We only close the folder if this is a true drag exit, ie. not because a drop
-        // has occurred above the folder.
+        // Exiting folder; stop the auto scroller.
+        mAutoScroller.stop();
+        // We only close the folder if this is a true drag exit, ie. not because
+        // a drop has occurred above the folder.
         if (!d.dragComplete) {
             mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener);
             mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY);
diff --git a/src/com/android/launcher3/RampUpScroller.java b/src/com/android/launcher3/RampUpScroller.java
new file mode 100644
index 0000000..89eb579
--- /dev/null
+++ b/src/com/android/launcher3/RampUpScroller.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2013 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 com.android.launcher3;
+
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+/**
+ * Scroller that gradually reaches a target velocity.
+ */
+class RampUpScroller {
+    private final Interpolator mInterpolator;
+    private final long mRampUpTime;
+
+    private long mStartTime;
+    private long mDeltaTime;
+    private float mTargetVelocityX;
+    private float mTargetVelocityY;
+    private int mDeltaX;
+    private int mDeltaY;
+
+    /**
+     * Creates a new ramp-up scroller that reaches full velocity after a
+     * specified duration.
+     *
+     * @param rampUpTime Duration before the scroller reaches target velocity.
+     */
+    public RampUpScroller(long rampUpTime) {
+        mInterpolator = new AccelerateInterpolator();
+        mRampUpTime = rampUpTime;
+    }
+
+    /**
+     * Starts the scroller at the current animation time.
+     */
+    public void start() {
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mDeltaTime = mStartTime;
+    }
+
+    /**
+     * Computes the current scroll deltas. This usually only be called after
+     * starting the scroller with {@link #start()}.
+     *
+     * @see #getDeltaX()
+     * @see #getDeltaY()
+     */
+    public void computeScrollDelta() {
+        final long currentTime = AnimationUtils.currentAnimationTimeMillis();
+        final long elapsedSinceStart = currentTime - mStartTime;
+        final float scale;
+        if (elapsedSinceStart < mRampUpTime) {
+            scale = mInterpolator.getInterpolation((float) elapsedSinceStart / mRampUpTime);
+        } else {
+            scale = 1f;
+        }
+
+        final long elapsedSinceDelta = currentTime - mDeltaTime;
+        mDeltaTime = currentTime;
+
+        mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX);
+        mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY);
+    }
+
+    /**
+     * Sets the target velocity for this scroller.
+     *
+     * @param x The target X velocity in pixels per millisecond.
+     * @param y The target Y velocity in pixels per millisecond.
+     */
+    public void setTargetVelocity(float x, float y) {
+        mTargetVelocityX = x;
+        mTargetVelocityY = y;
+    }
+
+    /**
+     * @return The target X velocity for this scroller.
+     */
+    public float getTargetVelocityX() {
+        return mTargetVelocityX;
+    }
+
+    /**
+     * @return The target Y velocity for this scroller.
+     */
+    public float getTargetVelocityY() {
+        return mTargetVelocityY;
+    }
+
+    /**
+     * The distance traveled in the X-coordinate computed by the last call to
+     * {@link #computeScrollDelta()}.
+     */
+    public int getDeltaX() {
+        return mDeltaX;
+    }
+
+    /**
+     * The distance traveled in the Y-coordinate computed by the last call to
+     * {@link #computeScrollDelta()}.
+     */
+    public int getDeltaY() {
+        return mDeltaY;
+    }
+}