diff options
| author | 2010-11-15 12:19:35 -0800 | |
|---|---|---|
| committer | 2010-11-17 12:16:09 -0800 | |
| commit | d348bb4feff72d047a1037537be2d334a00c380c (patch) | |
| tree | eaf13500e873b3be4b4de9353affafeb24277bd3 | |
| parent | 77a6be41252e1b66083ade785a207be5f70aca7c (diff) | |
Changes to scrolling physics
Spline curve for Scroller fling motion (debunne)
Flywheel motion for AbsListView
Change-Id: Ic1f226878745ff4c302dc6bd0752868fa182dd7b
| -rw-r--r-- | api/current.xml | 14 | ||||
| -rw-r--r-- | core/java/android/widget/AbsListView.java | 129 | ||||
| -rw-r--r-- | core/java/android/widget/Scroller.java | 101 |
3 files changed, 182 insertions, 62 deletions
diff --git a/api/current.xml b/api/current.xml index 7688c08bb5ff..a66acbc8d2f4 100644 --- a/api/current.xml +++ b/api/current.xml @@ -239966,6 +239966,20 @@ <parameter name="interpolator" type="android.view.animation.Interpolator"> </parameter> </constructor> +<constructor name="Scroller" + type="android.widget.Scroller" + 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="flywheel" type="boolean"> +</parameter> +</constructor> <method name="abortAnimation" return="void" abstract="false" diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 466e54135e65..ffe78e713d4b 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -2543,6 +2543,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mMotionCorrection = 0; motionPosition = findMotionRow(y); reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); + mFlingRunnable.flywheelTouch(); } } } @@ -2715,6 +2716,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } else { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + if (mFlingRunnable != null) { + mFlingRunnable.endFling(); + } } } } else { @@ -2758,7 +2762,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te case MotionEvent.ACTION_CANCEL: { mTouchMode = TOUCH_MODE_REST; setPressed(false); - View motionView = this.getChildAt(mMotionPosition - mFirstPosition); + View motionView = getChildAt(mMotionPosition - mFirstPosition); if (motionView != null) { motionView.setPressed(false); } @@ -2946,6 +2950,30 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ private int mLastFlingY; + private final Runnable mCheckFlywheel = new Runnable() { + public void run() { + final int activeId = mActivePointerId; + final VelocityTracker vt = mVelocityTracker; + final Scroller scroller = mScroller; + if (vt == null || activeId == INVALID_POINTER) { + return; + } + + vt.computeCurrentVelocity(1000, mMaximumVelocity); + final float yvel = -vt.getYVelocity(activeId); + + if (scroller.isScrollingInDirection(0, yvel)) { + // Keep the fling alive a little longer + postDelayed(this, FLYWHEEL_TIMEOUT); + } else { + endFling(); + mTouchMode = TOUCH_MODE_SCROLL; + } + } + }; + + private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds + FlingRunnable() { mScroller = new Scroller(getContext()); } @@ -2978,73 +3006,85 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te post(this); } - private void endFling() { + void endFling() { mTouchMode = TOUCH_MODE_REST; removeCallbacks(this); + removeCallbacks(mCheckFlywheel); if (mPositionScroller != null) { removeCallbacks(mPositionScroller); } reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); clearScrollingCache(); + mScroller.abortAnimation(); + } + + void flywheelTouch() { + postDelayed(mCheckFlywheel, FLYWHEEL_TIMEOUT); } public void run() { switch (mTouchMode) { default: + endFling(); return; - case TOUCH_MODE_FLING: { + case TOUCH_MODE_SCROLL: + if (mScroller.isFinished()) { + return; + } + // Fall through + case TOUCH_MODE_FLING: if (mItemCount == 0 || getChildCount() == 0) { endFling(); return; } + break; + } + final Scroller scroller = mScroller; + boolean more = scroller.computeScrollOffset(); + final int y = scroller.getCurrY(); - final Scroller scroller = mScroller; - boolean more = scroller.computeScrollOffset(); - final int y = scroller.getCurrY(); - - // Flip sign to convert finger direction to list items direction - // (e.g. finger moving down means list is moving towards the top) - int delta = mLastFlingY - y; - - // Pretend that each frame of a fling scroll is a touch scroll - if (delta > 0) { - // List is moving towards the top. Use first view as mMotionPosition - mMotionPosition = mFirstPosition; - final View firstView = getChildAt(0); - mMotionViewOriginalTop = firstView.getTop(); - - // Don't fling more than 1 screen - delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta); - } else { - // List is moving towards the bottom. Use last view as mMotionPosition - int offsetToLast = getChildCount() - 1; - mMotionPosition = mFirstPosition + offsetToLast; + // Flip sign to convert finger direction to list items direction + // (e.g. finger moving down means list is moving towards the top) + int delta = mLastFlingY - y; - final View lastView = getChildAt(offsetToLast); - mMotionViewOriginalTop = lastView.getTop(); + // Pretend that each frame of a fling scroll is a touch scroll + if (delta > 0) { + // List is moving towards the top. Use first view as mMotionPosition + mMotionPosition = mFirstPosition; + final View firstView = getChildAt(0); + mMotionViewOriginalTop = firstView.getTop(); - // Don't fling more than 1 screen - delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta); - } + // Don't fling more than 1 screen + delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta); + } else { + // List is moving towards the bottom. Use last view as mMotionPosition + int offsetToLast = getChildCount() - 1; + mMotionPosition = mFirstPosition + offsetToLast; - // Don't stop just because delta is zero (it could have been rounded) - final boolean atEnd = trackMotionScroll(delta, delta) && (delta != 0); + final View lastView = getChildAt(offsetToLast); + mMotionViewOriginalTop = lastView.getTop(); - if (more && !atEnd) { - invalidate(); - mLastFlingY = y; - post(this); - } else { - endFling(); - - if (PROFILE_FLINGING) { - if (mFlingProfilingStarted) { - Debug.stopMethodTracing(); - mFlingProfilingStarted = false; - } + // Don't fling more than 1 screen + delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta); + } + + // Don't stop just because delta is zero (it could have been rounded) + final boolean atEnd = trackMotionScroll(delta, delta) && (delta != 0); + + if (more && !atEnd) { + invalidate(); + mLastFlingY = y; + post(this); + } else { + endFling(); + + if (PROFILE_FLINGING) { + if (mFlingProfilingStarted) { + Debug.stopMethodTracing(); + mFlingProfilingStarted = false; } if (mFlingStrictSpan != null) { @@ -3052,10 +3092,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mFlingStrictSpan = null; } } - break; } - } - } } diff --git a/core/java/android/widget/Scroller.java b/core/java/android/widget/Scroller.java index 79ab44890af2..3f2bd25d3dc1 100644 --- a/core/java/android/widget/Scroller.java +++ b/core/java/android/widget/Scroller.java @@ -18,6 +18,8 @@ package android.widget; import android.content.Context; import android.hardware.SensorManager; +import android.os.Build; +import android.util.FloatMath; import android.view.ViewConfiguration; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; @@ -54,6 +56,7 @@ public class Scroller { private float mViscousFluidNormalize; private boolean mFinished; private Interpolator mInterpolator; + private boolean mFlywheel; private float mCoeffX = 0.0f; private float mCoeffY = 1.0f; @@ -63,9 +66,37 @@ public class Scroller { private static final int SCROLL_MODE = 0; private static final int FLING_MODE = 1; + private static float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9)); + private static float ALPHA = 400; // 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 = new float[NB_SAMPLES + 1]; + + private float mDeceleration; private final float mPpi; + static { + float x_min = 0.0f; + for (int i = 0; i <= NB_SAMPLES; i++) { + final float t = (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 - t) < 1E-5) break; + if (tx > t) x_max = x; + else x_min = x; + } + final float d = coef + x * x * x; + SPLINE[i] = d; + } + SPLINE[NB_SAMPLES] = 1.0f; + } + /** * Create a Scroller with the default duration and interpolator. */ @@ -73,22 +104,28 @@ public class Scroller { this(context, null); } + public Scroller(Context context, Interpolator interpolator) { + this(context, interpolator, + context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); + } + /** * Create a Scroller with the specified interpolator. If the interpolator is * null, the default (viscous) interpolator will be used. */ - public Scroller(Context context, Interpolator interpolator) { + public Scroller(Context context, Interpolator interpolator, boolean flywheel) { mFinished = true; mInterpolator = interpolator; mPpi = context.getResources().getDisplayMetrics().density * 160.0f; mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); + mFlywheel = flywheel; } /** * The amount of friction applied to flings. The default value * is {@link ViewConfiguration#getScrollFriction}. * - * @return A scalar dimensionless value representing the coefficient of + * @param friction A scalar dimension-less value representing the coefficient of * friction. */ public final void setFriction(float friction) { @@ -210,7 +247,7 @@ public class Scroller { if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: - float x = (float)timePassed * mDurationReciprocal; + float x = timePassed * mDurationReciprocal; if (mInterpolator == null) x = viscousFluid(x); @@ -221,16 +258,20 @@ public class Scroller { mCurrY = mStartY + Math.round(x * mDeltaY); break; case FLING_MODE: - float timePassedSeconds = timePassed / 1000.0f; - float distance = (mVelocity * timePassedSeconds) - - (mDeceleration * timePassedSeconds * timePassedSeconds / 2.0f); + final float t = (float) timePassed / mDuration; + final int index = (int) (NB_SAMPLES * t); + final float t_inf = (float) index / NB_SAMPLES; + final float t_sup = (float) (index + 1) / NB_SAMPLES; + final float d_inf = SPLINE[index]; + final float d_sup = SPLINE[index + 1]; + final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf); - mCurrX = mStartX + Math.round(distance * mCoeffX); + mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); // Pin to mMinX <= mCurrX <= mMaxX mCurrX = Math.min(mCurrX, mMaxX); mCurrX = Math.max(mCurrX, mMinX); - mCurrY = mStartY + Math.round(distance * mCoeffY); + mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); // Pin to mMinY <= mCurrY <= mMaxY mCurrY = Math.min(mCurrY, mMaxY); mCurrY = Math.max(mCurrY, mMinY); @@ -292,7 +333,7 @@ public class Scroller { mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; - mDurationReciprocal = 1.0f / (float) mDuration; + mDurationReciprocal = 1.0f / mDuration; // This controls the viscous fluid effect (how much of it) mViscousFluidScale = 8.0f; // must be set to 1.0 (used in viscousFluid()) @@ -321,14 +362,34 @@ public class Scroller { */ public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) { + // Continue a scroll or fling in progress + if (mFlywheel && !mFinished) { + float oldVel = getCurrVelocity(); + + float dx = (float) (mFinalX - mStartX); + float dy = (float) (mFinalY - mStartY); + float hyp = FloatMath.sqrt(dx * dx + dy * dy); + + float ndx = dx / hyp; + float ndy = dy / hyp; + + float oldVelocityX = ndx * oldVel; + float oldVelocityY = ndy * oldVel; + if (Math.signum(velocityX) == Math.signum(oldVelocityX) && + Math.signum(velocityY) == Math.signum(oldVelocityY)) { + velocityX += oldVelocityX; + velocityY += oldVelocityY; + } + } + mMode = FLING_MODE; mFinished = false; - float velocity = (float)Math.hypot(velocityX, velocityY); + float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); mVelocity = velocity; - mDuration = (int) (1000 * velocity / mDeceleration); // Duration is in - // milliseconds + final double l = Math.log(START_TENSION * velocity / ALPHA); + mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0))); mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; @@ -336,14 +397,14 @@ public class Scroller { mCoeffX = velocity == 0 ? 1.0f : velocityX / velocity; mCoeffY = velocity == 0 ? 1.0f : velocityY / velocity; - int totalDistance = (int) ((velocity * velocity) / (2 * mDeceleration)); + int totalDistance = + (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)); mMinX = minX; mMaxX = maxX; mMinY = minY; mMaxY = maxY; - - + mFinalX = startX + Math.round(totalDistance * mCoeffX); // Pin to mMinX <= mFinalX <= mMaxX mFinalX = Math.min(mFinalX, mMaxX); @@ -395,7 +456,7 @@ public class Scroller { public void extendDuration(int extend) { int passed = timePassed(); mDuration = passed + extend; - mDurationReciprocal = 1.0f / (float)mDuration; + mDurationReciprocal = 1.0f / mDuration; mFinished = false; } @@ -433,4 +494,12 @@ public class Scroller { mDeltaY = mFinalY - mStartY; mFinished = false; } + + /** + * @hide + */ + public boolean isScrollingInDirection(float xvel, float yvel) { + return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && + Math.signum(yvel) == Math.signum(mFinalY - mStartY); + } } |