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.Color;
     26 import android.graphics.Paint;
     27 import android.graphics.Rect;
     28 import android.util.MathUtils;
     29 import android.view.HardwareCanvas;
     30 import android.view.RenderNodeAnimator;
     31 import android.view.animation.LinearInterpolator;
     32 
     33 import java.util.ArrayList;
     34 
     35 /**
     36  * Draws a Material ripple.
     37  */
     38 class RippleBackground {
     39     private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
     40 
     41     private static final float GLOBAL_SPEED = 1.0f;
     42     private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
     43     private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
     44     private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED;
     45     private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f;
     46     private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f;
     47 
     48     private static final int ENTER_DURATION = 667;
     49     private static final int ENTER_DURATION_FAST = 100;
     50 
     51     // Hardware animators.
     52     private final ArrayList<RenderNodeAnimator> mRunningAnimations =
     53             new ArrayList<RenderNodeAnimator>();
     54 
     55     private final RippleDrawable mOwner;
     56 
     57     /** Bounds used for computing max radius. */
     58     private final Rect mBounds;
     59 
     60     /** ARGB color for drawing this ripple. */
     61     private int mColor;
     62 
     63     /** Maximum ripple radius. */
     64     private float mOuterRadius;
     65 
     66     /** Screen density used to adjust pixel-based velocities. */
     67     private float mDensity;
     68 
     69     // Hardware rendering properties.
     70     private CanvasProperty<Paint> mPropOuterPaint;
     71     private CanvasProperty<Float> mPropOuterRadius;
     72     private CanvasProperty<Float> mPropOuterX;
     73     private CanvasProperty<Float> mPropOuterY;
     74 
     75     // Software animators.
     76     private ObjectAnimator mAnimOuterOpacity;
     77 
     78     // Temporary paint used for creating canvas properties.
     79     private Paint mTempPaint;
     80 
     81     // Software rendering properties.
     82     private float mOuterOpacity = 0;
     83     private float mOuterX;
     84     private float mOuterY;
     85 
     86     /** Whether we should be drawing hardware animations. */
     87     private boolean mHardwareAnimating;
     88 
     89     /** Whether we can use hardware acceleration for the exit animation. */
     90     private boolean mCanUseHardware;
     91 
     92     /** Whether we have an explicit maximum radius. */
     93     private boolean mHasMaxRadius;
     94 
     95     private boolean mHasPendingHardwareExit;
     96     private int mPendingOpacityDuration;
     97     private int mPendingInflectionDuration;
     98     private int mPendingInflectionOpacity;
     99 
    100     /**
    101      * Creates a new ripple.
    102      */
    103     public RippleBackground(RippleDrawable owner, Rect bounds) {
    104         mOwner = owner;
    105         mBounds = bounds;
    106     }
    107 
    108     public void setup(int maxRadius, float density) {
    109         if (maxRadius != RippleDrawable.RADIUS_AUTO) {
    110             mHasMaxRadius = true;
    111             mOuterRadius = maxRadius;
    112         } else {
    113             final float halfWidth = mBounds.width() / 2.0f;
    114             final float halfHeight = mBounds.height() / 2.0f;
    115             mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
    116         }
    117 
    118         mOuterX = 0;
    119         mOuterY = 0;
    120         mDensity = density;
    121     }
    122 
    123     public void onHotspotBoundsChanged() {
    124         if (!mHasMaxRadius) {
    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 
    131     @SuppressWarnings("unused")
    132     public void setOuterOpacity(float a) {
    133         mOuterOpacity = a;
    134         invalidateSelf();
    135     }
    136 
    137     @SuppressWarnings("unused")
    138     public float getOuterOpacity() {
    139         return mOuterOpacity;
    140     }
    141 
    142     /**
    143      * Draws the ripple centered at (0,0) using the specified paint.
    144      */
    145     public boolean draw(Canvas c, Paint p) {
    146         mColor = p.getColor();
    147 
    148         final boolean canUseHardware = c.isHardwareAccelerated();
    149         if (mCanUseHardware != canUseHardware && mCanUseHardware) {
    150             // We've switched from hardware to non-hardware mode. Panic.
    151             cancelHardwareAnimations(true);
    152         }
    153         mCanUseHardware = canUseHardware;
    154 
    155         final boolean hasContent;
    156         if (canUseHardware && (mHardwareAnimating || mHasPendingHardwareExit)) {
    157             hasContent = drawHardware((HardwareCanvas) c, p);
    158         } else {
    159             hasContent = drawSoftware(c, p);
    160         }
    161 
    162         return hasContent;
    163     }
    164 
    165     public boolean shouldDraw() {
    166         return (mCanUseHardware && mHardwareAnimating) || (mOuterOpacity > 0 && mOuterRadius > 0);
    167     }
    168 
    169     private boolean drawHardware(HardwareCanvas c, Paint p) {
    170         if (mHasPendingHardwareExit) {
    171             cancelHardwareAnimations(false);
    172             startPendingHardwareExit(c, p);
    173         }
    174 
    175         c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
    176 
    177         return true;
    178     }
    179 
    180     private boolean drawSoftware(Canvas c, Paint p) {
    181         boolean hasContent = false;
    182 
    183         final int paintAlpha = p.getAlpha();
    184         final int alpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
    185         final float radius = mOuterRadius;
    186         if (alpha > 0 && radius > 0) {
    187             p.setAlpha(alpha);
    188             c.drawCircle(mOuterX, mOuterY, radius, p);
    189             p.setAlpha(paintAlpha);
    190             hasContent = true;
    191         }
    192 
    193         return hasContent;
    194     }
    195 
    196     /**
    197      * Returns the maximum bounds of the ripple relative to the ripple center.
    198      */
    199     public void getBounds(Rect bounds) {
    200         final int outerX = (int) mOuterX;
    201         final int outerY = (int) mOuterY;
    202         final int r = (int) mOuterRadius + 1;
    203         bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
    204     }
    205 
    206     /**
    207      * Starts the enter animation.
    208      */
    209     public void enter(boolean fast) {
    210         cancel();
    211 
    212         final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
    213         opacity.setAutoCancel(true);
    214         opacity.setDuration(fast ? ENTER_DURATION_FAST : ENTER_DURATION);
    215         opacity.setInterpolator(LINEAR_INTERPOLATOR);
    216 
    217         mAnimOuterOpacity = opacity;
    218 
    219         // Enter animations always run on the UI thread, since it's unlikely
    220         // that anything interesting is happening until the user lifts their
    221         // finger.
    222         opacity.start();
    223     }
    224 
    225     /**
    226      * Starts the exit animation.
    227      */
    228     public void exit() {
    229         cancel();
    230 
    231         // Scale the outer max opacity and opacity velocity based
    232         // on the size of the outer radius.
    233         final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
    234         final float outerSizeInfluence = MathUtils.constrain(
    235                 (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
    236                 / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
    237         final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN,
    238                 WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence);
    239 
    240         // Determine at what time the inner and outer opacity intersect.
    241         // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
    242         // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
    243         final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
    244                 / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
    245         final int inflectionOpacity = (int) (Color.alpha(mColor) * (mOuterOpacity
    246                 + inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
    247 
    248         if (mCanUseHardware) {
    249             createPendingHardwareExit(opacityDuration, inflectionDuration, inflectionOpacity);
    250         } else {
    251             exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity);
    252         }
    253     }
    254 
    255     private void createPendingHardwareExit(
    256             int opacityDuration, int inflectionDuration, int inflectionOpacity) {
    257         mHasPendingHardwareExit = true;
    258         mPendingOpacityDuration = opacityDuration;
    259         mPendingInflectionDuration = inflectionDuration;
    260         mPendingInflectionOpacity = inflectionOpacity;
    261 
    262         // The animation will start on the next draw().
    263         invalidateSelf();
    264     }
    265 
    266     private void startPendingHardwareExit(HardwareCanvas c, Paint p) {
    267         mHasPendingHardwareExit = false;
    268 
    269         final int opacityDuration = mPendingOpacityDuration;
    270         final int inflectionDuration = mPendingInflectionDuration;
    271         final int inflectionOpacity = mPendingInflectionOpacity;
    272 
    273         final Paint outerPaint = getTempPaint(p);
    274         outerPaint.setAlpha((int) (outerPaint.getAlpha() * mOuterOpacity + 0.5f));
    275         mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
    276         mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
    277         mPropOuterX = CanvasProperty.createFloat(mOuterX);
    278         mPropOuterY = CanvasProperty.createFloat(mOuterY);
    279 
    280         final RenderNodeAnimator outerOpacityAnim;
    281         if (inflectionDuration > 0) {
    282             // Outer opacity continues to increase for a bit.
    283             outerOpacityAnim = new RenderNodeAnimator(mPropOuterPaint,
    284                     RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
    285             outerOpacityAnim.setDuration(inflectionDuration);
    286             outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
    287 
    288             // Chain the outer opacity exit animation.
    289             final int outerDuration = opacityDuration - inflectionDuration;
    290             if (outerDuration > 0) {
    291                 final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
    292                         mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
    293                 outerFadeOutAnim.setDuration(outerDuration);
    294                 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
    295                 outerFadeOutAnim.setStartDelay(inflectionDuration);
    296                 outerFadeOutAnim.setStartValue(inflectionOpacity);
    297                 outerFadeOutAnim.addListener(mAnimationListener);
    298                 outerFadeOutAnim.setTarget(c);
    299                 outerFadeOutAnim.start();
    300 
    301                 mRunningAnimations.add(outerFadeOutAnim);
    302             } else {
    303                 outerOpacityAnim.addListener(mAnimationListener);
    304             }
    305         } else {
    306             outerOpacityAnim = new RenderNodeAnimator(
    307                     mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
    308             outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
    309             outerOpacityAnim.setDuration(opacityDuration);
    310             outerOpacityAnim.addListener(mAnimationListener);
    311         }
    312 
    313         outerOpacityAnim.setTarget(c);
    314         outerOpacityAnim.start();
    315 
    316         mRunningAnimations.add(outerOpacityAnim);
    317 
    318         mHardwareAnimating = true;
    319 
    320         // Set up the software values to match the hardware end values.
    321         mOuterOpacity = 0;
    322     }
    323 
    324     /**
    325      * Jump all animations to their end state. The caller is responsible for
    326      * removing the ripple from the list of animating ripples.
    327      */
    328     public void jump() {
    329         endSoftwareAnimations();
    330         cancelHardwareAnimations(true);
    331     }
    332 
    333     private void endSoftwareAnimations() {
    334         if (mAnimOuterOpacity != null) {
    335             mAnimOuterOpacity.end();
    336             mAnimOuterOpacity = null;
    337         }
    338     }
    339 
    340     private Paint getTempPaint(Paint original) {
    341         if (mTempPaint == null) {
    342             mTempPaint = new Paint();
    343         }
    344         mTempPaint.set(original);
    345         return mTempPaint;
    346     }
    347 
    348     private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) {
    349         final ObjectAnimator outerOpacityAnim;
    350         if (inflectionDuration > 0) {
    351             // Outer opacity continues to increase for a bit.
    352             outerOpacityAnim = ObjectAnimator.ofFloat(this,
    353                     "outerOpacity", inflectionOpacity / 255.0f);
    354             outerOpacityAnim.setAutoCancel(true);
    355             outerOpacityAnim.setDuration(inflectionDuration);
    356             outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
    357 
    358             // Chain the outer opacity exit animation.
    359             final int outerDuration = opacityDuration - inflectionDuration;
    360             if (outerDuration > 0) {
    361                 outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
    362                     @Override
    363                     public void onAnimationEnd(Animator animation) {
    364                         final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
    365                                 RippleBackground.this, "outerOpacity", 0);
    366                         outerFadeOutAnim.setAutoCancel(true);
    367                         outerFadeOutAnim.setDuration(outerDuration);
    368                         outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
    369                         outerFadeOutAnim.addListener(mAnimationListener);
    370 
    371                         mAnimOuterOpacity = outerFadeOutAnim;
    372 
    373                         outerFadeOutAnim.start();
    374                     }
    375 
    376                     @Override
    377                     public void onAnimationCancel(Animator animation) {
    378                         animation.removeListener(this);
    379                     }
    380                 });
    381             } else {
    382                 outerOpacityAnim.addListener(mAnimationListener);
    383             }
    384         } else {
    385             outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
    386             outerOpacityAnim.setAutoCancel(true);
    387             outerOpacityAnim.setDuration(opacityDuration);
    388             outerOpacityAnim.addListener(mAnimationListener);
    389         }
    390 
    391         mAnimOuterOpacity = outerOpacityAnim;
    392 
    393         outerOpacityAnim.start();
    394     }
    395 
    396     /**
    397      * Cancel all animations. The caller is responsible for removing
    398      * the ripple from the list of animating ripples.
    399      */
    400     public void cancel() {
    401         cancelSoftwareAnimations();
    402         cancelHardwareAnimations(false);
    403     }
    404 
    405     private void cancelSoftwareAnimations() {
    406         if (mAnimOuterOpacity != null) {
    407             mAnimOuterOpacity.cancel();
    408             mAnimOuterOpacity = null;
    409         }
    410     }
    411 
    412     /**
    413      * Cancels any running hardware animations.
    414      */
    415     private void cancelHardwareAnimations(boolean jumpToEnd) {
    416         final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
    417         final int N = runningAnimations.size();
    418         for (int i = 0; i < N; i++) {
    419             if (jumpToEnd) {
    420                 runningAnimations.get(i).end();
    421             } else {
    422                 runningAnimations.get(i).cancel();
    423             }
    424         }
    425         runningAnimations.clear();
    426 
    427         if (mHasPendingHardwareExit) {
    428             // If we had a pending hardware exit, jump to the end state.
    429             mHasPendingHardwareExit = false;
    430 
    431             if (jumpToEnd) {
    432                 mOuterOpacity = 0;
    433             }
    434         }
    435 
    436         mHardwareAnimating = false;
    437     }
    438 
    439     private void invalidateSelf() {
    440         mOwner.invalidateSelf();
    441     }
    442 
    443     private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
    444         @Override
    445         public void onAnimationEnd(Animator animation) {
    446             mHardwareAnimating = false;
    447         }
    448     };
    449 }
    450