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