From fb1a07967e9b89476fb44c6cb9a8c568fdd73f13 Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Fri, 1 Sep 2023 12:13:24 +0200 Subject: [PATCH] copy android.widget.Scroller from AOSP and implement missing scroll APIs This is needed to make ViewPager functional --- src/api-impl-jni/util.c | 3 + src/api-impl-jni/util.h | 3 + .../views/android_view_ViewGroup.c | 8 +- .../android/hardware/SensorManager.java | 3 + src/api-impl/android/view/View.java | 31 +- .../android/view/ViewConfiguration.java | 4 + src/api-impl/android/widget/OverScroller.java | 15 +- src/api-impl/android/widget/Scroller.java | 564 +++++++++++++++++- 8 files changed, 614 insertions(+), 17 deletions(-) diff --git a/src/api-impl-jni/util.c b/src/api-impl-jni/util.c index 6558f4e5..e49fb1ba 100644 --- a/src/api-impl-jni/util.c +++ b/src/api-impl-jni/util.c @@ -116,6 +116,9 @@ void set_up_handle_cache(JNIEnv *env) handle_cache.view.getSuggestedMinimumHeight = _METHOD(handle_cache.view.class, "getSuggestedMinimumHeight", "()I"); handle_cache.view.setMeasuredDimension = _METHOD(handle_cache.view.class, "setMeasuredDimension", "(II)V"); handle_cache.view.onGenericMotionEvent = _METHOD(handle_cache.view.class, "onGenericMotionEvent", "(Landroid/view/MotionEvent;)Z"); + handle_cache.view.computeScroll = _METHOD(handle_cache.view.class, "computeScroll", "()V"); + handle_cache.view.getScrollX = _METHOD(handle_cache.view.class, "getScrollX", "()I"); + handle_cache.view.getScrollY = _METHOD(handle_cache.view.class, "getScrollY", "()I"); handle_cache.asset_manager.class = _REF((*env)->FindClass(env, "android/content/res/AssetManager")); handle_cache.asset_manager.extractFromAPK = _STATIC_METHOD(handle_cache.asset_manager.class, "extractFromAPK", "(Ljava/lang/String;Ljava/lang/String;)V"); diff --git a/src/api-impl-jni/util.h b/src/api-impl-jni/util.h index 065d2e33..6cfc3db0 100644 --- a/src/api-impl-jni/util.h +++ b/src/api-impl-jni/util.h @@ -77,6 +77,9 @@ struct handle_cache { jmethodID getSuggestedMinimumHeight; jmethodID setMeasuredDimension; jmethodID onGenericMotionEvent; + jmethodID computeScroll; + jmethodID getScrollX; + jmethodID getScrollY; } view; struct { jclass class; diff --git a/src/api-impl-jni/views/android_view_ViewGroup.c b/src/api-impl-jni/views/android_view_ViewGroup.c index a0511fb2..26fc8f5e 100644 --- a/src/api-impl-jni/views/android_view_ViewGroup.c +++ b/src/api-impl-jni/views/android_view_ViewGroup.c @@ -45,7 +45,13 @@ static void android_layout_allocate(GtkLayoutManager *layout_manager, GtkWidget if((*env)->ExceptionCheck(env)) (*env)->ExceptionDescribe(env); - (*env)->CallVoidMethod(env, layout->view, handle_cache.view.onLayout, TRUE, 0, 0, width, height); + (*env)->CallVoidMethod(env, layout->view, handle_cache.view.computeScroll); + if((*env)->ExceptionCheck(env)) + (*env)->ExceptionDescribe(env); + int scroll_x = (*env)->CallIntMethod(env, layout->view, handle_cache.view.getScrollX); + int scroll_y = (*env)->CallIntMethod(env, layout->view, handle_cache.view.getScrollY); + + (*env)->CallVoidMethod(env, layout->view, handle_cache.view.onLayout, TRUE, scroll_x, scroll_y, width, height); if((*env)->ExceptionCheck(env)) (*env)->ExceptionDescribe(env); } diff --git a/src/api-impl/android/hardware/SensorManager.java b/src/api-impl/android/hardware/SensorManager.java index 1a33193d..e10ceb5a 100644 --- a/src/api-impl/android/hardware/SensorManager.java +++ b/src/api-impl/android/hardware/SensorManager.java @@ -6,6 +6,9 @@ import android.location.LocationManager; import android.os.Handler; public class SensorManager { + + public static float GRAVITY_EARTH = 9.81f; + public Sensor getDefaultSensor(int type) { return new Sensor(type); } diff --git a/src/api-impl/android/view/View.java b/src/api-impl/android/view/View.java index 1847ba62..f17c0170 100644 --- a/src/api-impl/android/view/View.java +++ b/src/api-impl/android/view/View.java @@ -788,6 +788,9 @@ public class View extends Object { private int right; private int bottom; + private int scrollX = 0; + private int scrollY = 0; + public long widget; // pointer public static HashMap view_by_id = new HashMap(); @@ -935,10 +938,17 @@ public class View extends Object { public void getHitRect(Rect outRect) {} public final boolean getLocalVisibleRect(Rect r) { return false; } - public final int getScrollX() { return 0; } - public final int getScrollY() { return 0; } + public final int getScrollX() { + return scrollX; + } + public final int getScrollY() { + return scrollY; + } - public void scrollTo(int x, int y) {} + public void scrollTo(int x, int y) { + scrollX = x; + scrollY = y; + } protected void onScrollChanged(int l, int t, int oldl, int oldt) {} @@ -972,7 +982,14 @@ public class View extends Object { return 0; } - public void postInvalidate() {} + public void postInvalidate() { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + requestLayout(); + } + }); + } public void postInvalidate(int left, int top, int right, int bottom) { System.out.println("postInvalidate(" + left + "," + top + "," + right + "," + bottom + ") called"); @@ -1108,6 +1125,10 @@ public class View extends Object { layout(left, top + offset, right, bottom + offset); } + public void offsetLeftAndRight(int offset) { + layout(left + offset, top, right + offset, bottom); + } + public void setBackgroundDrawable(Drawable backgroundDrawable) {} public int getOverScrollMode() {return 0;} @@ -1228,4 +1249,6 @@ public class View extends Object { } return result; } + + public void computeScroll() {} } diff --git a/src/api-impl/android/view/ViewConfiguration.java b/src/api-impl/android/view/ViewConfiguration.java index f6394ee9..b12e56f4 100644 --- a/src/api-impl/android/view/ViewConfiguration.java +++ b/src/api-impl/android/view/ViewConfiguration.java @@ -43,4 +43,8 @@ public class ViewConfiguration { public int getScaledDoubleTapSlop() { return 0; } + + public static float getScrollFriction() { + return 0.f; + } } diff --git a/src/api-impl/android/widget/OverScroller.java b/src/api-impl/android/widget/OverScroller.java index 97a31ed0..54a019c5 100644 --- a/src/api-impl/android/widget/OverScroller.java +++ b/src/api-impl/android/widget/OverScroller.java @@ -1,16 +1,25 @@ package android.widget; import android.content.Context; -import android.view.ViewGroup; import android.view.animation.Interpolator; -public class OverScroller extends ViewGroup { +public class OverScroller { public OverScroller(Context context, Interpolator interpolator) { - super(context); + } + + public OverScroller(Context context) { } public void abortAnimation () {} public void startScroll(int startX, int startY, int dx, int dy, int duration) {} + + public boolean computeScrollOffset() {return false;} + + public int getCurrX() {return 0;} + public int getCurrY() {return 0;} + + public int getFinalX() {return 0;} + public int getFinalY() {return 0;} } diff --git a/src/api-impl/android/widget/Scroller.java b/src/api-impl/android/widget/Scroller.java index cae7bc48..8d03e915 100644 --- a/src/api-impl/android/widget/Scroller.java +++ b/src/api-impl/android/widget/Scroller.java @@ -1,22 +1,568 @@ +/* + * Copyright (C) 2006 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 android.widget; import android.content.Context; -import android.util.AttributeSet; -import android.view.View; +import android.hardware.SensorManager; +import android.os.Build; +import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; -public class Scroller extends View { +/** + *

This class encapsulates scrolling. You can use scrollers ({@link Scroller} + * or {@link OverScroller}) to collect the data you need to produce a scrolling + * animation—for example, in response to a fling gesture. Scrollers track + * scroll offsets for you over time, but they don't automatically apply those + * positions to your view. It's your responsibility to get and apply new + * coordinates at a rate that will make the scrolling animation look smooth.

+ * + *

Here is a simple example:

+ * + *
 private Scroller mScroller = new Scroller(context);
+ * ...
+ * public void zoomIn() {
+ *     // Revert any animation currently in progress
+ *     mScroller.forceFinished(true);
+ *     // Start scrolling by providing a starting point and
+ *     // the distance to travel
+ *     mScroller.startScroll(0, 0, 100, 0);
+ *     // Invalidate to request a redraw
+ *     invalidate();
+ * }
+ * + *

To track the changing positions of the x/y coordinates, use + * {@link #computeScrollOffset}. The method returns a boolean to indicate + * whether the scroller is finished. If it isn't, it means that a fling or + * programmatic pan operation is still in progress. You can use this method to + * find the current offsets of the x and y coordinates, for example:

+ * + *
if (mScroller.computeScrollOffset()) {
+ *     // Get current x and y positions
+ *     int currX = mScroller.getCurrX();
+ *     int currY = mScroller.getCurrY();
+ *    ...
+ * }
+ */ +public class Scroller { + private final Interpolator mInterpolator; + private int mMode; + private int mStartX; + private int mStartY; + private int mFinalX; + private int mFinalY; + private int mMinX; + private int mMaxX; + private int mMinY; + private int mMaxY; + private int mCurrX; + private int mCurrY; + private long mStartTime; + private int mDuration; + private float mDurationReciprocal; + private float mDeltaX; + private float mDeltaY; + private boolean mFinished; + private boolean mFlywheel; + private float mVelocity; + private float mCurrVelocity; + private int mDistance; + private float mFlingFriction = ViewConfiguration.getScrollFriction(); + private static final int DEFAULT_DURATION = 250; + private static final int SCROLL_MODE = 0; + private static final int FLING_MODE = 1; + private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); + private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) + private static final float START_TENSION = 0.5f; + private static final float END_TENSION = 1.0f; + 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 float mDeceleration; + private final float mPpi; + // A context-specific coefficient adjusted to physical values. + private float mPhysicalCoeff; + 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) * 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; + } + + /** + * Create a Scroller with the default duration and interpolator. + */ public Scroller(Context context) { - super(context); - } - - public Scroller(Context context, AttributeSet attributeSet) { - super(context, attributeSet); + this(context, null); } + /** + * Create a Scroller with the specified interpolator. If the interpolator is + * null, the default (viscous) interpolator will be used. "Flywheel" behavior will + * be in effect for apps targeting Honeycomb or newer. + */ public Scroller(Context context, Interpolator interpolator) { - this(context); + 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. Specify whether or + * not to support progressive "flywheel" behavior in flinging. + */ + public Scroller(Context context, Interpolator interpolator, boolean flywheel) { + mFinished = true; + if (interpolator == null) { + mInterpolator = new ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } + mPpi = context.getResources().getDisplayMetrics().density * 160.0f; + mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); + mFlywheel = flywheel; + mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning + } + + /** + * The amount of friction applied to flings. The default value + * is {@link ViewConfiguration#getScrollFriction}. + * + * @param friction A scalar dimension-less value representing the coefficient of + * friction. + */ + public final void setFriction(float friction) { + mDeceleration = computeDeceleration(friction); + mFlingFriction = friction; + } + + private float computeDeceleration(float friction) { + return SensorManager.GRAVITY_EARTH // g (m/s^2) + * 39.37f // inch/meter + * mPpi // pixels per inch + * friction; + } + + /** + * + * Returns whether the scroller has finished scrolling. + * + * @return True if the scroller has finished scrolling, false otherwise. + */ + public final boolean isFinished() { + return mFinished; + } + + /** + * Force the finished field to a particular value. + * + * @param finished The new finished value. + */ + public final void forceFinished(boolean finished) { + mFinished = finished; + } + + /** + * Returns how long the scroll event will take, in milliseconds. + * + * @return The duration of the scroll in milliseconds. + */ + public final int getDuration() { + return mDuration; + } + + /** + * Returns the current X offset in the scroll. + * + * @return The new X offset as an absolute distance from the origin. + */ + public final int getCurrX() { + return mCurrX; + } + + /** + * Returns the current Y offset in the scroll. + * + * @return The new Y offset as an absolute distance from the origin. + */ + public final int getCurrY() { + return mCurrY; + } + + /** + * Returns the current velocity. + * + * @return The original velocity less the deceleration. Result may be + * negative. + */ + public float getCurrVelocity() { + return mMode == FLING_MODE ? + mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f; + } + + /** + * Returns the start X offset in the scroll. + * + * @return The start X offset as an absolute distance from the origin. + */ + public final int getStartX() { + return mStartX; + } + + /** + * Returns the start Y offset in the scroll. + * + * @return The start Y offset as an absolute distance from the origin. + */ + public final int getStartY() { + return mStartY; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final X offset as an absolute distance from the origin. + */ + public final int getFinalX() { + return mFinalX; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final Y offset as an absolute distance from the origin. + */ + public final int getFinalY() { + return mFinalY; + } + + /** + * Call this when you want to know the new location. If it returns true, + * the animation is not yet finished. + */ + public boolean computeScrollOffset() { + if (mFinished) { + return false; + } + int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); + + if (timePassed < mDuration) { + switch (mMode) { + case SCROLL_MODE: + final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); + mCurrX = mStartX + Math.round(x * mDeltaX); + mCurrY = mStartY + Math.round(x * mDeltaY); + break; + case FLING_MODE: + final float t = (float) timePassed / mDuration; + final int index = (int) (NB_SAMPLES * t); + float distanceCoef = 1.f; + float velocityCoef = 0.f; + if (index < NB_SAMPLES) { + final float t_inf = (float) index / NB_SAMPLES; + final float t_sup = (float) (index + 1) / NB_SAMPLES; + final float d_inf = SPLINE_POSITION[index]; + final float d_sup = SPLINE_POSITION[index + 1]; + velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); + distanceCoef = d_inf + (t - t_inf) * velocityCoef; + } + mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; + + 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(distanceCoef * (mFinalY - mStartY)); + // Pin to mMinY <= mCurrY <= mMaxY + mCurrY = Math.min(mCurrY, mMaxY); + mCurrY = Math.max(mCurrY, mMinY); + if (mCurrX == mFinalX && mCurrY == mFinalY) { + mFinished = true; + } + break; + } + } + else { + mCurrX = mFinalX; + mCurrY = mFinalY; + mFinished = true; + } + return true; + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * The scroll will use the default value of 250 milliseconds for the + * duration. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + */ + public void startScroll(int startX, int startY, int dx, int dy) { + startScroll(startX, startY, dx, dy, DEFAULT_DURATION); + } + + /** + * Start scrolling by providing a starting point, the distance to travel, + * and the duration of the scroll. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + * @param duration Duration of the scroll in milliseconds. + */ + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + mMode = SCROLL_MODE; + mFinished = false; + mDuration = duration; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStartX = startX; + mStartY = startY; + mFinalX = startX + dx; + mFinalY = startY + dy; + mDeltaX = dx; + mDeltaY = dy; + mDurationReciprocal = 1.0f / (float) mDuration; + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point. + * @param maxX Maximum X value. The scroller will not scroll past this + * point. + * @param minY Minimum Y value. The scroller will not scroll past this + * point. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point. + */ + 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 = (float) Math.hypot(dx, 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); + + mVelocity = velocity; + mDuration = getSplineFlingDuration(velocity); + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStartX = startX; + mStartY = startY; + float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; + float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; + double totalDistance = getSplineFlingDistance(velocity); + mDistance = (int) (totalDistance * Math.signum(velocity)); + + mMinX = minX; + mMaxX = maxX; + mMinY = minY; + mMaxY = maxY; + mFinalX = startX + (int) Math.round(totalDistance * coeffX); + // Pin to mMinX <= mFinalX <= mMaxX + mFinalX = Math.min(mFinalX, mMaxX); + mFinalX = Math.max(mFinalX, mMinX); + + mFinalY = startY + (int) Math.round(totalDistance * coeffY); + // Pin to mMinY <= mFinalY <= mMaxY + mFinalY = Math.min(mFinalY, mMaxY); + mFinalY = Math.max(mFinalY, mMinY); + } + + private double getSplineDeceleration(float velocity) { + return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); + } + + private int getSplineFlingDuration(float velocity) { + final double l = getSplineDeceleration(velocity); + final double decelMinusOne = DECELERATION_RATE - 1.0; + return (int) (1000.0 * Math.exp(l / decelMinusOne)); + } + + private double getSplineFlingDistance(float velocity) { + final double l = getSplineDeceleration(velocity); + final double decelMinusOne = DECELERATION_RATE - 1.0; + return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); + } + + /** + * Stops the animation. Contrary to {@link #forceFinished(boolean)}, + * aborting the animating cause the scroller to move to the final x and y + * position + * + * @see #forceFinished(boolean) + */ + public void abortAnimation() { + mCurrX = mFinalX; + mCurrY = mFinalY; + mFinished = true; + } + + /** + * Extend the scroll animation. This allows a running animation to scroll + * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. + * + * @param extend Additional time to scroll in milliseconds. + * @see #setFinalX(int) + * @see #setFinalY(int) + */ + public void extendDuration(int extend) { + int passed = timePassed(); + mDuration = passed + extend; + mDurationReciprocal = 1.0f / mDuration; + mFinished = false; + } + + /** + * Returns the time elapsed since the beginning of the scrolling. + * + * @return The elapsed time in milliseconds. + */ + public int timePassed() { + return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); + } + + /** + * Sets the final position (X) for this scroller. + * + * @param newX The new X offset as an absolute distance from the origin. + * @see #extendDuration(int) + * @see #setFinalY(int) + */ + public void setFinalX(int newX) { + mFinalX = newX; + mDeltaX = mFinalX - mStartX; + mFinished = false; + } + + /** + * Sets the final position (Y) for this scroller. + * + * @param newY The new Y offset as an absolute distance from the origin. + * @see #extendDuration(int) + * @see #setFinalX(int) + */ + public void setFinalY(int newY) { + mFinalY = newY; + 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); + } + + static class ViscousFluidInterpolator implements Interpolator { + /** Controls the viscous fluid effect (how much of it). */ + private static final float VISCOUS_FLUID_SCALE = 8.0f; + private static final float VISCOUS_FLUID_NORMALIZE; + private static final float VISCOUS_FLUID_OFFSET; + + static { + // must be set to 1.0 (used in viscousFluid()) + VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f); + // account for very small floating-point error + VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f); + } + + private static float viscousFluid(float x) { + x *= VISCOUS_FLUID_SCALE; + if (x < 1.0f) { + x -= (1.0f - (float)Math.exp(-x)); + } else { + float start = 0.36787944117f; // 1/e == exp(-1) + x = 1.0f - (float)Math.exp(1.0f - x); + x = start + x * (1.0f - start); + } + return x; + } + + @Override + public float getInterpolation(float input) { + final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); + if (interpolated > 0) { + return interpolated + VISCOUS_FLUID_OFFSET; + } + return interpolated; + } + } }