Refactored OverScroller

Restored the interpolator and a constructor with 4 parameters.

New spline coefficients, spline computation moved to MagneticOverScroller, which has been
renamed SplineOverScroller.

Change-Id: If1ab2653bb998600e9c5d6f46dfd6cd30fa44efc
This commit is contained in:
Gilles Debunne
2010-11-23 15:34:37 -08:00
parent 12df6f72a6
commit fff4ab09b6
3 changed files with 207 additions and 110 deletions

View File

@@ -238771,7 +238771,35 @@
>
<parameter name="context" type="android.content.Context">
</parameter>
<parameter name="interpolator" type="android.graphics.Interpolator">
<parameter name="interpolator" type="android.view.animation.Interpolator">
</parameter>
</constructor>
<constructor name="OverScroller"
type="android.widget.OverScroller"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="context" type="android.content.Context">
</parameter>
<parameter name="interpolator" type="android.view.animation.Interpolator">
</parameter>
<parameter name="bounceCoefficientX" type="float">
</parameter>
<parameter name="bounceCoefficientY" type="float">
</parameter>
</constructor>
<constructor name="OverScroller"
type="android.widget.OverScroller"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="context" type="android.content.Context">
</parameter>
<parameter name="interpolator" type="android.view.animation.Interpolator">
</parameter>
<parameter name="bounceCoefficientX" type="float">
</parameter>

View File

@@ -166,7 +166,7 @@ public class ViewConfiguration {
/**
* Max distance to overfling for edge effects
*/
private static final int OVERFLING_DISTANCE = 4;
private static final int OVERFLING_DISTANCE = 12;
private final int mEdgeSlop;
private final int mFadingEdgeLength;

View File

@@ -17,9 +17,12 @@
package android.widget;
import android.content.Context;
import android.graphics.Interpolator;
import android.hardware.SensorManager;
import android.util.FloatMath;
import android.util.Log;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
/**
* This class encapsulates scrolling with the ability to overshoot the bounds
@@ -27,65 +30,51 @@ import android.view.animation.AnimationUtils;
* {@link android.widget.Scroller} in most cases.
*/
public class OverScroller {
int mMode;
private int mMode;
private final MagneticOverScroller mScrollerX;
private final MagneticOverScroller mScrollerY;
private final SplineOverScroller mScrollerX;
private final SplineOverScroller mScrollerY;
private final Interpolator mInterpolator;
private float mDeceleration;
private final float mPpi;
private final boolean mFlywheel;
private static float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9));
private static float ALPHA = 800; // pixels / seconds
private static float START_TENSION = 0.4f; // Tension at start: (0.4 * total T, 1.0 * Distance)
private static float END_TENSION = 1.0f - START_TENSION;
private static final int NB_SAMPLES = 100;
private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
private static final int DEFAULT_DURATION = 250;
private static final int SCROLL_MODE = 0;
private static final int FLING_MODE = 1;
static {
float x_min = 0.0f;
float y_min = 0.0f;
for (int i = 0; i < NB_SAMPLES; i++) {
final float alpha = (float) i / NB_SAMPLES;
{
float x_max = 1.0f;
float x, tx, coef;
while (true) {
x = x_min + (x_max - x_min) / 2.0f;
coef = 3.0f * x * (1.0f - x);
tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x;
if (Math.abs(tx - alpha) < 1E-5) break;
if (tx > alpha) x_max = x;
else x_min = x;
}
SPLINE_POSITION[i] = coef + x * x * x;
}
{
float y_max = 1.0f;
float y, dy, coef;
while (true) {
y = y_min + (y_max - y_min) / 2.0f;
coef = 3.0f * y * (1.0f - y);
dy = coef + y * y * y;
if (Math.abs(dy - alpha) < 1E-5) break;
if (dy > alpha) y_max = y;
else y_min = y;
}
SPLINE_TIME[i] = coef * ((1.0f - y) * START_TENSION + y * END_TENSION) + y * y * y;
}
}
SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
/**
* Creates an OverScroller with a viscous fluid scroll interpolator.
* @param context
*/
public OverScroller(Context context) {
this(context, null);
}
public OverScroller(Context context) {
this(context, null, 0.f, 0.f, true);
/**
* Creates an OverScroller with default edge bounce coefficients and flywheel enabled.
* @param context The context of this application.
* @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
* be used.
*/
public OverScroller(Context context, Interpolator interpolator) {
this(context, interpolator, SplineOverScroller.DEFAULT_BOUNCE_COEFFICIENT,
SplineOverScroller.DEFAULT_BOUNCE_COEFFICIENT);
}
/**
* Creates an OverScroller with flywheel enabled.
* @param context The context of this application.
* @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
* be used.
* @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
* velocity which is preserved in the bounce when the horizontal edge is reached. A null value
* means no bounce.
* @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction.
*/
public OverScroller(Context context, Interpolator interpolator,
float bounceCoefficientX, float bounceCoefficientY) {
this(context, interpolator, bounceCoefficientX, bounceCoefficientY, true);
}
/**
@@ -97,20 +86,21 @@ public class OverScroller {
* velocity which is preserved in the bounce when the horizontal edge is reached. A null value
* means no bounce.
* @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction.
* @param flywheel If true, successive fling motions will keep on increasing scroll speed.
*/
public OverScroller(Context context, Interpolator interpolator,
float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) {
mInterpolator = interpolator;
mFlywheel = flywheel;
mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
mScrollerX = new MagneticOverScroller();
mScrollerY = new MagneticOverScroller();
mScrollerX = new SplineOverScroller();
mScrollerY = new SplineOverScroller();
SplineOverScroller.initFromContext(context);
mScrollerX.setBounceCoefficient(bounceCoefficientX);
mScrollerY.setBounceCoefficient(bounceCoefficientY);
}
/**
* The amount of friction applied to flings. The default value
* is {@link ViewConfiguration#getScrollFriction}.
@@ -119,14 +109,8 @@ public class OverScroller {
* friction.
*/
public final void setFriction(float friction) {
mDeceleration = computeDeceleration(friction);
}
private float computeDeceleration(float friction) {
return 9.81f // g (m/s^2)
* 39.37f // inch/meter
* mPpi // pixels per inch
* friction;
mScrollerX.setFriction(friction);
mScrollerY.setFriction(friction);
}
/**
@@ -178,7 +162,7 @@ public class OverScroller {
public float getCurrVelocity() {
float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity;
squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity;
return (float) Math.sqrt(squaredNorm);
return FloatMath.sqrt(squaredNorm);
}
/**
@@ -307,7 +291,11 @@ public class OverScroller {
if (elapsedTime < duration) {
float q = (float) (elapsedTime) / duration;
q = Scroller.viscousFluid(q);
if (mInterpolator == null) {
q = Scroller.viscousFluid(q);
} else {
q = mInterpolator.getInterpolation(q);
}
mScrollerX.updateScroll(q);
mScrollerY.updateScroll(q);
@@ -496,9 +484,9 @@ public class OverScroller {
*/
public boolean isOverScrolled() {
return ((!mScrollerX.mFinished &&
mScrollerX.mState != MagneticOverScroller.TO_EDGE) ||
mScrollerX.mState != SplineOverScroller.TO_EDGE) ||
(!mScrollerY.mFinished &&
mScrollerY.mState != MagneticOverScroller.TO_EDGE));
mScrollerY.mState != SplineOverScroller.TO_EDGE));
}
/**
@@ -536,60 +524,126 @@ public class OverScroller {
Math.signum(yvel) == Math.signum(dy);
}
class MagneticOverScroller {
static class SplineOverScroller {
// Initial position
int mStart;
private int mStart;
// Current position
int mCurrentPosition;
private int mCurrentPosition;
// Final position
int mFinal;
private int mFinal;
// Initial velocity
int mVelocity;
private int mVelocity;
// Current velocity
float mCurrVelocity;
private float mCurrVelocity;
// Constant current deceleration
float mDeceleration;
private float mDeceleration;
// Animation starting time, in system milliseconds
long mStartTime;
private long mStartTime;
// Animation duration, in milliseconds
int mDuration;
private int mDuration;
// Duration to complete spline component of animation
int mSplineDuration;
private int mSplineDuration;
// Distance to travel along spline animation
int mSplineDistance;
private int mSplineDistance;
// Whether the animation is currently in progress
boolean mFinished;
private boolean mFinished;
// The allowed overshot distance before boundary is reached.
private int mOver;
// Fling friction
private float mFlingFriction = ViewConfiguration.getScrollFriction();
// Proportion of velocity preserved at the end of a bounce animation.
private float mBounceCoefficient = DEFAULT_BOUNCE_COEFFICIENT;
// Current state of the animation.
private int mState = TO_EDGE;
// Constant gravity value, used in the deceleration phase.
private static final float GRAVITY = 2000.0f;
// A device specific coefficient adjusted to physical values.
private static float PHYSICAL_COEF;
private static float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9));
private static final float INFLEXION = 0.3f; // Tension lines cross at (INFLEXION, 1)
private static final float START_TENSION = 0.7f;
private static final float END_TENSION = 0.8f;
private static final float P1 = START_TENSION * INFLEXION;
private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
private static final int NB_SAMPLES = 100;
private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
private static final int TO_EDGE = 0;
private static final int TO_BOUNDARY = 1;
private static final int TO_BOUNCE = 2;
private int mState = TO_EDGE;
// The allowed overshot distance before boundary is reached.
private int mOver;
// If the velocity is smaller than this value, no bounce is triggered
// when the edge limits are reached (would result in a zero pixels
// displacement anyway).
private static final float MINIMUM_VELOCITY_FOR_BOUNCE = 140.0f; //Float.MAX_VALUE;//140.0f;
// when the edge limits are reached.
private static final float MINIMUM_VELOCITY_FOR_BOUNCE = Float.MAX_VALUE;//140.0f;
// Proportion of the velocity that is preserved when the edge is reached.
private static final float DEFAULT_BOUNCE_COEFFICIENT = 0.36f;
private static final float DEFAULT_BOUNCE_COEFFICIENT = 0.16f;
private float mBounceCoefficient = DEFAULT_BOUNCE_COEFFICIENT;
static {
float x_min = 0.0f;
float y_min = 0.0f;
for (int i = 0; i < NB_SAMPLES; i++) {
final float alpha = (float) i / NB_SAMPLES;
MagneticOverScroller() {
float x_max = 1.0f;
float x, tx, coef;
while (true) {
x = x_min + (x_max - x_min) / 2.0f;
coef = 3.0f * x * (1.0f - x);
tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
if (Math.abs(tx - alpha) < 1E-5) break;
if (tx > alpha) x_max = x;
else x_min = x;
}
SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
float y_max = 1.0f;
float y, dy;
while (true) {
y = y_min + (y_max - y_min) / 2.0f;
coef = 3.0f * y * (1.0f - y);
dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
if (Math.abs(dy - alpha) < 1E-5) break;
if (dy > alpha) y_max = y;
else y_min = y;
}
SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
}
SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
}
static void initFromContext(Context context) {
final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
PHYSICAL_COEF = SensorManager.GRAVITY_EARTH // g (m/s^2)
* 39.37f // inch/meter
* ppi
* 0.84f; // look and feel tuning
}
void setFriction(float friction) {
mFlingFriction = friction;
}
SplineOverScroller() {
mFinished = true;
}
@@ -600,18 +654,18 @@ public class OverScroller {
/*
* Get a signed deceleration that will reduce the velocity.
*/
float getDeceleration(int velocity) {
return velocity > 0 ? -OverScroller.this.mDeceleration : OverScroller.this.mDeceleration;
static private float getDeceleration(int velocity) {
return velocity > 0 ? -GRAVITY : GRAVITY;
}
/*
* Modifies mDuration to the duration it takes to get from start to newFinal using the
* spline interpolation. The previous duration was needed to get to oldFinal.
*/
void adjustDuration(int start, int oldFinal, int newFinal) {
private void adjustDuration(int start, int oldFinal, int newFinal) {
final int oldDistance = oldFinal - start;
final int newDistance = newFinal - start;
final float x = (float) Math.abs((float) newDistance / oldDistance);
final float x = Math.abs((float) newDistance / oldDistance);
final int index = (int) (NB_SAMPLES * x);
if (index < NB_SAMPLES) {
final float x_inf = (float) index / NB_SAMPLES;
@@ -619,7 +673,6 @@ public class OverScroller {
final float t_inf = SPLINE_TIME[index];
final float t_sup = SPLINE_TIME[index + 1];
final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
mDuration *= timeCoef;
}
}
@@ -696,7 +749,7 @@ public class OverScroller {
mCurrVelocity = mVelocity = velocity;
mDuration = mSplineDuration = 0;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStart = start;
mCurrentPosition = mStart = start;
if (start > max || start < min) {
startAfterEdge(start, min, max, velocity);
@@ -707,10 +760,8 @@ public class OverScroller {
double totalDistance = 0.0;
if (velocity != 0) {
final double l = Math.log(START_TENSION * Math.abs(velocity) / ALPHA);
// Duration are expressed in milliseconds
mDuration = mSplineDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0)));
totalDistance = (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l));
mDuration = mSplineDuration = getSplineFlingDuration(velocity);
totalDistance = getSplineFlingDistance(velocity);
}
mSplineDistance = (int) (totalDistance * Math.signum(velocity));
@@ -728,6 +779,23 @@ public class OverScroller {
}
}
private double getSplineDeceleration(int velocity) {
return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * PHYSICAL_COEF));
}
private double getSplineFlingDistance(int velocity) {
final double l = getSplineDeceleration(velocity);
final double decelMinusOne = DECELERATION_RATE - 1.0;
return mFlingFriction * PHYSICAL_COEF * Math.exp(DECELERATION_RATE / decelMinusOne * l);
}
/* Returns the duration, expressed in milliseconds */
private int getSplineFlingDuration(int velocity) {
final double l = getSplineDeceleration(velocity);
final double decelMinusOne = DECELERATION_RATE - 1.0;
return (int) (1000.0 * Math.exp(l / decelMinusOne));
}
private void fitOnBounceCurve(int start, int end, int velocity) {
// Simulate a bounce that started from edge
final float durationToApex = - velocity / mDeceleration;
@@ -748,6 +816,7 @@ public class OverScroller {
private void startAfterEdge(int start, int min, int max, int velocity) {
if (start > min && start < max) {
Log.e("OverScroller", "startAfterEdge called from a valid position");
mFinished = true;
return;
}
@@ -759,9 +828,7 @@ public class OverScroller {
// Will result in a bounce or a to_boundary depending on velocity.
startBounceAfterEdge(start, edge, velocity);
} else {
final double l = Math.log(START_TENSION * Math.abs(velocity) / ALPHA);
final double totalDistance =
(ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l));
final double totalDistance = getSplineFlingDistance(velocity);
if (totalDistance > Math.abs(overDistance)) {
fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
} else {
@@ -771,11 +838,13 @@ public class OverScroller {
}
void notifyEdgeReached(int start, int end, int over) {
mOver = over;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
// We were in fling/scroll mode before: current velocity is such that distance to edge
// is increasing. Ensures that startAfterEdge will not start a new fling.
startAfterEdge(start, end, end, (int) mCurrVelocity);
if (mState == TO_EDGE) {
mOver = over;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
// We were in fling/scroll mode before: current velocity is such that distance to
// edge is increasing. Ensures that startAfterEdge will not start a new fling.
startAfterEdge(start, end, end, (int) mCurrVelocity);
}
}
private void onEdgeReached() {