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