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