Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import android.content.Context;
     20 import android.hardware.SensorManager;
     21 import android.os.Build;
     22 import android.util.FloatMath;
     23 import android.view.ViewConfiguration;
     24 import android.view.animation.AnimationUtils;
     25 import android.view.animation.Interpolator;
     26 
     27 
     28 /**
     29  * <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller}
     30  * or {@link OverScroller}) to collect the data you need to produce a scrolling
     31  * animation&mdash;for example, in response to a fling gesture. Scrollers track
     32  * scroll offsets for you over time, but they don't automatically apply those
     33  * positions to your view. It's your responsibility to get and apply new
     34  * coordinates at a rate that will make the scrolling animation look smooth.</p>
     35  *
     36  * <p>Here is a simple example:</p>
     37  *
     38  * <pre> private Scroller mScroller = new Scroller(context);
     39  * ...
     40  * public void zoomIn() {
     41  *     // Revert any animation currently in progress
     42  *     mScroller.forceFinished(true);
     43  *     // Start scrolling by providing a starting point and
     44  *     // the distance to travel
     45  *     mScroller.startScroll(0, 0, 100, 0);
     46  *     // Invalidate to request a redraw
     47  *     invalidate();
     48  * }</pre>
     49  *
     50  * <p>To track the changing positions of the x/y coordinates, use
     51  * {@link #computeScrollOffset}. The method returns a boolean to indicate
     52  * whether the scroller is finished. If it isn't, it means that a fling or
     53  * programmatic pan operation is still in progress. You can use this method to
     54  * find the current offsets of the x and y coordinates, for example:</p>
     55  *
     56  * <pre>if (mScroller.computeScrollOffset()) {
     57  *     // Get current x and y positions
     58  *     int currX = mScroller.getCurrX();
     59  *     int currY = mScroller.getCurrY();
     60  *    ...
     61  * }</pre>
     62  */
     63 public class Scroller  {
     64     private int mMode;
     65 
     66     private int mStartX;
     67     private int mStartY;
     68     private int mFinalX;
     69     private int mFinalY;
     70 
     71     private int mMinX;
     72     private int mMaxX;
     73     private int mMinY;
     74     private int mMaxY;
     75 
     76     private int mCurrX;
     77     private int mCurrY;
     78     private long mStartTime;
     79     private int mDuration;
     80     private float mDurationReciprocal;
     81     private float mDeltaX;
     82     private float mDeltaY;
     83     private boolean mFinished;
     84     private Interpolator mInterpolator;
     85     private boolean mFlywheel;
     86 
     87     private float mVelocity;
     88     private float mCurrVelocity;
     89     private int mDistance;
     90 
     91     private float mFlingFriction = ViewConfiguration.getScrollFriction();
     92 
     93     private static final int DEFAULT_DURATION = 250;
     94     private static final int SCROLL_MODE = 0;
     95     private static final int FLING_MODE = 1;
     96 
     97     private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
     98     private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
     99     private static final float START_TENSION = 0.5f;
    100     private static final float END_TENSION = 1.0f;
    101     private static final float P1 = START_TENSION * INFLEXION;
    102     private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
    103 
    104     private static final int NB_SAMPLES = 100;
    105     private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
    106     private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
    107 
    108     private float mDeceleration;
    109     private final float mPpi;
    110 
    111     // A context-specific coefficient adjusted to physical values.
    112     private float mPhysicalCoeff;
    113 
    114     static {
    115         float x_min = 0.0f;
    116         float y_min = 0.0f;
    117         for (int i = 0; i < NB_SAMPLES; i++) {
    118             final float alpha = (float) i / NB_SAMPLES;
    119 
    120             float x_max = 1.0f;
    121             float x, tx, coef;
    122             while (true) {
    123                 x = x_min + (x_max - x_min) / 2.0f;
    124                 coef = 3.0f * x * (1.0f - x);
    125                 tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
    126                 if (Math.abs(tx - alpha) < 1E-5) break;
    127                 if (tx > alpha) x_max = x;
    128                 else x_min = x;
    129             }
    130             SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
    131 
    132             float y_max = 1.0f;
    133             float y, dy;
    134             while (true) {
    135                 y = y_min + (y_max - y_min) / 2.0f;
    136                 coef = 3.0f * y * (1.0f - y);
    137                 dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
    138                 if (Math.abs(dy - alpha) < 1E-5) break;
    139                 if (dy > alpha) y_max = y;
    140                 else y_min = y;
    141             }
    142             SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
    143         }
    144         SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
    145 
    146         // This controls the viscous fluid effect (how much of it)
    147         sViscousFluidScale = 8.0f;
    148         // must be set to 1.0 (used in viscousFluid())
    149         sViscousFluidNormalize = 1.0f;
    150         sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
    151 
    152     }
    153 
    154     private static float sViscousFluidScale;
    155     private static float sViscousFluidNormalize;
    156 
    157     /**
    158      * Create a Scroller with the default duration and interpolator.
    159      */
    160     public Scroller(Context context) {
    161         this(context, null);
    162     }
    163 
    164     /**
    165      * Create a Scroller with the specified interpolator. If the interpolator is
    166      * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
    167      * be in effect for apps targeting Honeycomb or newer.
    168      */
    169     public Scroller(Context context, Interpolator interpolator) {
    170         this(context, interpolator,
    171                 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    172     }
    173 
    174     /**
    175      * Create a Scroller with the specified interpolator. If the interpolator is
    176      * null, the default (viscous) interpolator will be used. Specify whether or
    177      * not to support progressive "flywheel" behavior in flinging.
    178      */
    179     public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
    180         mFinished = true;
    181         mInterpolator = interpolator;
    182         mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
    183         mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
    184         mFlywheel = flywheel;
    185 
    186         mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
    187     }
    188 
    189     /**
    190      * The amount of friction applied to flings. The default value
    191      * is {@link ViewConfiguration#getScrollFriction}.
    192      *
    193      * @param friction A scalar dimension-less value representing the coefficient of
    194      *         friction.
    195      */
    196     public final void setFriction(float friction) {
    197         mDeceleration = computeDeceleration(friction);
    198         mFlingFriction = friction;
    199     }
    200 
    201     private float computeDeceleration(float friction) {
    202         return SensorManager.GRAVITY_EARTH   // g (m/s^2)
    203                       * 39.37f               // inch/meter
    204                       * mPpi                 // pixels per inch
    205                       * friction;
    206     }
    207 
    208     /**
    209      *
    210      * Returns whether the scroller has finished scrolling.
    211      *
    212      * @return True if the scroller has finished scrolling, false otherwise.
    213      */
    214     public final boolean isFinished() {
    215         return mFinished;
    216     }
    217 
    218     /**
    219      * Force the finished field to a particular value.
    220      *
    221      * @param finished The new finished value.
    222      */
    223     public final void forceFinished(boolean finished) {
    224         mFinished = finished;
    225     }
    226 
    227     /**
    228      * Returns how long the scroll event will take, in milliseconds.
    229      *
    230      * @return The duration of the scroll in milliseconds.
    231      */
    232     public final int getDuration() {
    233         return mDuration;
    234     }
    235 
    236     /**
    237      * Returns the current X offset in the scroll.
    238      *
    239      * @return The new X offset as an absolute distance from the origin.
    240      */
    241     public final int getCurrX() {
    242         return mCurrX;
    243     }
    244 
    245     /**
    246      * Returns the current Y offset in the scroll.
    247      *
    248      * @return The new Y offset as an absolute distance from the origin.
    249      */
    250     public final int getCurrY() {
    251         return mCurrY;
    252     }
    253 
    254     /**
    255      * Returns the current velocity.
    256      *
    257      * @return The original velocity less the deceleration. Result may be
    258      * negative.
    259      */
    260     public float getCurrVelocity() {
    261         return mMode == FLING_MODE ?
    262                 mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f;
    263     }
    264 
    265     /**
    266      * Returns the start X offset in the scroll.
    267      *
    268      * @return The start X offset as an absolute distance from the origin.
    269      */
    270     public final int getStartX() {
    271         return mStartX;
    272     }
    273 
    274     /**
    275      * Returns the start Y offset in the scroll.
    276      *
    277      * @return The start Y offset as an absolute distance from the origin.
    278      */
    279     public final int getStartY() {
    280         return mStartY;
    281     }
    282 
    283     /**
    284      * Returns where the scroll will end. Valid only for "fling" scrolls.
    285      *
    286      * @return The final X offset as an absolute distance from the origin.
    287      */
    288     public final int getFinalX() {
    289         return mFinalX;
    290     }
    291 
    292     /**
    293      * Returns where the scroll will end. Valid only for "fling" scrolls.
    294      *
    295      * @return The final Y offset as an absolute distance from the origin.
    296      */
    297     public final int getFinalY() {
    298         return mFinalY;
    299     }
    300 
    301     /**
    302      * Call this when you want to know the new location.  If it returns true,
    303      * the animation is not yet finished.
    304      */
    305     public boolean computeScrollOffset() {
    306         if (mFinished) {
    307             return false;
    308         }
    309 
    310         int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    311 
    312         if (timePassed < mDuration) {
    313             switch (mMode) {
    314             case SCROLL_MODE:
    315                 float x = timePassed * mDurationReciprocal;
    316 
    317                 if (mInterpolator == null)
    318                     x = viscousFluid(x);
    319                 else
    320                     x = mInterpolator.getInterpolation(x);
    321 
    322                 mCurrX = mStartX + Math.round(x * mDeltaX);
    323                 mCurrY = mStartY + Math.round(x * mDeltaY);
    324                 break;
    325             case FLING_MODE:
    326                 final float t = (float) timePassed / mDuration;
    327                 final int index = (int) (NB_SAMPLES * t);
    328                 float distanceCoef = 1.f;
    329                 float velocityCoef = 0.f;
    330                 if (index < NB_SAMPLES) {
    331                     final float t_inf = (float) index / NB_SAMPLES;
    332                     final float t_sup = (float) (index + 1) / NB_SAMPLES;
    333                     final float d_inf = SPLINE_POSITION[index];
    334                     final float d_sup = SPLINE_POSITION[index + 1];
    335                     velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
    336                     distanceCoef = d_inf + (t - t_inf) * velocityCoef;
    337                 }
    338 
    339                 mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
    340 
    341                 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
    342                 // Pin to mMinX <= mCurrX <= mMaxX
    343                 mCurrX = Math.min(mCurrX, mMaxX);
    344                 mCurrX = Math.max(mCurrX, mMinX);
    345 
    346                 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
    347                 // Pin to mMinY <= mCurrY <= mMaxY
    348                 mCurrY = Math.min(mCurrY, mMaxY);
    349                 mCurrY = Math.max(mCurrY, mMinY);
    350 
    351                 if (mCurrX == mFinalX && mCurrY == mFinalY) {
    352                     mFinished = true;
    353                 }
    354 
    355                 break;
    356             }
    357         }
    358         else {
    359             mCurrX = mFinalX;
    360             mCurrY = mFinalY;
    361             mFinished = true;
    362         }
    363         return true;
    364     }
    365 
    366     /**
    367      * Start scrolling by providing a starting point and the distance to travel.
    368      * The scroll will use the default value of 250 milliseconds for the
    369      * duration.
    370      *
    371      * @param startX Starting horizontal scroll offset in pixels. Positive
    372      *        numbers will scroll the content to the left.
    373      * @param startY Starting vertical scroll offset in pixels. Positive numbers
    374      *        will scroll the content up.
    375      * @param dx Horizontal distance to travel. Positive numbers will scroll the
    376      *        content to the left.
    377      * @param dy Vertical distance to travel. Positive numbers will scroll the
    378      *        content up.
    379      */
    380     public void startScroll(int startX, int startY, int dx, int dy) {
    381         startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
    382     }
    383 
    384     /**
    385      * Start scrolling by providing a starting point, the distance to travel,
    386      * and the duration of the scroll.
    387      *
    388      * @param startX Starting horizontal scroll offset in pixels. Positive
    389      *        numbers will scroll the content to the left.
    390      * @param startY Starting vertical scroll offset in pixels. Positive numbers
    391      *        will scroll the content up.
    392      * @param dx Horizontal distance to travel. Positive numbers will scroll the
    393      *        content to the left.
    394      * @param dy Vertical distance to travel. Positive numbers will scroll the
    395      *        content up.
    396      * @param duration Duration of the scroll in milliseconds.
    397      */
    398     public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    399         mMode = SCROLL_MODE;
    400         mFinished = false;
    401         mDuration = duration;
    402         mStartTime = AnimationUtils.currentAnimationTimeMillis();
    403         mStartX = startX;
    404         mStartY = startY;
    405         mFinalX = startX + dx;
    406         mFinalY = startY + dy;
    407         mDeltaX = dx;
    408         mDeltaY = dy;
    409         mDurationReciprocal = 1.0f / (float) mDuration;
    410     }
    411 
    412     /**
    413      * Start scrolling based on a fling gesture. The distance travelled will
    414      * depend on the initial velocity of the fling.
    415      *
    416      * @param startX Starting point of the scroll (X)
    417      * @param startY Starting point of the scroll (Y)
    418      * @param velocityX Initial velocity of the fling (X) measured in pixels per
    419      *        second.
    420      * @param velocityY Initial velocity of the fling (Y) measured in pixels per
    421      *        second
    422      * @param minX Minimum X value. The scroller will not scroll past this
    423      *        point.
    424      * @param maxX Maximum X value. The scroller will not scroll past this
    425      *        point.
    426      * @param minY Minimum Y value. The scroller will not scroll past this
    427      *        point.
    428      * @param maxY Maximum Y value. The scroller will not scroll past this
    429      *        point.
    430      */
    431     public void fling(int startX, int startY, int velocityX, int velocityY,
    432             int minX, int maxX, int minY, int maxY) {
    433         // Continue a scroll or fling in progress
    434         if (mFlywheel && !mFinished) {
    435             float oldVel = getCurrVelocity();
    436 
    437             float dx = (float) (mFinalX - mStartX);
    438             float dy = (float) (mFinalY - mStartY);
    439             float hyp = FloatMath.sqrt(dx * dx + dy * dy);
    440 
    441             float ndx = dx / hyp;
    442             float ndy = dy / hyp;
    443 
    444             float oldVelocityX = ndx * oldVel;
    445             float oldVelocityY = ndy * oldVel;
    446             if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
    447                     Math.signum(velocityY) == Math.signum(oldVelocityY)) {
    448                 velocityX += oldVelocityX;
    449                 velocityY += oldVelocityY;
    450             }
    451         }
    452 
    453         mMode = FLING_MODE;
    454         mFinished = false;
    455 
    456         float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
    457 
    458         mVelocity = velocity;
    459         mDuration = getSplineFlingDuration(velocity);
    460         mStartTime = AnimationUtils.currentAnimationTimeMillis();
    461         mStartX = startX;
    462         mStartY = startY;
    463 
    464         float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
    465         float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
    466 
    467         double totalDistance = getSplineFlingDistance(velocity);
    468         mDistance = (int) (totalDistance * Math.signum(velocity));
    469 
    470         mMinX = minX;
    471         mMaxX = maxX;
    472         mMinY = minY;
    473         mMaxY = maxY;
    474 
    475         mFinalX = startX + (int) Math.round(totalDistance * coeffX);
    476         // Pin to mMinX <= mFinalX <= mMaxX
    477         mFinalX = Math.min(mFinalX, mMaxX);
    478         mFinalX = Math.max(mFinalX, mMinX);
    479 
    480         mFinalY = startY + (int) Math.round(totalDistance * coeffY);
    481         // Pin to mMinY <= mFinalY <= mMaxY
    482         mFinalY = Math.min(mFinalY, mMaxY);
    483         mFinalY = Math.max(mFinalY, mMinY);
    484     }
    485 
    486     private double getSplineDeceleration(float velocity) {
    487         return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
    488     }
    489 
    490     private int getSplineFlingDuration(float velocity) {
    491         final double l = getSplineDeceleration(velocity);
    492         final double decelMinusOne = DECELERATION_RATE - 1.0;
    493         return (int) (1000.0 * Math.exp(l / decelMinusOne));
    494     }
    495 
    496     private double getSplineFlingDistance(float velocity) {
    497         final double l = getSplineDeceleration(velocity);
    498         final double decelMinusOne = DECELERATION_RATE - 1.0;
    499         return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
    500     }
    501 
    502     static float viscousFluid(float x)
    503     {
    504         x *= sViscousFluidScale;
    505         if (x < 1.0f) {
    506             x -= (1.0f - (float)Math.exp(-x));
    507         } else {
    508             float start = 0.36787944117f;   // 1/e == exp(-1)
    509             x = 1.0f - (float)Math.exp(1.0f - x);
    510             x = start + x * (1.0f - start);
    511         }
    512         x *= sViscousFluidNormalize;
    513         return x;
    514     }
    515 
    516     /**
    517      * Stops the animation. Contrary to {@link #forceFinished(boolean)},
    518      * aborting the animating cause the scroller to move to the final x and y
    519      * position
    520      *
    521      * @see #forceFinished(boolean)
    522      */
    523     public void abortAnimation() {
    524         mCurrX = mFinalX;
    525         mCurrY = mFinalY;
    526         mFinished = true;
    527     }
    528 
    529     /**
    530      * Extend the scroll animation. This allows a running animation to scroll
    531      * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
    532      *
    533      * @param extend Additional time to scroll in milliseconds.
    534      * @see #setFinalX(int)
    535      * @see #setFinalY(int)
    536      */
    537     public void extendDuration(int extend) {
    538         int passed = timePassed();
    539         mDuration = passed + extend;
    540         mDurationReciprocal = 1.0f / mDuration;
    541         mFinished = false;
    542     }
    543 
    544     /**
    545      * Returns the time elapsed since the beginning of the scrolling.
    546      *
    547      * @return The elapsed time in milliseconds.
    548      */
    549     public int timePassed() {
    550         return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    551     }
    552 
    553     /**
    554      * Sets the final position (X) for this scroller.
    555      *
    556      * @param newX The new X offset as an absolute distance from the origin.
    557      * @see #extendDuration(int)
    558      * @see #setFinalY(int)
    559      */
    560     public void setFinalX(int newX) {
    561         mFinalX = newX;
    562         mDeltaX = mFinalX - mStartX;
    563         mFinished = false;
    564     }
    565 
    566     /**
    567      * Sets the final position (Y) for this scroller.
    568      *
    569      * @param newY The new Y offset as an absolute distance from the origin.
    570      * @see #extendDuration(int)
    571      * @see #setFinalX(int)
    572      */
    573     public void setFinalY(int newY) {
    574         mFinalY = newY;
    575         mDeltaY = mFinalY - mStartY;
    576         mFinished = false;
    577     }
    578 
    579     /**
    580      * @hide
    581      */
    582     public boolean isScrollingInDirection(float xvel, float yvel) {
    583         return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) &&
    584                 Math.signum(yvel) == Math.signum(mFinalY - mStartY);
    585     }
    586 }
    587