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