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.graphics.Canvas;
     20 import android.graphics.drawable.Drawable;
     21 import android.view.animation.AnimationUtils;
     22 import android.view.animation.DecelerateInterpolator;
     23 import android.view.animation.Interpolator;
     24 
     25 /**
     26  * This class performs the glow effect used at the edges of scrollable widgets.
     27  * @hide
     28  */
     29 public class EdgeGlow {
     30     private static final String TAG = "EdgeGlow";
     31 
     32     // Time it will take the effect to fully recede in ms
     33     private static final int RECEDE_TIME = 1000;
     34 
     35     // Time it will take before a pulled glow begins receding
     36     private static final int PULL_TIME = 167;
     37 
     38     // Time it will take for a pulled glow to decay to partial strength before release
     39     private static final int PULL_DECAY_TIME = 1000;
     40 
     41     private static final float MAX_ALPHA = 0.8f;
     42     private static final float HELD_EDGE_ALPHA = 0.7f;
     43     private static final float HELD_EDGE_SCALE_Y = 0.5f;
     44     private static final float HELD_GLOW_ALPHA = 0.5f;
     45     private static final float HELD_GLOW_SCALE_Y = 0.5f;
     46 
     47     private static final float MAX_GLOW_HEIGHT = 3.f;
     48 
     49     private static final float PULL_GLOW_BEGIN = 1.f;
     50     private static final float PULL_EDGE_BEGIN = 0.6f;
     51 
     52     // Minimum velocity that will be absorbed
     53     private static final int MIN_VELOCITY = 100;
     54 
     55     private static final float EPSILON = 0.001f;
     56 
     57     private final Drawable mEdge;
     58     private final Drawable mGlow;
     59     private int mWidth;
     60     private int mHeight;
     61 
     62     private float mEdgeAlpha;
     63     private float mEdgeScaleY;
     64     private float mGlowAlpha;
     65     private float mGlowScaleY;
     66 
     67     private float mEdgeAlphaStart;
     68     private float mEdgeAlphaFinish;
     69     private float mEdgeScaleYStart;
     70     private float mEdgeScaleYFinish;
     71     private float mGlowAlphaStart;
     72     private float mGlowAlphaFinish;
     73     private float mGlowScaleYStart;
     74     private float mGlowScaleYFinish;
     75 
     76     private long mStartTime;
     77     private float mDuration;
     78 
     79     private final Interpolator mInterpolator;
     80 
     81     private static final int STATE_IDLE = 0;
     82     private static final int STATE_PULL = 1;
     83     private static final int STATE_ABSORB = 2;
     84     private static final int STATE_RECEDE = 3;
     85     private static final int STATE_PULL_DECAY = 4;
     86 
     87     // How much dragging should effect the height of the edge image.
     88     // Number determined by user testing.
     89     private static final int PULL_DISTANCE_EDGE_FACTOR = 5;
     90 
     91     // How much dragging should effect the height of the glow image.
     92     // Number determined by user testing.
     93     private static final int PULL_DISTANCE_GLOW_FACTOR = 5;
     94     private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f;
     95 
     96     private static final int VELOCITY_EDGE_FACTOR = 8;
     97     private static final int VELOCITY_GLOW_FACTOR = 16;
     98 
     99     private int mState = STATE_IDLE;
    100 
    101     private float mPullDistance;
    102 
    103     public EdgeGlow(Drawable edge, Drawable glow) {
    104         mEdge = edge;
    105         mGlow = glow;
    106 
    107         mInterpolator = new DecelerateInterpolator();
    108     }
    109 
    110     public void setSize(int width, int height) {
    111         mWidth = width;
    112         mHeight = height;
    113     }
    114 
    115     public boolean isFinished() {
    116         return mState == STATE_IDLE;
    117     }
    118 
    119     public void finish() {
    120         mState = STATE_IDLE;
    121     }
    122 
    123     /**
    124      * Call when the object is pulled by the user.
    125      *
    126      * @param deltaDistance Change in distance since the last call
    127      */
    128     public void onPull(float deltaDistance) {
    129         final long now = AnimationUtils.currentAnimationTimeMillis();
    130         if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
    131             return;
    132         }
    133         if (mState != STATE_PULL) {
    134             mGlowScaleY = PULL_GLOW_BEGIN;
    135         }
    136         mState = STATE_PULL;
    137 
    138         mStartTime = now;
    139         mDuration = PULL_TIME;
    140 
    141         mPullDistance += deltaDistance;
    142         float distance = Math.abs(mPullDistance);
    143 
    144         mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
    145         mEdgeScaleY = mEdgeScaleYStart = Math.max(
    146                 HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
    147 
    148         mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
    149                 mGlowAlpha +
    150                 (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
    151 
    152         float glowChange = Math.abs(deltaDistance);
    153         if (deltaDistance > 0 && mPullDistance < 0) {
    154             glowChange = -glowChange;
    155         }
    156         if (mPullDistance == 0) {
    157             mGlowScaleY = 0;
    158         }
    159 
    160         // Do not allow glow to get larger than MAX_GLOW_HEIGHT.
    161         mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max(
    162                 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));
    163 
    164         mEdgeAlphaFinish = mEdgeAlpha;
    165         mEdgeScaleYFinish = mEdgeScaleY;
    166         mGlowAlphaFinish = mGlowAlpha;
    167         mGlowScaleYFinish = mGlowScaleY;
    168     }
    169 
    170     /**
    171      * Call when the object is released after being pulled.
    172      */
    173     public void onRelease() {
    174         mPullDistance = 0;
    175 
    176         if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
    177             return;
    178         }
    179 
    180         mState = STATE_RECEDE;
    181         mEdgeAlphaStart = mEdgeAlpha;
    182         mEdgeScaleYStart = mEdgeScaleY;
    183         mGlowAlphaStart = mGlowAlpha;
    184         mGlowScaleYStart = mGlowScaleY;
    185 
    186         mEdgeAlphaFinish = 0.f;
    187         mEdgeScaleYFinish = 0.f;
    188         mGlowAlphaFinish = 0.f;
    189         mGlowScaleYFinish = 0.f;
    190 
    191         mStartTime = AnimationUtils.currentAnimationTimeMillis();
    192         mDuration = RECEDE_TIME;
    193     }
    194 
    195     /**
    196      * Call when the effect absorbs an impact at the given velocity.
    197      *
    198      * @param velocity Velocity at impact in pixels per second.
    199      */
    200     public void onAbsorb(int velocity) {
    201         mState = STATE_ABSORB;
    202         velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
    203 
    204         mStartTime = AnimationUtils.currentAnimationTimeMillis();
    205         mDuration = 0.1f + (velocity * 0.03f);
    206 
    207         // The edge should always be at least partially visible, regardless
    208         // of velocity.
    209         mEdgeAlphaStart = 0.f;
    210         mEdgeScaleY = mEdgeScaleYStart = 0.f;
    211         // The glow depends more on the velocity, and therefore starts out
    212         // nearly invisible.
    213         mGlowAlphaStart = 0.5f;
    214         mGlowScaleYStart = 0.f;
    215 
    216         // Factor the velocity by 8. Testing on device shows this works best to
    217         // reflect the strength of the user's scrolling.
    218         mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
    219         // Edge should never get larger than the size of its asset.
    220         mEdgeScaleYFinish = Math.max(
    221                 HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
    222 
    223         // Growth for the size of the glow should be quadratic to properly
    224         // respond
    225         // to a user's scrolling speed. The faster the scrolling speed, the more
    226         // intense the effect should be for both the size and the saturation.
    227         mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
    228         // Alpha should change for the glow as well as size.
    229         mGlowAlphaFinish = Math.max(
    230                 mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
    231     }
    232 
    233 
    234     /**
    235      * Draw into the provided canvas. Assumes that the canvas has been rotated
    236      * accordingly and the size has been set. The effect will be drawn the full
    237      * width of X=0 to X=width, emitting from Y=0 and extending to some factor <
    238      * 1.f of height.
    239      *
    240      * @param canvas Canvas to draw into
    241      * @return true if drawing should continue beyond this frame to continue the
    242      *         animation
    243      */
    244     public boolean draw(Canvas canvas) {
    245         update();
    246 
    247         final int edgeHeight = mEdge.getIntrinsicHeight();
    248         final int glowHeight = mGlow.getIntrinsicHeight();
    249 
    250         final float distScale = (float) mHeight / mWidth;
    251 
    252         mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
    253         // Width of the image should be 3 * the width of the screen.
    254         // Should start off screen to the left.
    255         mGlow.setBounds(-mWidth, 0, mWidth * 2, (int) Math.min(
    256                 glowHeight * mGlowScaleY * distScale * 0.6f, mHeight * MAX_GLOW_HEIGHT));
    257         mGlow.draw(canvas);
    258 
    259         mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
    260         mEdge.setBounds(0, 0, mWidth, (int) (edgeHeight * mEdgeScaleY));
    261         mEdge.draw(canvas);
    262 
    263         return mState != STATE_IDLE;
    264     }
    265 
    266     private void update() {
    267         final long time = AnimationUtils.currentAnimationTimeMillis();
    268         final float t = Math.min((time - mStartTime) / mDuration, 1.f);
    269 
    270         final float interp = mInterpolator.getInterpolation(t);
    271 
    272         mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
    273         mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
    274         mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
    275         mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
    276 
    277         if (t >= 1.f - EPSILON) {
    278             switch (mState) {
    279                 case STATE_ABSORB:
    280                     mState = STATE_RECEDE;
    281                     mStartTime = AnimationUtils.currentAnimationTimeMillis();
    282                     mDuration = RECEDE_TIME;
    283 
    284                     mEdgeAlphaStart = mEdgeAlpha;
    285                     mEdgeScaleYStart = mEdgeScaleY;
    286                     mGlowAlphaStart = mGlowAlpha;
    287                     mGlowScaleYStart = mGlowScaleY;
    288 
    289                     // After absorb, the glow and edge should fade to nothing.
    290                     mEdgeAlphaFinish = 0.f;
    291                     mEdgeScaleYFinish = 0.f;
    292                     mGlowAlphaFinish = 0.f;
    293                     mGlowScaleYFinish = 0.f;
    294                     break;
    295                 case STATE_PULL:
    296                     mState = STATE_PULL_DECAY;
    297                     mStartTime = AnimationUtils.currentAnimationTimeMillis();
    298                     mDuration = PULL_DECAY_TIME;
    299 
    300                     mEdgeAlphaStart = mEdgeAlpha;
    301                     mEdgeScaleYStart = mEdgeScaleY;
    302                     mGlowAlphaStart = mGlowAlpha;
    303                     mGlowScaleYStart = mGlowScaleY;
    304 
    305                     // After pull, the glow and edge should fade to nothing.
    306                     mEdgeAlphaFinish = 0.f;
    307                     mEdgeScaleYFinish = 0.f;
    308                     mGlowAlphaFinish = 0.f;
    309                     mGlowScaleYFinish = 0.f;
    310                     break;
    311                 case STATE_PULL_DECAY:
    312                     // When receding, we want edge to decrease more slowly
    313                     // than the glow.
    314                     float factor = mGlowScaleYFinish != 0 ? 1
    315                             / (mGlowScaleYFinish * mGlowScaleYFinish)
    316                             : Float.MAX_VALUE;
    317                     mEdgeScaleY = mEdgeScaleYStart +
    318                         (mEdgeScaleYFinish - mEdgeScaleYStart) *
    319                             interp * factor;
    320                     break;
    321                 case STATE_RECEDE:
    322                     mState = STATE_IDLE;
    323                     break;
    324             }
    325         }
    326     }
    327 }
    328