Home | History | Annotate | Download | only in common
      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 com.android.gallery3d.common;
     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  * This class encapsulates scrolling.  The duration of the scroll
     29  * can be passed in the constructor and specifies the maximum time that
     30  * the scrolling animation should take.  Past this time, the scrolling is
     31  * automatically moved to its final stage and computeScrollOffset()
     32  * will always return false to indicate that scrolling is over.
     33  */
     34 public class Scroller  {
     35     private int mMode;
     36 
     37     private int mStartX;
     38     private int mStartY;
     39     private int mFinalX;
     40     private int mFinalY;
     41 
     42     private int mMinX;
     43     private int mMaxX;
     44     private int mMinY;
     45     private int mMaxY;
     46 
     47     private int mCurrX;
     48     private int mCurrY;
     49     private long mStartTime;
     50     private int mDuration;
     51     private float mDurationReciprocal;
     52     private float mDeltaX;
     53     private float mDeltaY;
     54     private boolean mFinished;
     55     private Interpolator mInterpolator;
     56     private boolean mFlywheel;
     57 
     58     private float mVelocity;
     59 
     60     private static final int DEFAULT_DURATION = 250;
     61     private static final int SCROLL_MODE = 0;
     62     private static final int FLING_MODE = 1;
     63 
     64     private static float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9));
     65     private static float ALPHA = 800; // pixels / seconds
     66     private static float START_TENSION = 0.4f; // Tension at start: (0.4 * total T, 1.0 * Distance)
     67     private static float END_TENSION = 1.0f - START_TENSION;
     68     private static final int NB_SAMPLES = 100;
     69     private static final float[] SPLINE = new float[NB_SAMPLES + 1];
     70 
     71     private float mDeceleration;
     72     private final float mPpi;
     73 
     74     static {
     75         float x_min = 0.0f;
     76         for (int i = 0; i <= NB_SAMPLES; i++) {
     77             final float t = (float) i / NB_SAMPLES;
     78             float x_max = 1.0f;
     79             float x, tx, coef;
     80             while (true) {
     81                 x = x_min + (x_max - x_min) / 2.0f;
     82                 coef = 3.0f * x * (1.0f - x);
     83                 tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x;
     84                 if (Math.abs(tx - t) < 1E-5) break;
     85                 if (tx > t) x_max = x;
     86                 else x_min = x;
     87             }
     88             final float d = coef + x * x * x;
     89             SPLINE[i] = d;
     90         }
     91         SPLINE[NB_SAMPLES] = 1.0f;
     92 
     93         // This controls the viscous fluid effect (how much of it)
     94         sViscousFluidScale = 8.0f;
     95         // must be set to 1.0 (used in viscousFluid())
     96         sViscousFluidNormalize = 1.0f;
     97         sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
     98     }
     99 
    100     private static float sViscousFluidScale;
    101     private static float sViscousFluidNormalize;
    102 
    103     /**
    104      * Create a Scroller with the default duration and interpolator.
    105      */
    106     public Scroller(Context context) {
    107         this(context, null);
    108     }
    109 
    110     /**
    111      * Create a Scroller with the specified interpolator. If the interpolator is
    112      * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
    113      * be in effect for apps targeting Honeycomb or newer.
    114      */
    115     public Scroller(Context context, Interpolator interpolator) {
    116         this(context, interpolator,
    117                 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    118     }
    119 
    120     /**
    121      * Create a Scroller with the specified interpolator. If the interpolator is
    122      * null, the default (viscous) interpolator will be used. Specify whether or
    123      * not to support progressive "flywheel" behavior in flinging.
    124      */
    125     public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
    126         mFinished = true;
    127         mInterpolator = interpolator;
    128         mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
    129         mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
    130         mFlywheel = flywheel;
    131     }
    132 
    133     /**
    134      * The amount of friction applied to flings. The default value
    135      * is {@link ViewConfiguration#getScrollFriction}.
    136      *
    137      * @param friction A scalar dimension-less value representing the coefficient of
    138      *         friction.
    139      */
    140     public final void setFriction(float friction) {
    141         mDeceleration = computeDeceleration(friction);
    142     }
    143 
    144     private float computeDeceleration(float friction) {
    145         return SensorManager.GRAVITY_EARTH   // g (m/s^2)
    146                       * 39.37f               // inch/meter
    147                       * mPpi                 // pixels per inch
    148                       * friction;
    149     }
    150 
    151     /**
    152      *
    153      * Returns whether the scroller has finished scrolling.
    154      *
    155      * @return True if the scroller has finished scrolling, false otherwise.
    156      */
    157     public final boolean isFinished() {
    158         return mFinished;
    159     }
    160 
    161     /**
    162      * Force the finished field to a particular value.
    163      *
    164      * @param finished The new finished value.
    165      */
    166     public final void forceFinished(boolean finished) {
    167         mFinished = finished;
    168     }
    169 
    170     /**
    171      * Returns how long the scroll event will take, in milliseconds.
    172      *
    173      * @return The duration of the scroll in milliseconds.
    174      */
    175     public final int getDuration() {
    176         return mDuration;
    177     }
    178 
    179     /**
    180      * Returns the current X offset in the scroll.
    181      *
    182      * @return The new X offset as an absolute distance from the origin.
    183      */
    184     public final int getCurrX() {
    185         return mCurrX;
    186     }
    187 
    188     /**
    189      * Returns the current Y offset in the scroll.
    190      *
    191      * @return The new Y offset as an absolute distance from the origin.
    192      */
    193     public final int getCurrY() {
    194         return mCurrY;
    195     }
    196 
    197     /**
    198      * Returns the current velocity.
    199      *
    200      * @return The original velocity less the deceleration. Result may be
    201      * negative.
    202      */
    203     public float getCurrVelocity() {
    204         return mVelocity - mDeceleration * timePassed() / 2000.0f;
    205     }
    206 
    207     /**
    208      * Returns the start X offset in the scroll.
    209      *
    210      * @return The start X offset as an absolute distance from the origin.
    211      */
    212     public final int getStartX() {
    213         return mStartX;
    214     }
    215 
    216     /**
    217      * Returns the start Y offset in the scroll.
    218      *
    219      * @return The start Y offset as an absolute distance from the origin.
    220      */
    221     public final int getStartY() {
    222         return mStartY;
    223     }
    224 
    225     /**
    226      * Returns where the scroll will end. Valid only for "fling" scrolls.
    227      *
    228      * @return The final X offset as an absolute distance from the origin.
    229      */
    230     public final int getFinalX() {
    231         return mFinalX;
    232     }
    233 
    234     /**
    235      * Returns where the scroll will end. Valid only for "fling" scrolls.
    236      *
    237      * @return The final Y offset as an absolute distance from the origin.
    238      */
    239     public final int getFinalY() {
    240         return mFinalY;
    241     }
    242 
    243     /**
    244      * Call this when you want to know the new location.  If it returns true,
    245      * the animation is not yet finished.  loc will be altered to provide the
    246      * new location.
    247      */
    248     public boolean computeScrollOffset() {
    249         if (mFinished) {
    250             return false;
    251         }
    252 
    253         int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    254 
    255         if (timePassed < mDuration) {
    256             switch (mMode) {
    257             case SCROLL_MODE:
    258                 float x = timePassed * mDurationReciprocal;
    259 
    260                 if (mInterpolator == null)
    261                     x = viscousFluid(x);
    262                 else
    263                     x = mInterpolator.getInterpolation(x);
    264 
    265                 mCurrX = mStartX + Math.round(x * mDeltaX);
    266                 mCurrY = mStartY + Math.round(x * mDeltaY);
    267                 break;
    268             case FLING_MODE:
    269                 final float t = (float) timePassed / mDuration;
    270                 final int index = (int) (NB_SAMPLES * t);
    271                 final float t_inf = (float) index / NB_SAMPLES;
    272                 final float t_sup = (float) (index + 1) / NB_SAMPLES;
    273                 final float d_inf = SPLINE[index];
    274                 final float d_sup = SPLINE[index + 1];
    275                 final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf);
    276 
    277                 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
    278                 // Pin to mMinX <= mCurrX <= mMaxX
    279                 mCurrX = Math.min(mCurrX, mMaxX);
    280                 mCurrX = Math.max(mCurrX, mMinX);
    281 
    282                 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
    283                 // Pin to mMinY <= mCurrY <= mMaxY
    284                 mCurrY = Math.min(mCurrY, mMaxY);
    285                 mCurrY = Math.max(mCurrY, mMinY);
    286 
    287                 if (mCurrX == mFinalX && mCurrY == mFinalY) {
    288                     mFinished = true;
    289                 }
    290 
    291                 break;
    292             }
    293         }
    294         else {
    295             mCurrX = mFinalX;
    296             mCurrY = mFinalY;
    297             mFinished = true;
    298         }
    299         return true;
    300     }
    301 
    302     /**
    303      * Start scrolling by providing a starting point and the distance to travel.
    304      * The scroll will use the default value of 250 milliseconds for the
    305      * duration.
    306      *
    307      * @param startX Starting horizontal scroll offset in pixels. Positive
    308      *        numbers will scroll the content to the left.
    309      * @param startY Starting vertical scroll offset in pixels. Positive numbers
    310      *        will scroll the content up.
    311      * @param dx Horizontal distance to travel. Positive numbers will scroll the
    312      *        content to the left.
    313      * @param dy Vertical distance to travel. Positive numbers will scroll the
    314      *        content up.
    315      */
    316     public void startScroll(int startX, int startY, int dx, int dy) {
    317         startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
    318     }
    319 
    320     /**
    321      * Start scrolling by providing a starting point and the distance to travel.
    322      *
    323      * @param startX Starting horizontal scroll offset in pixels. Positive
    324      *        numbers will scroll the content to the left.
    325      * @param startY Starting vertical scroll offset in pixels. Positive numbers
    326      *        will scroll the content up.
    327      * @param dx Horizontal distance to travel. Positive numbers will scroll the
    328      *        content to the left.
    329      * @param dy Vertical distance to travel. Positive numbers will scroll the
    330      *        content up.
    331      * @param duration Duration of the scroll in milliseconds.
    332      */
    333     public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    334         mMode = SCROLL_MODE;
    335         mFinished = false;
    336         mDuration = duration;
    337         mStartTime = AnimationUtils.currentAnimationTimeMillis();
    338         mStartX = startX;
    339         mStartY = startY;
    340         mFinalX = startX + dx;
    341         mFinalY = startY + dy;
    342         mDeltaX = dx;
    343         mDeltaY = dy;
    344         mDurationReciprocal = 1.0f / mDuration;
    345     }
    346 
    347     /**
    348      * Start scrolling based on a fling gesture. The distance travelled will
    349      * depend on the initial velocity of the fling.
    350      *
    351      * @param startX Starting point of the scroll (X)
    352      * @param startY Starting point of the scroll (Y)
    353      * @param velocityX Initial velocity of the fling (X) measured in pixels per
    354      *        second.
    355      * @param velocityY Initial velocity of the fling (Y) measured in pixels per
    356      *        second
    357      * @param minX Minimum X value. The scroller will not scroll past this
    358      *        point.
    359      * @param maxX Maximum X value. The scroller will not scroll past this
    360      *        point.
    361      * @param minY Minimum Y value. The scroller will not scroll past this
    362      *        point.
    363      * @param maxY Maximum Y value. The scroller will not scroll past this
    364      *        point.
    365      */
    366     public void fling(int startX, int startY, int velocityX, int velocityY,
    367             int minX, int maxX, int minY, int maxY) {
    368         // Continue a scroll or fling in progress
    369         if (mFlywheel && !mFinished) {
    370             float oldVel = getCurrVelocity();
    371 
    372             float dx = mFinalX - mStartX;
    373             float dy = mFinalY - mStartY;
    374             float hyp = (float) Math.hypot(dx, dy);
    375 
    376             float ndx = dx / hyp;
    377             float ndy = dy / hyp;
    378 
    379             float oldVelocityX = ndx * oldVel;
    380             float oldVelocityY = ndy * oldVel;
    381             if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
    382                     Math.signum(velocityY) == Math.signum(oldVelocityY)) {
    383                 velocityX += oldVelocityX;
    384                 velocityY += oldVelocityY;
    385             }
    386         }
    387 
    388         mMode = FLING_MODE;
    389         mFinished = false;
    390 
    391         float velocity = (float) Math.hypot(velocityX, velocityY);
    392 
    393         mVelocity = velocity;
    394         final double l = Math.log(START_TENSION * velocity / ALPHA);
    395         mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0)));
    396         mStartTime = AnimationUtils.currentAnimationTimeMillis();
    397         mStartX = startX;
    398         mStartY = startY;
    399 
    400         float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
    401         float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
    402 
    403         int totalDistance =
    404                 (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l));
    405 
    406         mMinX = minX;
    407         mMaxX = maxX;
    408         mMinY = minY;
    409         mMaxY = maxY;
    410 
    411         mFinalX = startX + Math.round(totalDistance * coeffX);
    412         // Pin to mMinX <= mFinalX <= mMaxX
    413         mFinalX = Math.min(mFinalX, mMaxX);
    414         mFinalX = Math.max(mFinalX, mMinX);
    415 
    416         mFinalY = startY + Math.round(totalDistance * coeffY);
    417         // Pin to mMinY <= mFinalY <= mMaxY
    418         mFinalY = Math.min(mFinalY, mMaxY);
    419         mFinalY = Math.max(mFinalY, mMinY);
    420     }
    421 
    422     static float viscousFluid(float x)
    423     {
    424         x *= sViscousFluidScale;
    425         if (x < 1.0f) {
    426             x -= (1.0f - (float)Math.exp(-x));
    427         } else {
    428             float start = 0.36787944117f;   // 1/e == exp(-1)
    429             x = 1.0f - (float)Math.exp(1.0f - x);
    430             x = start + x * (1.0f - start);
    431         }
    432         x *= sViscousFluidNormalize;
    433         return x;
    434     }
    435 
    436     /**
    437      * Stops the animation. Contrary to {@link #forceFinished(boolean)},
    438      * aborting the animating cause the scroller to move to the final x and y
    439      * position
    440      *
    441      * @see #forceFinished(boolean)
    442      */
    443     public void abortAnimation() {
    444         mCurrX = mFinalX;
    445         mCurrY = mFinalY;
    446         mFinished = true;
    447     }
    448 
    449     /**
    450      * Extend the scroll animation. This allows a running animation to scroll
    451      * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
    452      *
    453      * @param extend Additional time to scroll in milliseconds.
    454      * @see #setFinalX(int)
    455      * @see #setFinalY(int)
    456      */
    457     public void extendDuration(int extend) {
    458         int passed = timePassed();
    459         mDuration = passed + extend;
    460         mDurationReciprocal = 1.0f / mDuration;
    461         mFinished = false;
    462     }
    463 
    464     /**
    465      * Returns the time elapsed since the beginning of the scrolling.
    466      *
    467      * @return The elapsed time in milliseconds.
    468      */
    469     public int timePassed() {
    470         return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    471     }
    472 
    473     /**
    474      * Sets the final position (X) for this scroller.
    475      *
    476      * @param newX The new X offset as an absolute distance from the origin.
    477      * @see #extendDuration(int)
    478      * @see #setFinalY(int)
    479      */
    480     public void setFinalX(int newX) {
    481         mFinalX = newX;
    482         mDeltaX = mFinalX - mStartX;
    483         mFinished = false;
    484     }
    485 
    486     /**
    487      * Sets the final position (Y) for this scroller.
    488      *
    489      * @param newY The new Y offset as an absolute distance from the origin.
    490      * @see #extendDuration(int)
    491      * @see #setFinalX(int)
    492      */
    493     public void setFinalY(int newY) {
    494         mFinalY = newY;
    495         mDeltaY = mFinalY - mStartY;
    496         mFinished = false;
    497     }
    498 
    499     /**
    500      * @hide
    501      */
    502     public boolean isScrollingInDirection(float xvel, float yvel) {
    503         return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) &&
    504                 Math.signum(yvel) == Math.signum(mFinalY - mStartY);
    505     }
    506 }
    507