Home | History | Annotate | Download | only in drawable
      1 /*
      2  * Copyright (C) 2013 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.graphics.drawable;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.animation.TimeInterpolator;
     23 import android.graphics.Canvas;
     24 import android.graphics.CanvasProperty;
     25 import android.graphics.Paint;
     26 import android.graphics.Rect;
     27 import android.util.MathUtils;
     28 import android.view.HardwareCanvas;
     29 import android.view.RenderNodeAnimator;
     30 import android.view.animation.LinearInterpolator;
     31 
     32 import java.util.ArrayList;
     33 
     34 /**
     35  * Draws a Material ripple.
     36  */
     37 class Ripple {
     38     private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
     39     private static final TimeInterpolator DECEL_INTERPOLATOR = new LogInterpolator();
     40 
     41     private static final float GLOBAL_SPEED = 1.0f;
     42     private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED;
     43     private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED;
     44     private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
     45 
     46     private static final long RIPPLE_ENTER_DELAY = 80;
     47 
     48     // Hardware animators.
     49     private final ArrayList<RenderNodeAnimator> mRunningAnimations =
     50             new ArrayList<RenderNodeAnimator>();
     51 
     52     private final RippleDrawable mOwner;
     53 
     54     /** Bounds used for computing max radius. */
     55     private final Rect mBounds;
     56 
     57     /** Maximum ripple radius. */
     58     private float mOuterRadius;
     59 
     60     /** Screen density used to adjust pixel-based velocities. */
     61     private float mDensity;
     62 
     63     private float mStartingX;
     64     private float mStartingY;
     65     private float mClampedStartingX;
     66     private float mClampedStartingY;
     67 
     68     // Hardware rendering properties.
     69     private CanvasProperty<Paint> mPropPaint;
     70     private CanvasProperty<Float> mPropRadius;
     71     private CanvasProperty<Float> mPropX;
     72     private CanvasProperty<Float> mPropY;
     73 
     74     // Software animators.
     75     private ObjectAnimator mAnimRadius;
     76     private ObjectAnimator mAnimOpacity;
     77     private ObjectAnimator mAnimX;
     78     private ObjectAnimator mAnimY;
     79 
     80     // Temporary paint used for creating canvas properties.
     81     private Paint mTempPaint;
     82 
     83     // Software rendering properties.
     84     private float mOpacity = 1;
     85     private float mOuterX;
     86     private float mOuterY;
     87 
     88     // Values used to tween between the start and end positions.
     89     private float mTweenRadius = 0;
     90     private float mTweenX = 0;
     91     private float mTweenY = 0;
     92 
     93     /** Whether we should be drawing hardware animations. */
     94     private boolean mHardwareAnimating;
     95 
     96     /** Whether we can use hardware acceleration for the exit animation. */
     97     private boolean mCanUseHardware;
     98 
     99     /** Whether we have an explicit maximum radius. */
    100     private boolean mHasMaxRadius;
    101 
    102     /** Whether we were canceled externally and should avoid self-removal. */
    103     private boolean mCanceled;
    104 
    105     private boolean mHasPendingHardwareExit;
    106     private int mPendingRadiusDuration;
    107     private int mPendingOpacityDuration;
    108 
    109     /**
    110      * Creates a new ripple.
    111      */
    112     public Ripple(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
    113         mOwner = owner;
    114         mBounds = bounds;
    115 
    116         mStartingX = startingX;
    117         mStartingY = startingY;
    118     }
    119 
    120     public void setup(int maxRadius, float density) {
    121         if (maxRadius != RippleDrawable.RADIUS_AUTO) {
    122             mHasMaxRadius = true;
    123             mOuterRadius = maxRadius;
    124         } else {
    125             final float halfWidth = mBounds.width() / 2.0f;
    126             final float halfHeight = mBounds.height() / 2.0f;
    127             mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
    128         }
    129 
    130         mOuterX = 0;
    131         mOuterY = 0;
    132         mDensity = density;
    133 
    134         clampStartingPosition();
    135     }
    136 
    137     public boolean isHardwareAnimating() {
    138         return mHardwareAnimating;
    139     }
    140 
    141     private void clampStartingPosition() {
    142         final float cX = mBounds.exactCenterX();
    143         final float cY = mBounds.exactCenterY();
    144         final float dX = mStartingX - cX;
    145         final float dY = mStartingY - cY;
    146         final float r = mOuterRadius;
    147         if (dX * dX + dY * dY > r * r) {
    148             // Point is outside the circle, clamp to the circumference.
    149             final double angle = Math.atan2(dY, dX);
    150             mClampedStartingX = cX + (float) (Math.cos(angle) * r);
    151             mClampedStartingY = cY + (float) (Math.sin(angle) * r);
    152         } else {
    153             mClampedStartingX = mStartingX;
    154             mClampedStartingY = mStartingY;
    155         }
    156     }
    157 
    158     public void onHotspotBoundsChanged() {
    159         if (!mHasMaxRadius) {
    160             final float halfWidth = mBounds.width() / 2.0f;
    161             final float halfHeight = mBounds.height() / 2.0f;
    162             mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
    163 
    164             clampStartingPosition();
    165         }
    166     }
    167 
    168     public void setOpacity(float a) {
    169         mOpacity = a;
    170         invalidateSelf();
    171     }
    172 
    173     public float getOpacity() {
    174         return mOpacity;
    175     }
    176 
    177     @SuppressWarnings("unused")
    178     public void setRadiusGravity(float r) {
    179         mTweenRadius = r;
    180         invalidateSelf();
    181     }
    182 
    183     @SuppressWarnings("unused")
    184     public float getRadiusGravity() {
    185         return mTweenRadius;
    186     }
    187 
    188     @SuppressWarnings("unused")
    189     public void setXGravity(float x) {
    190         mTweenX = x;
    191         invalidateSelf();
    192     }
    193 
    194     @SuppressWarnings("unused")
    195     public float getXGravity() {
    196         return mTweenX;
    197     }
    198 
    199     @SuppressWarnings("unused")
    200     public void setYGravity(float y) {
    201         mTweenY = y;
    202         invalidateSelf();
    203     }
    204 
    205     @SuppressWarnings("unused")
    206     public float getYGravity() {
    207         return mTweenY;
    208     }
    209 
    210     /**
    211      * Draws the ripple centered at (0,0) using the specified paint.
    212      */
    213     public boolean draw(Canvas c, Paint p) {
    214         final boolean canUseHardware = c.isHardwareAccelerated();
    215         if (mCanUseHardware != canUseHardware && mCanUseHardware) {
    216             // We've switched from hardware to non-hardware mode. Panic.
    217             cancelHardwareAnimations(true);
    218         }
    219         mCanUseHardware = canUseHardware;
    220 
    221         final boolean hasContent;
    222         if (canUseHardware && (mHardwareAnimating || mHasPendingHardwareExit)) {
    223             hasContent = drawHardware((HardwareCanvas) c, p);
    224         } else {
    225             hasContent = drawSoftware(c, p);
    226         }
    227 
    228         return hasContent;
    229     }
    230 
    231     private boolean drawHardware(HardwareCanvas c, Paint p) {
    232         if (mHasPendingHardwareExit) {
    233             cancelHardwareAnimations(false);
    234             startPendingHardwareExit(c, p);
    235         }
    236 
    237         c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
    238 
    239         return true;
    240     }
    241 
    242     private boolean drawSoftware(Canvas c, Paint p) {
    243         boolean hasContent = false;
    244 
    245         final int paintAlpha = p.getAlpha();
    246         final int alpha = (int) (paintAlpha * mOpacity + 0.5f);
    247         final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
    248         if (alpha > 0 && radius > 0) {
    249             final float x = MathUtils.lerp(
    250                     mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
    251             final float y = MathUtils.lerp(
    252                     mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
    253             p.setAlpha(alpha);
    254             c.drawCircle(x, y, radius, p);
    255             p.setAlpha(paintAlpha);
    256             hasContent = true;
    257         }
    258 
    259         return hasContent;
    260     }
    261 
    262     /**
    263      * Returns the maximum bounds of the ripple relative to the ripple center.
    264      */
    265     public void getBounds(Rect bounds) {
    266         final int outerX = (int) mOuterX;
    267         final int outerY = (int) mOuterY;
    268         final int r = (int) mOuterRadius + 1;
    269         bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
    270     }
    271 
    272     /**
    273      * Specifies the starting position relative to the drawable bounds. No-op if
    274      * the ripple has already entered.
    275      */
    276     public void move(float x, float y) {
    277         mStartingX = x;
    278         mStartingY = y;
    279 
    280         clampStartingPosition();
    281     }
    282 
    283     /**
    284      * Starts the enter animation.
    285      */
    286     public void enter() {
    287         cancel();
    288 
    289         final int radiusDuration = (int)
    290                 (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
    291 
    292         final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
    293         radius.setAutoCancel(true);
    294         radius.setDuration(radiusDuration);
    295         radius.setInterpolator(LINEAR_INTERPOLATOR);
    296         radius.setStartDelay(RIPPLE_ENTER_DELAY);
    297 
    298         final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
    299         cX.setAutoCancel(true);
    300         cX.setDuration(radiusDuration);
    301         cX.setInterpolator(LINEAR_INTERPOLATOR);
    302         cX.setStartDelay(RIPPLE_ENTER_DELAY);
    303 
    304         final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
    305         cY.setAutoCancel(true);
    306         cY.setDuration(radiusDuration);
    307         cY.setInterpolator(LINEAR_INTERPOLATOR);
    308         cY.setStartDelay(RIPPLE_ENTER_DELAY);
    309 
    310         mAnimRadius = radius;
    311         mAnimX = cX;
    312         mAnimY = cY;
    313 
    314         // Enter animations always run on the UI thread, since it's unlikely
    315         // that anything interesting is happening until the user lifts their
    316         // finger.
    317         radius.start();
    318         cX.start();
    319         cY.start();
    320     }
    321 
    322     /**
    323      * Starts the exit animation.
    324      */
    325     public void exit() {
    326         final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
    327         final float remaining;
    328         if (mAnimRadius != null && mAnimRadius.isRunning()) {
    329             remaining = mOuterRadius - radius;
    330         } else {
    331             remaining = mOuterRadius;
    332         }
    333 
    334         cancel();
    335 
    336         final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
    337                 + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
    338         final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
    339 
    340         if (mCanUseHardware) {
    341             createPendingHardwareExit(radiusDuration, opacityDuration);
    342         } else {
    343             exitSoftware(radiusDuration, opacityDuration);
    344         }
    345     }
    346 
    347     private void createPendingHardwareExit(int radiusDuration, int opacityDuration) {
    348         mHasPendingHardwareExit = true;
    349         mPendingRadiusDuration = radiusDuration;
    350         mPendingOpacityDuration = opacityDuration;
    351 
    352         // The animation will start on the next draw().
    353         invalidateSelf();
    354     }
    355 
    356     private void startPendingHardwareExit(HardwareCanvas c, Paint p) {
    357         mHasPendingHardwareExit = false;
    358 
    359         final int radiusDuration = mPendingRadiusDuration;
    360         final int opacityDuration = mPendingOpacityDuration;
    361 
    362         final float startX = MathUtils.lerp(
    363                 mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
    364         final float startY = MathUtils.lerp(
    365                 mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
    366 
    367         final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
    368         final Paint paint = getTempPaint(p);
    369         paint.setAlpha((int) (paint.getAlpha() * mOpacity + 0.5f));
    370         mPropPaint = CanvasProperty.createPaint(paint);
    371         mPropRadius = CanvasProperty.createFloat(startRadius);
    372         mPropX = CanvasProperty.createFloat(startX);
    373         mPropY = CanvasProperty.createFloat(startY);
    374 
    375         final RenderNodeAnimator radiusAnim = new RenderNodeAnimator(mPropRadius, mOuterRadius);
    376         radiusAnim.setDuration(radiusDuration);
    377         radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
    378         radiusAnim.setTarget(c);
    379         radiusAnim.start();
    380 
    381         final RenderNodeAnimator xAnim = new RenderNodeAnimator(mPropX, mOuterX);
    382         xAnim.setDuration(radiusDuration);
    383         xAnim.setInterpolator(DECEL_INTERPOLATOR);
    384         xAnim.setTarget(c);
    385         xAnim.start();
    386 
    387         final RenderNodeAnimator yAnim = new RenderNodeAnimator(mPropY, mOuterY);
    388         yAnim.setDuration(radiusDuration);
    389         yAnim.setInterpolator(DECEL_INTERPOLATOR);
    390         yAnim.setTarget(c);
    391         yAnim.start();
    392 
    393         final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPropPaint,
    394                 RenderNodeAnimator.PAINT_ALPHA, 0);
    395         opacityAnim.setDuration(opacityDuration);
    396         opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
    397         opacityAnim.addListener(mAnimationListener);
    398         opacityAnim.setTarget(c);
    399         opacityAnim.start();
    400 
    401         mRunningAnimations.add(radiusAnim);
    402         mRunningAnimations.add(opacityAnim);
    403         mRunningAnimations.add(xAnim);
    404         mRunningAnimations.add(yAnim);
    405 
    406         mHardwareAnimating = true;
    407 
    408         // Set up the software values to match the hardware end values.
    409         mOpacity = 0;
    410         mTweenX = 1;
    411         mTweenY = 1;
    412         mTweenRadius = 1;
    413     }
    414 
    415     /**
    416      * Jump all animations to their end state. The caller is responsible for
    417      * removing the ripple from the list of animating ripples.
    418      */
    419     public void jump() {
    420         mCanceled = true;
    421         endSoftwareAnimations();
    422         cancelHardwareAnimations(true);
    423         mCanceled = false;
    424     }
    425 
    426     private void endSoftwareAnimations() {
    427         if (mAnimRadius != null) {
    428             mAnimRadius.end();
    429             mAnimRadius = null;
    430         }
    431 
    432         if (mAnimOpacity != null) {
    433             mAnimOpacity.end();
    434             mAnimOpacity = null;
    435         }
    436 
    437         if (mAnimX != null) {
    438             mAnimX.end();
    439             mAnimX = null;
    440         }
    441 
    442         if (mAnimY != null) {
    443             mAnimY.end();
    444             mAnimY = null;
    445         }
    446     }
    447 
    448     private Paint getTempPaint(Paint original) {
    449         if (mTempPaint == null) {
    450             mTempPaint = new Paint();
    451         }
    452         mTempPaint.set(original);
    453         return mTempPaint;
    454     }
    455 
    456     private void exitSoftware(int radiusDuration, int opacityDuration) {
    457         final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
    458         radiusAnim.setAutoCancel(true);
    459         radiusAnim.setDuration(radiusDuration);
    460         radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
    461 
    462         final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
    463         xAnim.setAutoCancel(true);
    464         xAnim.setDuration(radiusDuration);
    465         xAnim.setInterpolator(DECEL_INTERPOLATOR);
    466 
    467         final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
    468         yAnim.setAutoCancel(true);
    469         yAnim.setDuration(radiusDuration);
    470         yAnim.setInterpolator(DECEL_INTERPOLATOR);
    471 
    472         final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0);
    473         opacityAnim.setAutoCancel(true);
    474         opacityAnim.setDuration(opacityDuration);
    475         opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
    476         opacityAnim.addListener(mAnimationListener);
    477 
    478         mAnimRadius = radiusAnim;
    479         mAnimOpacity = opacityAnim;
    480         mAnimX = xAnim;
    481         mAnimY = yAnim;
    482 
    483         radiusAnim.start();
    484         opacityAnim.start();
    485         xAnim.start();
    486         yAnim.start();
    487     }
    488 
    489     /**
    490      * Cancels all animations. The caller is responsible for removing
    491      * the ripple from the list of animating ripples.
    492      */
    493     public void cancel() {
    494         mCanceled = true;
    495         cancelSoftwareAnimations();
    496         cancelHardwareAnimations(false);
    497         mCanceled = false;
    498     }
    499 
    500     private void cancelSoftwareAnimations() {
    501         if (mAnimRadius != null) {
    502             mAnimRadius.cancel();
    503             mAnimRadius = null;
    504         }
    505 
    506         if (mAnimOpacity != null) {
    507             mAnimOpacity.cancel();
    508             mAnimOpacity = null;
    509         }
    510 
    511         if (mAnimX != null) {
    512             mAnimX.cancel();
    513             mAnimX = null;
    514         }
    515 
    516         if (mAnimY != null) {
    517             mAnimY.cancel();
    518             mAnimY = null;
    519         }
    520     }
    521 
    522     /**
    523      * Cancels any running hardware animations.
    524      */
    525     private void cancelHardwareAnimations(boolean jumpToEnd) {
    526         final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
    527         final int N = runningAnimations.size();
    528         for (int i = 0; i < N; i++) {
    529             if (jumpToEnd) {
    530                 runningAnimations.get(i).end();
    531             } else {
    532                 runningAnimations.get(i).cancel();
    533             }
    534         }
    535         runningAnimations.clear();
    536 
    537         if (mHasPendingHardwareExit) {
    538             // If we had a pending hardware exit, jump to the end state.
    539             mHasPendingHardwareExit = false;
    540 
    541             if (jumpToEnd) {
    542                 mOpacity = 0;
    543                 mTweenX = 1;
    544                 mTweenY = 1;
    545                 mTweenRadius = 1;
    546             }
    547         }
    548 
    549         mHardwareAnimating = false;
    550     }
    551 
    552     private void removeSelf() {
    553         // The owner will invalidate itself.
    554         if (!mCanceled) {
    555             mOwner.removeRipple(this);
    556         }
    557     }
    558 
    559     private void invalidateSelf() {
    560         mOwner.invalidateSelf();
    561     }
    562 
    563     private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
    564         @Override
    565         public void onAnimationEnd(Animator animation) {
    566             removeSelf();
    567         }
    568     };
    569 
    570     /**
    571     * Interpolator with a smooth log deceleration
    572     */
    573     private static final class LogInterpolator implements TimeInterpolator {
    574         @Override
    575         public float getInterpolation(float input) {
    576             return 1 - (float) Math.pow(400, -input * 1.4);
    577         }
    578     }
    579 }
    580