929 lines
34 KiB
Java
929 lines
34 KiB
Java
/*
|
|
* 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.internal.widget;
|
|
|
|
import android.content.res.Resources;
|
|
import android.os.SystemClock;
|
|
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;
|
|
import android.widget.AbsListView;
|
|
|
|
/**
|
|
* 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 AbsListViewAutoScroller} for an {@link android.widget.AbsListView}
|
|
* -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 500 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 = 500;
|
|
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 = event.getActionMasked();
|
|
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) {
|
|
mTarget.postOnAnimationDelayed(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.
|
|
mTarget.postOnAnimation(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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An implementation of {@link AutoScrollHelper} that knows how to scroll
|
|
* through an {@link AbsListView}.
|
|
*/
|
|
public static class AbsListViewAutoScroller extends AutoScrollHelper {
|
|
private final AbsListView mTarget;
|
|
|
|
public AbsListViewAutoScroller(AbsListView target) {
|
|
super(target);
|
|
|
|
mTarget = target;
|
|
}
|
|
|
|
@Override
|
|
public void scrollTargetBy(int deltaX, int deltaY) {
|
|
mTarget.scrollListBy(deltaY);
|
|
}
|
|
|
|
@Override
|
|
public boolean canTargetScrollHorizontally(int direction) {
|
|
// List do not scroll horizontally.
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean canTargetScrollVertically(int direction) {
|
|
final AbsListView target = mTarget;
|
|
final int itemCount = target.getCount();
|
|
if (itemCount == 0) {
|
|
return false;
|
|
}
|
|
|
|
final int childCount = target.getChildCount();
|
|
final int firstPosition = target.getFirstVisiblePosition();
|
|
final int lastPosition = firstPosition + childCount;
|
|
|
|
if (direction > 0) {
|
|
// Are we already showing the entire last item?
|
|
if (lastPosition >= itemCount) {
|
|
final View lastView = target.getChildAt(childCount - 1);
|
|
if (lastView.getBottom() <= target.getHeight()) {
|
|
return false;
|
|
}
|
|
}
|
|
} else if (direction < 0) {
|
|
// Are we already showing the entire first item?
|
|
if (firstPosition <= 0) {
|
|
final View firstView = target.getChildAt(0);
|
|
if (firstView.getTop() >= 0) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
// The behavior for direction 0 is undefined and we can return
|
|
// whatever we want.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|