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 com.android.internal.R;
     20 
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.graphics.Canvas;
     24 import android.graphics.drawable.Drawable;
     25 import android.view.animation.AnimationUtils;
     26 import android.view.animation.DecelerateInterpolator;
     27 import android.view.animation.Interpolator;
     28 
     29 /**
     30  * This class performs the graphical effect used at the edges of scrollable widgets
     31  * when the user scrolls beyond the content bounds in 2D space.
     32  *
     33  * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
     34  * instance for each edge that should show the effect, feed it input data using
     35  * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
     36  * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
     37  * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
     38  * false after drawing, the edge effect's animation is not yet complete and the widget
     39  * should schedule another drawing pass to continue the animation.</p>
     40  *
     41  * <p>When drawing, widgets should draw their main content and child views first,
     42  * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
     43  * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
     44  * The edge effect may then be drawn on top of the view's content using the
     45  * {@link #draw(Canvas)} method.</p>
     46  */
     47 public class EdgeEffect {
     48     private static final String TAG = "EdgeEffect";
     49 
     50     // Time it will take the effect to fully recede in ms
     51     private static final int RECEDE_TIME = 1000;
     52 
     53     // Time it will take before a pulled glow begins receding in ms
     54     private static final int PULL_TIME = 167;
     55 
     56     // Time it will take in ms for a pulled glow to decay to partial strength before release
     57     private static final int PULL_DECAY_TIME = 1000;
     58 
     59     private static final float MAX_ALPHA = 0.8f;
     60     private static final float HELD_EDGE_ALPHA = 0.7f;
     61     private static final float HELD_EDGE_SCALE_Y = 0.5f;
     62     private static final float HELD_GLOW_ALPHA = 0.5f;
     63     private static final float HELD_GLOW_SCALE_Y = 0.5f;
     64 
     65     private static final float MAX_GLOW_HEIGHT = 4.f;
     66 
     67     private static final float PULL_GLOW_BEGIN = 1.f;
     68     private static final float PULL_EDGE_BEGIN = 0.6f;
     69 
     70     // Minimum velocity that will be absorbed
     71     private static final int MIN_VELOCITY = 100;
     72 
     73     private static final float EPSILON = 0.001f;
     74 
     75     private final Drawable mEdge;
     76     private final Drawable mGlow;
     77     private int mWidth;
     78     private int mHeight;
     79     private final int MIN_WIDTH = 300;
     80     private final int mMinWidth;
     81 
     82     private float mEdgeAlpha;
     83     private float mEdgeScaleY;
     84     private float mGlowAlpha;
     85     private float mGlowScaleY;
     86 
     87     private float mEdgeAlphaStart;
     88     private float mEdgeAlphaFinish;
     89     private float mEdgeScaleYStart;
     90     private float mEdgeScaleYFinish;
     91     private float mGlowAlphaStart;
     92     private float mGlowAlphaFinish;
     93     private float mGlowScaleYStart;
     94     private float mGlowScaleYFinish;
     95 
     96     private long mStartTime;
     97     private float mDuration;
     98 
     99     private final Interpolator mInterpolator;
    100 
    101     private static final int STATE_IDLE = 0;
    102     private static final int STATE_PULL = 1;
    103     private static final int STATE_ABSORB = 2;
    104     private static final int STATE_RECEDE = 3;
    105     private static final int STATE_PULL_DECAY = 4;
    106 
    107     // How much dragging should effect the height of the edge image.
    108     // Number determined by user testing.
    109     private static final int PULL_DISTANCE_EDGE_FACTOR = 7;
    110 
    111     // How much dragging should effect the height of the glow image.
    112     // Number determined by user testing.
    113     private static final int PULL_DISTANCE_GLOW_FACTOR = 7;
    114     private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f;
    115 
    116     private static final int VELOCITY_EDGE_FACTOR = 8;
    117     private static final int VELOCITY_GLOW_FACTOR = 16;
    118 
    119     private int mState = STATE_IDLE;
    120 
    121     private float mPullDistance;
    122 
    123     /**
    124      * Construct a new EdgeEffect with a theme appropriate for the provided context.
    125      * @param context Context used to provide theming and resource information for the EdgeEffect
    126      */
    127     public EdgeEffect(Context context) {
    128         final Resources res = context.getResources();
    129         mEdge = res.getDrawable(R.drawable.overscroll_edge);
    130         mGlow = res.getDrawable(R.drawable.overscroll_glow);
    131 
    132         mMinWidth = (int) (res.getDisplayMetrics().density * MIN_WIDTH + 0.5f);
    133         mInterpolator = new DecelerateInterpolator();
    134     }
    135 
    136     /**
    137      * Set the size of this edge effect in pixels.
    138      *
    139      * @param width Effect width in pixels
    140      * @param height Effect height in pixels
    141      */
    142     public void setSize(int width, int height) {
    143         mWidth = width;
    144         mHeight = height;
    145     }
    146 
    147     /**
    148      * Reports if this EdgeEffect's animation is finished. If this method returns false
    149      * after a call to {@link #draw(Canvas)} the host widget should schedule another
    150      * drawing pass to continue the animation.
    151      *
    152      * @return true if animation is finished, false if drawing should continue on the next frame.
    153      */
    154     public boolean isFinished() {
    155         return mState == STATE_IDLE;
    156     }
    157 
    158     /**
    159      * Immediately finish the current animation.
    160      * After this call {@link #isFinished()} will return true.
    161      */
    162     public void finish() {
    163         mState = STATE_IDLE;
    164     }
    165 
    166     /**
    167      * A view should call this when content is pulled away from an edge by the user.
    168      * This will update the state of the current visual effect and its associated animation.
    169      * The host view should always {@link android.view.View#invalidate()} after this
    170      * and draw the results accordingly.
    171      *
    172      * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
    173      *                      1.f (full length of the view) or negative values to express change
    174      *                      back toward the edge reached to initiate the effect.
    175      */
    176     public void onPull(float deltaDistance) {
    177         final long now = AnimationUtils.currentAnimationTimeMillis();
    178         if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
    179             return;
    180         }
    181         if (mState != STATE_PULL) {
    182             mGlowScaleY = PULL_GLOW_BEGIN;
    183         }
    184         mState = STATE_PULL;
    185 
    186         mStartTime = now;
    187         mDuration = PULL_TIME;
    188 
    189         mPullDistance += deltaDistance;
    190         float distance = Math.abs(mPullDistance);
    191 
    192         mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
    193         mEdgeScaleY = mEdgeScaleYStart = Math.max(
    194                 HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
    195 
    196         mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
    197                 mGlowAlpha +
    198                 (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
    199 
    200         float glowChange = Math.abs(deltaDistance);
    201         if (deltaDistance > 0 && mPullDistance < 0) {
    202             glowChange = -glowChange;
    203         }
    204         if (mPullDistance == 0) {
    205             mGlowScaleY = 0;
    206         }
    207 
    208         // Do not allow glow to get larger than MAX_GLOW_HEIGHT.
    209         mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max(
    210                 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));
    211 
    212         mEdgeAlphaFinish = mEdgeAlpha;
    213         mEdgeScaleYFinish = mEdgeScaleY;
    214         mGlowAlphaFinish = mGlowAlpha;
    215         mGlowScaleYFinish = mGlowScaleY;
    216     }
    217 
    218     /**
    219      * Call when the object is released after being pulled.
    220      * This will begin the "decay" phase of the effect. After calling this method
    221      * the host view should {@link android.view.View#invalidate()} and thereby
    222      * draw the results accordingly.
    223      */
    224     public void onRelease() {
    225         mPullDistance = 0;
    226 
    227         if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
    228             return;
    229         }
    230 
    231         mState = STATE_RECEDE;
    232         mEdgeAlphaStart = mEdgeAlpha;
    233         mEdgeScaleYStart = mEdgeScaleY;
    234         mGlowAlphaStart = mGlowAlpha;
    235         mGlowScaleYStart = mGlowScaleY;
    236 
    237         mEdgeAlphaFinish = 0.f;
    238         mEdgeScaleYFinish = 0.f;
    239         mGlowAlphaFinish = 0.f;
    240         mGlowScaleYFinish = 0.f;
    241 
    242         mStartTime = AnimationUtils.currentAnimationTimeMillis();
    243         mDuration = RECEDE_TIME;
    244     }
    245 
    246     /**
    247      * Call when the effect absorbs an impact at the given velocity.
    248      * Used when a fling reaches the scroll boundary.
    249      *
    250      * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
    251      * the method <code>getCurrVelocity</code> will provide a reasonable approximation
    252      * to use here.</p>
    253      *
    254      * @param velocity Velocity at impact in pixels per second.
    255      */
    256     public void onAbsorb(int velocity) {
    257         mState = STATE_ABSORB;
    258         velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
    259 
    260         mStartTime = AnimationUtils.currentAnimationTimeMillis();
    261         mDuration = 0.1f + (velocity * 0.03f);
    262 
    263         // The edge should always be at least partially visible, regardless
    264         // of velocity.
    265         mEdgeAlphaStart = 0.f;
    266         mEdgeScaleY = mEdgeScaleYStart = 0.f;
    267         // The glow depends more on the velocity, and therefore starts out
    268         // nearly invisible.
    269         mGlowAlphaStart = 0.5f;
    270         mGlowScaleYStart = 0.f;
    271 
    272         // Factor the velocity by 8. Testing on device shows this works best to
    273         // reflect the strength of the user's scrolling.
    274         mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
    275         // Edge should never get larger than the size of its asset.
    276         mEdgeScaleYFinish = Math.max(
    277                 HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
    278 
    279         // Growth for the size of the glow should be quadratic to properly
    280         // respond
    281         // to a user's scrolling speed. The faster the scrolling speed, the more
    282         // intense the effect should be for both the size and the saturation.
    283         mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
    284         // Alpha should change for the glow as well as size.
    285         mGlowAlphaFinish = Math.max(
    286                 mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
    287     }
    288 
    289 
    290     /**
    291      * Draw into the provided canvas. Assumes that the canvas has been rotated
    292      * accordingly and the size has been set. The effect will be drawn the full
    293      * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
    294      * 1.f of height.
    295      *
    296      * @param canvas Canvas to draw into
    297      * @return true if drawing should continue beyond this frame to continue the
    298      *         animation
    299      */
    300     public boolean draw(Canvas canvas) {
    301         update();
    302 
    303         final int edgeHeight = mEdge.getIntrinsicHeight();
    304         final int edgeWidth = mEdge.getIntrinsicWidth();
    305         final int glowHeight = mGlow.getIntrinsicHeight();
    306         final int glowWidth = mGlow.getIntrinsicWidth();
    307 
    308         mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
    309 
    310         int glowBottom = (int) Math.min(
    311                 glowHeight * mGlowScaleY * glowHeight/ glowWidth * 0.6f,
    312                 glowHeight * MAX_GLOW_HEIGHT);
    313         if (mWidth < mMinWidth) {
    314             // Center the glow and clip it.
    315             int glowLeft = (mWidth - mMinWidth)/2;
    316             mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom);
    317         } else {
    318             // Stretch the glow to fit.
    319             mGlow.setBounds(0, 0, mWidth, glowBottom);
    320         }
    321 
    322         mGlow.draw(canvas);
    323 
    324         mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
    325 
    326         int edgeBottom = (int) (edgeHeight * mEdgeScaleY);
    327         if (mWidth < mMinWidth) {
    328             // Center the edge and clip it.
    329             int edgeLeft = (mWidth - mMinWidth)/2;
    330             mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom);
    331         } else {
    332             // Stretch the edge to fit.
    333             mEdge.setBounds(0, 0, mWidth, edgeBottom);
    334         }
    335         mEdge.draw(canvas);
    336 
    337         return mState != STATE_IDLE;
    338     }
    339 
    340     private void update() {
    341         final long time = AnimationUtils.currentAnimationTimeMillis();
    342         final float t = Math.min((time - mStartTime) / mDuration, 1.f);
    343 
    344         final float interp = mInterpolator.getInterpolation(t);
    345 
    346         mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
    347         mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
    348         mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
    349         mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
    350 
    351         if (t >= 1.f - EPSILON) {
    352             switch (mState) {
    353                 case STATE_ABSORB:
    354                     mState = STATE_RECEDE;
    355                     mStartTime = AnimationUtils.currentAnimationTimeMillis();
    356                     mDuration = RECEDE_TIME;
    357 
    358                     mEdgeAlphaStart = mEdgeAlpha;
    359                     mEdgeScaleYStart = mEdgeScaleY;
    360                     mGlowAlphaStart = mGlowAlpha;
    361                     mGlowScaleYStart = mGlowScaleY;
    362 
    363                     // After absorb, the glow and edge should fade to nothing.
    364                     mEdgeAlphaFinish = 0.f;
    365                     mEdgeScaleYFinish = 0.f;
    366                     mGlowAlphaFinish = 0.f;
    367                     mGlowScaleYFinish = 0.f;
    368                     break;
    369                 case STATE_PULL:
    370                     mState = STATE_PULL_DECAY;
    371                     mStartTime = AnimationUtils.currentAnimationTimeMillis();
    372                     mDuration = PULL_DECAY_TIME;
    373 
    374                     mEdgeAlphaStart = mEdgeAlpha;
    375                     mEdgeScaleYStart = mEdgeScaleY;
    376                     mGlowAlphaStart = mGlowAlpha;
    377                     mGlowScaleYStart = mGlowScaleY;
    378 
    379                     // After pull, the glow and edge should fade to nothing.
    380                     mEdgeAlphaFinish = 0.f;
    381                     mEdgeScaleYFinish = 0.f;
    382                     mGlowAlphaFinish = 0.f;
    383                     mGlowScaleYFinish = 0.f;
    384                     break;
    385                 case STATE_PULL_DECAY:
    386                     // When receding, we want edge to decrease more slowly
    387                     // than the glow.
    388                     float factor = mGlowScaleYFinish != 0 ? 1
    389                             / (mGlowScaleYFinish * mGlowScaleYFinish)
    390                             : Float.MAX_VALUE;
    391                     mEdgeScaleY = mEdgeScaleYStart +
    392                         (mEdgeScaleYFinish - mEdgeScaleYStart) *
    393                             interp * factor;
    394                     mState = STATE_RECEDE;
    395                     break;
    396                 case STATE_RECEDE:
    397                     mState = STATE_IDLE;
    398                     break;
    399             }
    400         }
    401     }
    402 }
    403