diff options
author | Alan Viverette <alanv@google.com> | 2013-08-28 17:53:41 -0700 |
---|---|---|
committer | Mindy Pereira <mindyp@google.com> | 2013-09-03 08:32:03 -0700 |
commit | 4cda5b770db7f375a4766fe6c0d657b9d5303374 (patch) | |
tree | e6cb8760a0e81835aa1fbc69a0195f1f97e7eaf8 /src | |
parent | 8b3cd79082043a08c319f1550679d4078b7d509c (diff) | |
download | android_packages_apps_Trebuchet-4cda5b770db7f375a4766fe6c0d657b9d5303374.tar.gz android_packages_apps_Trebuchet-4cda5b770db7f375a4766fe6c0d657b9d5303374.tar.bz2 android_packages_apps_Trebuchet-4cda5b770db7f375a4766fe6c0d657b9d5303374.zip |
Port AutoScroller to launcher3
Change-Id: Ic2b7b5faac4878f3b4645c291e36c5f7734c7f8b
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/launcher3/AutoScrollHelper.java | 867 | ||||
-rw-r--r-- | src/com/android/launcher3/AutoScroller.java | 423 | ||||
-rw-r--r-- | src/com/android/launcher3/Folder.java | 46 | ||||
-rw-r--r-- | src/com/android/launcher3/FolderAutoScrollHelper.java | 56 |
4 files changed, 946 insertions, 446 deletions
diff --git a/src/com/android/launcher3/AutoScrollHelper.java b/src/com/android/launcher3/AutoScrollHelper.java new file mode 100644 index 000000000..9a7c3b0f0 --- /dev/null +++ b/src/com/android/launcher3/AutoScrollHelper.java @@ -0,0 +1,867 @@ +/* + * 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.content.res.Resources; +import android.os.SystemClock; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +/** + * AutoScrollHelper is a utility class for adding automatic edge-triggered + * scrolling to Views. + * <p> + * <b>Note:</b> Implementing classes are responsible for overriding the + * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and + * {@link #canTargetScrollVertically} methods. See + * {@link ListViewAutoScrollHelper} for a {@link android.widget.ListView} + * -specific implementation. + * <p> + * <h1>Activation</h1> Automatic scrolling starts when the user touches within + * an activation area. By default, activation areas are defined as the top, + * left, right, and bottom 20% of the host view's total area. Touching within + * the top activation area scrolls up, left scrolls to the left, and so on. + * <p> + * As the user touches closer to the extreme edge of the activation area, + * scrolling accelerates up to a maximum velocity. When using the default edge + * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds + * will scroll at the maximum velocity. + * <p> + * The following activation properties may be configured: + * <ul> + * <li>Delay after entering activation area before auto-scrolling begins, see + * {@link #setActivationDelay}. Default value is + * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps. + * <li>Location of activation areas, see {@link #setEdgeType}. Default value is + * {@link #EDGE_TYPE_INSIDE_EXTEND}. + * <li>Size of activation areas relative to view size, see + * {@link #setRelativeEdges}. Default value is 20% for both vertical and + * horizontal edges. + * <li>Maximum size used to constrain relative size, see + * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}. + * </ul> + * <h1>Scrolling</h1> When automatic scrolling is active, the helper will + * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets. + * <p> + * The following scrolling properties may be configured: + * <ul> + * <li>Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default + * value is 2500 milliseconds. + * <li>Acceleration ramp-down duration, see {@link #setRampDownDuration}. + * Default value is 500 milliseconds. + * <li>Target velocity relative to view size, see {@link #setRelativeVelocity}. + * Default value is 100% per second for both vertical and horizontal. + * <li>Minimum velocity used to constrain relative velocity, see + * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the + * larger of either this value or the relative target value. Default value is + * approximately 5 centimeters or 315 dips per second. + * <li>Maximum velocity used to constrain relative velocity, see + * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or + * 1575 dips per second. + * </ul> + */ +public abstract class AutoScrollHelper implements View.OnTouchListener { + /** + * Constant passed to {@link #setRelativeEdges} or + * {@link #setRelativeVelocity}. Using this value ensures that the computed + * relative value is ignored and the absolute maximum value is always used. + */ + public static final float RELATIVE_UNSPECIFIED = 0; + + /** + * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity}, + * or {@link #setMinimumVelocity}. Using this value ensures that the + * computed relative value is always used without constraining to a + * particular minimum or maximum value. + */ + public static final float NO_MAX = Float.MAX_VALUE; + + /** + * Constant passed to {@link #setMaximumEdges}, or + * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this + * value ensures that the computed relative value is always used without + * constraining to a particular minimum or maximum value. + */ + public static final float NO_MIN = 0; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending inward. Moving outside the view bounds will stop scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_INSIDE = 0; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending inward. After activation begins, moving outside the view + * bounds will continue scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_INSIDE_EXTEND = 1; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending outward. Moving inside the view bounds will stop scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_OUTSIDE = 2; + + private static final int HORIZONTAL = 0; + private static final int VERTICAL = 1; + + /** Scroller used to control acceleration toward maximum velocity. */ + private final ClampedScroller mScroller = new ClampedScroller(); + + /** Interpolator used to scale velocity with touch position. */ + private final Interpolator mEdgeInterpolator = new AccelerateInterpolator(); + + /** The view to auto-scroll. Might not be the source of touch events. */ + private final View mTarget; + + /** Runnable used to animate scrolling. */ + private Runnable mRunnable; + + /** Edge insets used to activate auto-scrolling. */ + private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED }; + + /** Clamping values for edge insets used to activate auto-scrolling. */ + private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX }; + + /** The type of edge being used. */ + private int mEdgeType; + + /** Delay after entering an activation edge before auto-scrolling begins. */ + private int mActivationDelay; + + /** Relative scrolling velocity at maximum edge distance. */ + private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED }; + + /** Clamping values used for scrolling velocity. */ + private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN }; + + /** Clamping values used for scrolling velocity. */ + private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX }; + + /** Whether to start activation immediately. */ + private boolean mAlreadyDelayed; + + /** Whether to reset the scroller start time on the next animation. */ + private boolean mNeedsReset; + + /** Whether to send a cancel motion event to the target view. */ + private boolean mNeedsCancel; + + /** Whether the auto-scroller is actively scrolling. */ + private boolean mAnimating; + + /** Whether the auto-scroller is enabled. */ + private boolean mEnabled; + + /** Whether the auto-scroller consumes events when scrolling. */ + private boolean mExclusive; + + // Default values. + private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND; + private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315; + private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575; + private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX; + private static final float DEFAULT_RELATIVE_EDGE = 0.2f; + private static final float DEFAULT_RELATIVE_VELOCITY = 1f; + private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout(); + private static final int DEFAULT_RAMP_UP_DURATION = 2500; + private static final int DEFAULT_RAMP_DOWN_DURATION = 500; + + /** + * Creates a new helper for scrolling the specified target view. + * <p> + * The resulting helper may be configured by chaining setter calls and + * should be set as a touch listener on the target view. + * <p> + * By default, the helper is disabled and will not respond to touch events + * until it is enabled using {@link #setEnabled}. + * + * @param target The view to automatically scroll. + */ + public AutoScrollHelper(View target) { + mTarget = target; + + final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); + final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f); + final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f); + setMaximumVelocity(maxVelocity, maxVelocity); + setMinimumVelocity(minVelocity, minVelocity); + + setEdgeType(DEFAULT_EDGE_TYPE); + setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE); + setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE); + setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY); + setActivationDelay(DEFAULT_ACTIVATION_DELAY); + setRampUpDuration(DEFAULT_RAMP_UP_DURATION); + setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION); + } + + /** + * Sets whether the scroll helper is enabled and should respond to touch + * events. + * + * @param enabled Whether the scroll helper is enabled. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setEnabled(boolean enabled) { + if (mEnabled && !enabled) { + requestStop(); + } + + mEnabled = enabled; + return this; + } + + /** + * @return True if this helper is enabled and responding to touch events. + */ + public boolean isEnabled() { + return mEnabled; + } + + /** + * Enables or disables exclusive handling of touch events during scrolling. + * By default, exclusive handling is disabled and the target view receives + * all touch events. + * <p> + * When enabled, {@link #onTouch} will return true if the helper is + * currently scrolling and false otherwise. + * + * @param exclusive True to exclusively handle touch events during scrolling, + * false to allow the target view to receive all touch events. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setExclusive(boolean exclusive) { + mExclusive = exclusive; + return this; + } + + /** + * Indicates whether the scroll helper handles touch events exclusively + * during scrolling. + * + * @return True if exclusive handling of touch events during scrolling is + * enabled, false otherwise. + * @see #setExclusive(boolean) + */ + public boolean isExclusive() { + return mExclusive; + } + + /** + * Sets the absolute maximum scrolling velocity. + * <p> + * If relative velocity is not specified, scrolling will always reach the + * same maximum velocity. If both relative and maximum velocities are + * specified, the maximum velocity will be used to clamp the calculated + * relative velocity. + * + * @param horizontalMax The maximum horizontal scrolling velocity, or + * {@link #NO_MAX} to leave the relative value unconstrained. + * @param verticalMax The maximum vertical scrolling velocity, or + * {@link #NO_MAX} to leave the relative value unconstrained. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) { + mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f; + mMaximumVelocity[VERTICAL] = verticalMax / 1000f; + return this; + } + + /** + * Sets the absolute minimum scrolling velocity. + * <p> + * If both relative and minimum velocities are specified, the minimum + * velocity will be used to clamp the calculated relative velocity. + * + * @param horizontalMin The minimum horizontal scrolling velocity, or + * {@link #NO_MIN} to leave the relative value unconstrained. + * @param verticalMin The minimum vertical scrolling velocity, or + * {@link #NO_MIN} to leave the relative value unconstrained. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) { + mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f; + mMinimumVelocity[VERTICAL] = verticalMin / 1000f; + return this; + } + + /** + * Sets the target scrolling velocity relative to the host view's + * dimensions. + * <p> + * If both relative and maximum velocities are specified, the maximum + * velocity will be used to clamp the calculated relative velocity. + * + * @param horizontal The target horizontal velocity as a fraction of the + * host view width per second, or {@link #RELATIVE_UNSPECIFIED} + * to ignore. + * @param vertical The target vertical velocity as a fraction of the host + * view height per second, or {@link #RELATIVE_UNSPECIFIED} to + * ignore. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) { + mRelativeVelocity[HORIZONTAL] = horizontal / 1000f; + mRelativeVelocity[VERTICAL] = vertical / 1000f; + return this; + } + + /** + * Sets the activation edge type, one of: + * <ul> + * <li>{@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside + * the bounds of the host view. If touch moves outside the bounds, scrolling + * will stop. + * <li>{@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to + * scroll when touch moves outside the bounds of the host view. + * <li>{@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches + * that move outside the bounds of the host view. + * </ul> + * + * @param type The type of edge to use. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setEdgeType(int type) { + mEdgeType = type; + return this; + } + + /** + * Sets the activation edge size relative to the host view's dimensions. + * <p> + * If both relative and maximum edges are specified, the maximum edge will + * be used to constrain the calculated relative edge size. + * + * @param horizontal The horizontal edge size as a fraction of the host view + * width, or {@link #RELATIVE_UNSPECIFIED} to always use the + * maximum value. + * @param vertical The vertical edge size as a fraction of the host view + * height, or {@link #RELATIVE_UNSPECIFIED} to always use the + * maximum value. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) { + mRelativeEdges[HORIZONTAL] = horizontal; + mRelativeEdges[VERTICAL] = vertical; + return this; + } + + /** + * Sets the absolute maximum edge size. + * <p> + * If relative edge size is not specified, activation edges will always be + * the maximum edge size. If both relative and maximum edges are specified, + * the maximum edge will be used to constrain the calculated relative edge + * size. + * + * @param horizontalMax The maximum horizontal edge size in pixels, or + * {@link #NO_MAX} to use the unconstrained calculated relative + * value. + * @param verticalMax The maximum vertical edge size in pixels, or + * {@link #NO_MAX} to use the unconstrained calculated relative + * value. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) { + mMaximumEdges[HORIZONTAL] = horizontalMax; + mMaximumEdges[VERTICAL] = verticalMax; + return this; + } + + /** + * Sets the delay after entering an activation edge before activation of + * auto-scrolling. By default, the activation delay is set to + * {@link ViewConfiguration#getTapTimeout()}. + * <p> + * Specifying a delay of zero will start auto-scrolling immediately after + * the touch position enters an activation edge. + * + * @param delayMillis The activation delay in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setActivationDelay(int delayMillis) { + mActivationDelay = delayMillis; + return this; + } + + /** + * Sets the amount of time after activation of auto-scrolling that is takes + * to reach target velocity for the current touch position. + * <p> + * Specifying a duration greater than zero prevents sudden jumps in + * velocity. + * + * @param durationMillis The ramp-up duration in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRampUpDuration(int durationMillis) { + mScroller.setRampUpDuration(durationMillis); + return this; + } + + /** + * Sets the amount of time after de-activation of auto-scrolling that is + * takes to slow to a stop. + * <p> + * Specifying a duration greater than zero prevents sudden jumps in + * velocity. + * + * @param durationMillis The ramp-down duration in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRampDownDuration(int durationMillis) { + mScroller.setRampDownDuration(durationMillis); + return this; + } + + /** + * Handles touch events by activating automatic scrolling, adjusting scroll + * velocity, or stopping. + * <p> + * If {@link #isExclusive()} is false, always returns false so that + * the host view may handle touch events. Otherwise, returns true when + * automatic scrolling is active and false otherwise. + */ + @Override + public boolean onTouch(View v, MotionEvent event) { + if (!mEnabled) { + return false; + } + + final int action = MotionEventCompat.getActionMasked(event); + switch (action) { + case MotionEvent.ACTION_DOWN: + mNeedsCancel = true; + mAlreadyDelayed = false; + // $FALL-THROUGH$ + case MotionEvent.ACTION_MOVE: + final float xTargetVelocity = computeTargetVelocity( + HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth()); + final float yTargetVelocity = computeTargetVelocity( + VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight()); + mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity); + + // If the auto scroller was not previously active, but it should + // be, then update the state and start animations. + if (!mAnimating && shouldAnimate()) { + startAnimating(); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + requestStop(); + break; + } + + return mExclusive && mAnimating; + } + + /** + * @return whether the target is able to scroll in the requested direction + */ + private boolean shouldAnimate() { + final ClampedScroller scroller = mScroller; + final int verticalDirection = scroller.getVerticalDirection(); + final int horizontalDirection = scroller.getHorizontalDirection(); + + return verticalDirection != 0 && canTargetScrollVertically(verticalDirection) + || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection); + } + + /** + * Starts the scroll animation. + */ + private void startAnimating() { + if (mRunnable == null) { + mRunnable = new ScrollAnimationRunnable(); + } + + mAnimating = true; + mNeedsReset = true; + + if (!mAlreadyDelayed && mActivationDelay > 0) { + ViewCompat.postOnAnimationDelayed(mTarget, mRunnable, mActivationDelay); + } else { + mRunnable.run(); + } + + // If we start animating again before the user lifts their finger, we + // already know it's not a tap and don't need an activation delay. + mAlreadyDelayed = true; + } + + /** + * Requests that the scroll animation slow to a stop. If there is an + * activation delay, this may occur between posting the animation and + * actually running it. + */ + private void requestStop() { + if (mNeedsReset) { + // The animation has been posted, but hasn't run yet. Manually + // stopping animation will prevent it from running. + mAnimating = false; + } else { + mScroller.requestStop(); + } + } + + private float computeTargetVelocity( + int direction, float coordinate, float srcSize, float dstSize) { + final float relativeEdge = mRelativeEdges[direction]; + final float maximumEdge = mMaximumEdges[direction]; + final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate); + if (value == 0) { + // The edge in this direction is not activated. + return 0; + } + + final float relativeVelocity = mRelativeVelocity[direction]; + final float minimumVelocity = mMinimumVelocity[direction]; + final float maximumVelocity = mMaximumVelocity[direction]; + final float targetVelocity = relativeVelocity * dstSize; + + // Target velocity is adjusted for interpolated edge position, then + // clamped to the minimum and maximum values. Later, this value will be + // adjusted for time-based acceleration. + if (value > 0) { + return constrain(value * targetVelocity, minimumVelocity, maximumVelocity); + } else { + return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity); + } + } + + /** + * Override this method to scroll the target view by the specified number of + * pixels. + * + * @param deltaX The number of pixels to scroll by horizontally. + * @param deltaY The number of pixels to scroll by vertically. + */ + public abstract void scrollTargetBy(int deltaX, int deltaY); + + /** + * Override this method to return whether the target view can be scrolled + * horizontally in a certain direction. + * + * @param direction Negative to check scrolling left, positive to check + * scrolling right. + * @return true if the target view is able to horizontally scroll in the + * specified direction. + */ + public abstract boolean canTargetScrollHorizontally(int direction); + + /** + * Override this method to return whether the target view can be scrolled + * vertically in a certain direction. + * + * @param direction Negative to check scrolling up, positive to check + * scrolling down. + * @return true if the target view is able to vertically scroll in the + * specified direction. + */ + public abstract boolean canTargetScrollVertically(int direction); + + /** + * Returns the interpolated position of a touch point relative to an edge + * defined by its relative inset, its maximum absolute inset, and the edge + * interpolator. + * + * @param relativeValue The size of the inset relative to the total size. + * @param size Total size. + * @param maxValue The maximum size of the inset, used to clamp (relative * + * total). + * @param current Touch position within within the total size. + * @return Interpolated value of the touch position within the edge. + */ + private float getEdgeValue(float relativeValue, float size, float maxValue, float current) { + // For now, leading and trailing edges are always the same size. + final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue); + final float valueLeading = constrainEdgeValue(current, edgeSize); + final float valueTrailing = constrainEdgeValue(size - current, edgeSize); + final float value = (valueTrailing - valueLeading); + final float interpolated; + if (value < 0) { + interpolated = -mEdgeInterpolator.getInterpolation(-value); + } else if (value > 0) { + interpolated = mEdgeInterpolator.getInterpolation(value); + } else { + return 0; + } + + return constrain(interpolated, -1, 1); + } + + private float constrainEdgeValue(float current, float leading) { + if (leading == 0) { + return 0; + } + + switch (mEdgeType) { + case EDGE_TYPE_INSIDE: + case EDGE_TYPE_INSIDE_EXTEND: + if (current < leading) { + if (current > 0) { + // Movement up to the edge is scaled. + return 1f - current / leading; + } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) { + // Movement beyond the edge is always maximum. + return 1f; + } + } + break; + case EDGE_TYPE_OUTSIDE: + if (current < 0) { + // Movement beyond the edge is scaled. + return current / -leading; + } + break; + } + + return 0; + } + + private static int constrain(int value, int min, int max) { + if (value > max) { + return max; + } else if (value < min) { + return min; + } else { + return value; + } + } + + private static float constrain(float value, float min, float max) { + if (value > max) { + return max; + } else if (value < min) { + return min; + } else { + return value; + } + } + + /** + * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view, + * canceling any ongoing touch events. + */ + private void cancelTargetTouch() { + final long eventTime = SystemClock.uptimeMillis(); + final MotionEvent cancel = MotionEvent.obtain( + eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mTarget.onTouchEvent(cancel); + cancel.recycle(); + } + + private class ScrollAnimationRunnable implements Runnable { + @Override + public void run() { + if (!mAnimating) { + return; + } + + if (mNeedsReset) { + mNeedsReset = false; + mScroller.start(); + } + + final ClampedScroller scroller = mScroller; + if (scroller.isFinished() || !shouldAnimate()) { + mAnimating = false; + return; + } + + if (mNeedsCancel) { + mNeedsCancel = false; + cancelTargetTouch(); + } + + scroller.computeScrollDelta(); + + final int deltaX = scroller.getDeltaX(); + final int deltaY = scroller.getDeltaY(); + scrollTargetBy(deltaX, deltaY); + + // Keep going until the scroller has permanently stopped. + ViewCompat.postOnAnimation(mTarget, this); + } + } + + /** + * Scroller whose velocity follows the curve of an {@link Interpolator} and + * is clamped to the interpolated 0f value before starting and the + * interpolated 1f value after a specified duration. + */ + private static class ClampedScroller { + private int mRampUpDuration; + private int mRampDownDuration; + private float mTargetVelocityX; + private float mTargetVelocityY; + + private long mStartTime; + + private long mDeltaTime; + private int mDeltaX; + private int mDeltaY; + + private long mStopTime; + private float mStopValue; + private int mEffectiveRampDown; + + /** + * Creates a new ramp-up scroller that reaches full velocity after a + * specified duration. + */ + public ClampedScroller() { + mStartTime = Long.MIN_VALUE; + mStopTime = -1; + mDeltaTime = 0; + mDeltaX = 0; + mDeltaY = 0; + } + + public void setRampUpDuration(int durationMillis) { + mRampUpDuration = durationMillis; + } + + public void setRampDownDuration(int durationMillis) { + mRampDownDuration = durationMillis; + } + + /** + * Starts the scroller at the current animation time. + */ + public void start() { + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStopTime = -1; + mDeltaTime = mStartTime; + mStopValue = 0.5f; + mDeltaX = 0; + mDeltaY = 0; + } + + /** + * Stops the scroller at the current animation time. + */ + public void requestStop() { + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration); + mStopValue = getValueAt(currentTime); + mStopTime = currentTime; + } + + public boolean isFinished() { + return mStopTime > 0 + && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown; + } + + private float getValueAt(long currentTime) { + if (currentTime < mStartTime) { + return 0f; + } else if (mStopTime < 0 || currentTime < mStopTime) { + final long elapsedSinceStart = currentTime - mStartTime; + return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1); + } else { + final long elapsedSinceEnd = currentTime - mStopTime; + return (1 - mStopValue) + mStopValue + * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1); + } + } + + /** + * Interpolates the value along a parabolic curve corresponding to the equation + * <code>y = -4x * (x-1)</code>. + * + * @param value The value to interpolate, between 0 and 1. + * @return the interpolated value, between 0 and 1. + */ + private float interpolateValue(float value) { + return -4 * value * value + 4 * value; + } + + /** + * Computes the current scroll deltas. This usually only be called after + * starting the scroller with {@link #start()}. + * + * @see #getDeltaX() + * @see #getDeltaY() + */ + public void computeScrollDelta() { + if (mDeltaTime == 0) { + throw new RuntimeException("Cannot compute scroll delta before calling start()"); + } + + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + final float value = getValueAt(currentTime); + final float scale = interpolateValue(value); + 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; + } + + public int getHorizontalDirection() { + return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX)); + } + + public int getVerticalDirection() { + return (int) (mTargetVelocityY / Math.abs(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; + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/AutoScroller.java b/src/com/android/launcher3/AutoScroller.java deleted file mode 100644 index ac8e2e61a..000000000 --- a/src/com/android/launcher3/AutoScroller.java +++ /dev/null @@ -1,423 +0,0 @@ -/* - * 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/Folder.java b/src/com/android/launcher3/Folder.java index f57ff07a9..33d300cad 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -25,6 +25,7 @@ import android.content.res.Resources; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.SystemClock; import android.text.InputType; import android.text.Selection; import android.text.Spannable; @@ -107,8 +108,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList private float mFolderIconPivotX; private float mFolderIconPivotY; - private static final float MAX_SCROLL_VELOCITY = 1500f; - private boolean mIsEditingName = false; private InputMethodManager mInputMethodManager; @@ -121,7 +120,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList private boolean mDestroyed; - private AutoScroller mAutoScroller; + private AutoScrollHelper mAutoScrollHelper; private Runnable mDeferredAction; private boolean mDeferDropAfterUninstall; @@ -193,9 +192,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList 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); + mAutoScrollHelper = new FolderAutoScrollHelper(mScrollView); } private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { @@ -626,24 +623,29 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList 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(); - float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, null); + final DragView dragView = d.dragView; + final int scrollOffset = mScrollView.getScrollY(); + final float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, 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); + final long downTime = SystemClock.uptimeMillis(); + final MotionEvent translatedEv = MotionEvent.obtain( + downTime, downTime, MotionEvent.ACTION_MOVE, d.x, d.y, 0); + + if (!mAutoScrollHelper.isEnabled()) { + mAutoScrollHelper.setEnabled(true); + } + + final boolean handled = mAutoScrollHelper.onTouch(this, translatedEv); + translatedEv.recycle(); + + if (handled) { + mReorderAlarm.cancelAlarm(); + } else { + mTargetCell = mContent.findNearestArea( + (int) r[0], (int) r[1] + scrollOffset, 1, 1, mTargetCell); if (isLayoutRtl()) { mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1; } @@ -658,8 +660,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } else { mDragMode = DRAG_MODE_NONE; } - } else { - mReorderAlarm.cancelAlarm(); } } @@ -705,7 +705,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void onDragExit(DragObject d) { // Exiting folder; stop the auto scroller. - mAutoScroller.stop(); + mAutoScrollHelper.setEnabled(false); // 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) { diff --git a/src/com/android/launcher3/FolderAutoScrollHelper.java b/src/com/android/launcher3/FolderAutoScrollHelper.java new file mode 100644 index 000000000..d7e677053 --- /dev/null +++ b/src/com/android/launcher3/FolderAutoScrollHelper.java @@ -0,0 +1,56 @@ +/* + * 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.widget.ScrollView; + +/** + * An implementation of {@link AutoScrollHelper} that knows how to scroll + * through a {@link Folder}. + */ +public class FolderAutoScrollHelper extends AutoScrollHelper { + private static final float MAX_SCROLL_VELOCITY = 1500f; + + private final ScrollView mTarget; + + public FolderAutoScrollHelper(ScrollView target) { + super(target); + + mTarget = target; + + setActivationDelay(0); + setEdgeType(EDGE_TYPE_INSIDE); + setExclusive(true); + setMaximumVelocity(MAX_SCROLL_VELOCITY, MAX_SCROLL_VELOCITY); + } + + @Override + public void scrollTargetBy(int deltaX, int deltaY) { + mTarget.scrollBy(deltaX, deltaY); + } + + @Override + public boolean canTargetScrollHorizontally(int direction) { + // List do not scroll horizontally. + return false; + } + + @Override + public boolean canTargetScrollVertically(int direction) { + return mTarget.canScrollVertically(direction); + } +}
\ No newline at end of file |