Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2010 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.annotation.ColorInt;
     20 import android.content.Context;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Canvas;
     23 import android.graphics.Paint;
     24 import android.graphics.PorterDuff;
     25 import android.graphics.PorterDuffXfermode;
     26 import android.graphics.Rect;
     27 import android.view.animation.AnimationUtils;
     28 import android.view.animation.DecelerateInterpolator;
     29 import android.view.animation.Interpolator;
     30 
     31 /**
     32  * This class performs the graphical effect used at the edges of scrollable widgets
     33  * when the user scrolls beyond the content bounds in 2D space.
     34  *
     35  * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
     36  * instance for each edge that should show the effect, feed it input data using
     37  * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
     38  * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
     39  * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
     40  * false after drawing, the edge effect's animation is not yet complete and the widget
     41  * should schedule another drawing pass to continue the animation.</p>
     42  *
     43  * <p>When drawing, widgets should draw their main content and child views first,
     44  * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
     45  * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
     46  * The edge effect may then be drawn on top of the view's content using the
     47  * {@link #draw(Canvas)} method.</p>
     48  */
     49 public class EdgeEffect {
     50     @SuppressWarnings("UnusedDeclaration")
     51     private static final String TAG = "EdgeEffect";
     52 
     53     // Time it will take the effect to fully recede in ms
     54     private static final int RECEDE_TIME = 600;
     55 
     56     // Time it will take before a pulled glow begins receding in ms
     57     private static final int PULL_TIME = 167;
     58 
     59     // Time it will take in ms for a pulled glow to decay to partial strength before release
     60     private static final int PULL_DECAY_TIME = 2000;
     61 
     62     private static final float MAX_ALPHA = 0.15f;
     63     private static final float GLOW_ALPHA_START = .09f;
     64 
     65     private static final float MAX_GLOW_SCALE = 2.f;
     66 
     67     private static final float PULL_GLOW_BEGIN = 0.f;
     68 
     69     // Minimum velocity that will be absorbed
     70     private static final int MIN_VELOCITY = 100;
     71     // Maximum velocity, clamps at this value
     72     private static final int MAX_VELOCITY = 10000;
     73 
     74     private static final float EPSILON = 0.001f;
     75 
     76     private static final double ANGLE = Math.PI / 6;
     77     private static final float SIN = (float) Math.sin(ANGLE);
     78     private static final float COS = (float) Math.cos(ANGLE);
     79     private static final float RADIUS_FACTOR = 0.6f;
     80 
     81     private float mGlowAlpha;
     82     private float mGlowScaleY;
     83 
     84     private float mGlowAlphaStart;
     85     private float mGlowAlphaFinish;
     86     private float mGlowScaleYStart;
     87     private float mGlowScaleYFinish;
     88 
     89     private long mStartTime;
     90     private float mDuration;
     91 
     92     private final Interpolator mInterpolator;
     93 
     94     private static final int STATE_IDLE = 0;
     95     private static final int STATE_PULL = 1;
     96     private static final int STATE_ABSORB = 2;
     97     private static final int STATE_RECEDE = 3;
     98     private static final int STATE_PULL_DECAY = 4;
     99 
    100     private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f;
    101 
    102     private static final int VELOCITY_GLOW_FACTOR = 6;
    103 
    104     private int mState = STATE_IDLE;
    105 
    106     private float mPullDistance;
    107 
    108     private final Rect mBounds = new Rect();
    109     private final Paint mPaint = new Paint();
    110     private float mRadius;
    111     private float mBaseGlowScale;
    112     private float mDisplacement = 0.5f;
    113     private float mTargetDisplacement = 0.5f;
    114 
    115     /**
    116      * Construct a new EdgeEffect with a theme appropriate for the provided context.
    117      * @param context Context used to provide theming and resource information for the EdgeEffect
    118      */
    119     public EdgeEffect(Context context) {
    120         mPaint.setAntiAlias(true);
    121         final TypedArray a = context.obtainStyledAttributes(
    122                 com.android.internal.R.styleable.EdgeEffect);
    123         final int themeColor = a.getColor(
    124                 com.android.internal.R.styleable.EdgeEffect_colorEdgeEffect, 0xff666666);
    125         a.recycle();
    126         mPaint.setColor((themeColor & 0xffffff) | 0x33000000);
    127         mPaint.setStyle(Paint.Style.FILL);
    128         mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
    129         mInterpolator = new DecelerateInterpolator();
    130     }
    131 
    132     /**
    133      * Set the size of this edge effect in pixels.
    134      *
    135      * @param width Effect width in pixels
    136      * @param height Effect height in pixels
    137      */
    138     public void setSize(int width, int height) {
    139         final float r = width * RADIUS_FACTOR / SIN;
    140         final float y = COS * r;
    141         final float h = r - y;
    142         final float or = height * RADIUS_FACTOR / SIN;
    143         final float oy = COS * or;
    144         final float oh = or - oy;
    145 
    146         mRadius = r;
    147         mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f;
    148 
    149         mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h));
    150     }
    151 
    152     /**
    153      * Reports if this EdgeEffect's animation is finished. If this method returns false
    154      * after a call to {@link #draw(Canvas)} the host widget should schedule another
    155      * drawing pass to continue the animation.
    156      *
    157      * @return true if animation is finished, false if drawing should continue on the next frame.
    158      */
    159     public boolean isFinished() {
    160         return mState == STATE_IDLE;
    161     }
    162 
    163     /**
    164      * Immediately finish the current animation.
    165      * After this call {@link #isFinished()} will return true.
    166      */
    167     public void finish() {
    168         mState = STATE_IDLE;
    169     }
    170 
    171     /**
    172      * A view should call this when content is pulled away from an edge by the user.
    173      * This will update the state of the current visual effect and its associated animation.
    174      * The host view should always {@link android.view.View#invalidate()} after this
    175      * and draw the results accordingly.
    176      *
    177      * <p>Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement
    178      * of the pull point is known.</p>
    179      *
    180      * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
    181      *                      1.f (full length of the view) or negative values to express change
    182      *                      back toward the edge reached to initiate the effect.
    183      */
    184     public void onPull(float deltaDistance) {
    185         onPull(deltaDistance, 0.5f);
    186     }
    187 
    188     /**
    189      * A view should call this when content is pulled away from an edge by the user.
    190      * This will update the state of the current visual effect and its associated animation.
    191      * The host view should always {@link android.view.View#invalidate()} after this
    192      * and draw the results accordingly.
    193      *
    194      * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
    195      *                      1.f (full length of the view) or negative values to express change
    196      *                      back toward the edge reached to initiate the effect.
    197      * @param displacement The displacement from the starting side of the effect of the point
    198      *                     initiating the pull. In the case of touch this is the finger position.
    199      *                     Values may be from 0-1.
    200      */
    201     public void onPull(float deltaDistance, float displacement) {
    202         final long now = AnimationUtils.currentAnimationTimeMillis();
    203         mTargetDisplacement = displacement;
    204         if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
    205             return;
    206         }
    207         if (mState != STATE_PULL) {
    208             mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY);
    209         }
    210         mState = STATE_PULL;
    211 
    212         mStartTime = now;
    213         mDuration = PULL_TIME;
    214 
    215         mPullDistance += deltaDistance;
    216 
    217         final float absdd = Math.abs(deltaDistance);
    218         mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
    219                 mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
    220 
    221         if (mPullDistance == 0) {
    222             mGlowScaleY = mGlowScaleYStart = 0;
    223         } else {
    224             final float scale = (float) (Math.max(0, 1 - 1 /
    225                     Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3d) / 0.7d);
    226 
    227             mGlowScaleY = mGlowScaleYStart = scale;
    228         }
    229 
    230         mGlowAlphaFinish = mGlowAlpha;
    231         mGlowScaleYFinish = mGlowScaleY;
    232     }
    233 
    234     /**
    235      * Call when the object is released after being pulled.
    236      * This will begin the "decay" phase of the effect. After calling this method
    237      * the host view should {@link android.view.View#invalidate()} and thereby
    238      * draw the results accordingly.
    239      */
    240     public void onRelease() {
    241         mPullDistance = 0;
    242 
    243         if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
    244             return;
    245         }
    246 
    247         mState = STATE_RECEDE;
    248         mGlowAlphaStart = mGlowAlpha;
    249         mGlowScaleYStart = mGlowScaleY;
    250 
    251         mGlowAlphaFinish = 0.f;
    252         mGlowScaleYFinish = 0.f;
    253 
    254         mStartTime = AnimationUtils.currentAnimationTimeMillis();
    255         mDuration = RECEDE_TIME;
    256     }
    257 
    258     /**
    259      * Call when the effect absorbs an impact at the given velocity.
    260      * Used when a fling reaches the scroll boundary.
    261      *
    262      * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
    263      * the method <code>getCurrVelocity</code> will provide a reasonable approximation
    264      * to use here.</p>
    265      *
    266      * @param velocity Velocity at impact in pixels per second.
    267      */
    268     public void onAbsorb(int velocity) {
    269         mState = STATE_ABSORB;
    270         velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY);
    271 
    272         mStartTime = AnimationUtils.currentAnimationTimeMillis();
    273         mDuration = 0.15f + (velocity * 0.02f);
    274 
    275         // The glow depends more on the velocity, and therefore starts out
    276         // nearly invisible.
    277         mGlowAlphaStart = GLOW_ALPHA_START;
    278         mGlowScaleYStart = Math.max(mGlowScaleY, 0.f);
    279 
    280 
    281         // Growth for the size of the glow should be quadratic to properly
    282         // respond
    283         // to a user's scrolling speed. The faster the scrolling speed, the more
    284         // intense the effect should be for both the size and the saturation.
    285         mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 1.f);
    286         // Alpha should change for the glow as well as size.
    287         mGlowAlphaFinish = Math.max(
    288                 mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
    289         mTargetDisplacement = 0.5f;
    290     }
    291 
    292     /**
    293      * Set the color of this edge effect in argb.
    294      *
    295      * @param color Color in argb
    296      */
    297     public void setColor(@ColorInt int color) {
    298         mPaint.setColor(color);
    299     }
    300 
    301     /**
    302      * Return the color of this edge effect in argb.
    303      * @return The color of this edge effect in argb
    304      */
    305     @ColorInt
    306     public int getColor() {
    307         return mPaint.getColor();
    308     }
    309 
    310     /**
    311      * Draw into the provided canvas. Assumes that the canvas has been rotated
    312      * accordingly and the size has been set. The effect will be drawn the full
    313      * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
    314      * 1.f of height.
    315      *
    316      * @param canvas Canvas to draw into
    317      * @return true if drawing should continue beyond this frame to continue the
    318      *         animation
    319      */
    320     public boolean draw(Canvas canvas) {
    321         update();
    322 
    323         final int count = canvas.save();
    324 
    325         final float centerX = mBounds.centerX();
    326         final float centerY = mBounds.height() - mRadius;
    327 
    328         canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0);
    329 
    330         final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f;
    331         float translateX = mBounds.width() * displacement / 2;
    332 
    333         canvas.clipRect(mBounds);
    334         canvas.translate(translateX, 0);
    335         mPaint.setAlpha((int) (0xff * mGlowAlpha));
    336         canvas.drawCircle(centerX, centerY, mRadius, mPaint);
    337         canvas.restoreToCount(count);
    338 
    339         boolean oneLastFrame = false;
    340         if (mState == STATE_RECEDE && mGlowScaleY == 0) {
    341             mState = STATE_IDLE;
    342             oneLastFrame = true;
    343         }
    344 
    345         return mState != STATE_IDLE || oneLastFrame;
    346     }
    347 
    348     /**
    349      * Return the maximum height that the edge effect will be drawn at given the original
    350      * {@link #setSize(int, int) input size}.
    351      * @return The maximum height of the edge effect
    352      */
    353     public int getMaxHeight() {
    354         return (int) (mBounds.height() * MAX_GLOW_SCALE + 0.5f);
    355     }
    356 
    357     private void update() {
    358         final long time = AnimationUtils.currentAnimationTimeMillis();
    359         final float t = Math.min((time - mStartTime) / mDuration, 1.f);
    360 
    361         final float interp = mInterpolator.getInterpolation(t);
    362 
    363         mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
    364         mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
    365         mDisplacement = (mDisplacement + mTargetDisplacement) / 2;
    366 
    367         if (t >= 1.f - EPSILON) {
    368             switch (mState) {
    369                 case STATE_ABSORB:
    370                     mState = STATE_RECEDE;
    371                     mStartTime = AnimationUtils.currentAnimationTimeMillis();
    372                     mDuration = RECEDE_TIME;
    373 
    374                     mGlowAlphaStart = mGlowAlpha;
    375                     mGlowScaleYStart = mGlowScaleY;
    376 
    377                     // After absorb, the glow should fade to nothing.
    378                     mGlowAlphaFinish = 0.f;
    379                     mGlowScaleYFinish = 0.f;
    380                     break;
    381                 case STATE_PULL:
    382                     mState = STATE_PULL_DECAY;
    383                     mStartTime = AnimationUtils.currentAnimationTimeMillis();
    384                     mDuration = PULL_DECAY_TIME;
    385 
    386                     mGlowAlphaStart = mGlowAlpha;
    387                     mGlowScaleYStart = mGlowScaleY;
    388 
    389                     // After pull, the glow should fade to nothing.
    390                     mGlowAlphaFinish = 0.f;
    391                     mGlowScaleYFinish = 0.f;
    392                     break;
    393                 case STATE_PULL_DECAY:
    394                     mState = STATE_RECEDE;
    395                     break;
    396                 case STATE_RECEDE:
    397                     mState = STATE_IDLE;
    398                     break;
    399             }
    400         }
    401     }
    402 }
    403