Home | History | Annotate | Download | only in drawable
      1 /*
      2  * Copyright (C) 2015 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.AnimatorSet;
     22 import android.animation.ObjectAnimator;
     23 import android.animation.TimeInterpolator;
     24 import android.graphics.Canvas;
     25 import android.graphics.CanvasProperty;
     26 import android.graphics.Paint;
     27 import android.graphics.Rect;
     28 import android.util.FloatProperty;
     29 import android.util.MathUtils;
     30 import android.view.DisplayListCanvas;
     31 import android.view.RenderNodeAnimator;
     32 import android.view.animation.LinearInterpolator;
     33 
     34 /**
     35  * Draws a ripple foreground.
     36  */
     37 class RippleForeground extends RippleComponent {
     38     private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
     39     private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator(
     40             400f, 1.4f, 0);
     41 
     42     // Pixel-based accelerations and velocities.
     43     private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024;
     44     private static final float WAVE_TOUCH_UP_ACCELERATION = 3400;
     45     private static final float WAVE_OPACITY_DECAY_VELOCITY = 3;
     46 
     47     // Bounded ripple animation properties.
     48     private static final int BOUNDED_ORIGIN_EXIT_DURATION = 300;
     49     private static final int BOUNDED_RADIUS_EXIT_DURATION = 800;
     50     private static final int BOUNDED_OPACITY_EXIT_DURATION = 400;
     51     private static final float MAX_BOUNDED_RADIUS = 350;
     52 
     53     private static final int RIPPLE_ENTER_DELAY = 80;
     54     private static final int OPACITY_ENTER_DURATION_FAST = 120;
     55 
     56     // Parent-relative values for starting position.
     57     private float mStartingX;
     58     private float mStartingY;
     59     private float mClampedStartingX;
     60     private float mClampedStartingY;
     61 
     62     // Hardware rendering properties.
     63     private CanvasProperty<Paint> mPropPaint;
     64     private CanvasProperty<Float> mPropRadius;
     65     private CanvasProperty<Float> mPropX;
     66     private CanvasProperty<Float> mPropY;
     67 
     68     // Target values for tween animations.
     69     private float mTargetX = 0;
     70     private float mTargetY = 0;
     71 
     72     /** Ripple target radius used when bounded. Not used for clamping. */
     73     private float mBoundedRadius = 0;
     74 
     75     // Software rendering properties.
     76     private float mOpacity = 1;
     77 
     78     // Values used to tween between the start and end positions.
     79     private float mTweenRadius = 0;
     80     private float mTweenX = 0;
     81     private float mTweenY = 0;
     82 
     83     /** Whether this ripple is bounded. */
     84     private boolean mIsBounded;
     85 
     86     /** Whether this ripple has finished its exit animation. */
     87     private boolean mHasFinishedExit;
     88 
     89     public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY,
     90             boolean isBounded, boolean forceSoftware) {
     91         super(owner, bounds, forceSoftware);
     92 
     93         mIsBounded = isBounded;
     94         mStartingX = startingX;
     95         mStartingY = startingY;
     96 
     97         if (isBounded) {
     98             mBoundedRadius = MAX_BOUNDED_RADIUS * 0.9f
     99                     + (float) (MAX_BOUNDED_RADIUS * Math.random() * 0.1);
    100         } else {
    101             mBoundedRadius = 0;
    102         }
    103     }
    104 
    105     @Override
    106     protected void onTargetRadiusChanged(float targetRadius) {
    107         clampStartingPosition();
    108     }
    109 
    110     @Override
    111     protected boolean drawSoftware(Canvas c, Paint p) {
    112         boolean hasContent = false;
    113 
    114         final int origAlpha = p.getAlpha();
    115         final int alpha = (int) (origAlpha * mOpacity + 0.5f);
    116         final float radius = getCurrentRadius();
    117         if (alpha > 0 && radius > 0) {
    118             final float x = getCurrentX();
    119             final float y = getCurrentY();
    120             p.setAlpha(alpha);
    121             c.drawCircle(x, y, radius, p);
    122             p.setAlpha(origAlpha);
    123             hasContent = true;
    124         }
    125 
    126         return hasContent;
    127     }
    128 
    129     @Override
    130     protected boolean drawHardware(DisplayListCanvas c) {
    131         c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
    132         return true;
    133     }
    134 
    135     /**
    136      * Returns the maximum bounds of the ripple relative to the ripple center.
    137      */
    138     public void getBounds(Rect bounds) {
    139         final int outerX = (int) mTargetX;
    140         final int outerY = (int) mTargetY;
    141         final int r = (int) mTargetRadius + 1;
    142         bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
    143     }
    144 
    145     /**
    146      * Specifies the starting position relative to the drawable bounds. No-op if
    147      * the ripple has already entered.
    148      */
    149     public void move(float x, float y) {
    150         mStartingX = x;
    151         mStartingY = y;
    152 
    153         clampStartingPosition();
    154     }
    155 
    156     /**
    157      * @return {@code true} if this ripple has finished its exit animation
    158      */
    159     public boolean hasFinishedExit() {
    160         return mHasFinishedExit;
    161     }
    162 
    163     @Override
    164     protected Animator createSoftwareEnter(boolean fast) {
    165         // Bounded ripples don't have enter animations.
    166         if (mIsBounded) {
    167             return null;
    168         }
    169 
    170         final int duration = (int)
    171                 (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensityScale) + 0.5);
    172 
    173         final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
    174         tweenRadius.setAutoCancel(true);
    175         tweenRadius.setDuration(duration);
    176         tweenRadius.setInterpolator(LINEAR_INTERPOLATOR);
    177         tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY);
    178 
    179         final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
    180         tweenOrigin.setAutoCancel(true);
    181         tweenOrigin.setDuration(duration);
    182         tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR);
    183         tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY);
    184 
    185         final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
    186         opacity.setAutoCancel(true);
    187         opacity.setDuration(OPACITY_ENTER_DURATION_FAST);
    188         opacity.setInterpolator(LINEAR_INTERPOLATOR);
    189 
    190         final AnimatorSet set = new AnimatorSet();
    191         set.play(tweenOrigin).with(tweenRadius).with(opacity);
    192 
    193         return set;
    194     }
    195 
    196     private float getCurrentX() {
    197         return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX);
    198     }
    199 
    200     private float getCurrentY() {
    201         return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY);
    202     }
    203 
    204     private int getRadiusExitDuration() {
    205         final float remainingRadius = mTargetRadius - getCurrentRadius();
    206         return (int) (1000 * Math.sqrt(remainingRadius / (WAVE_TOUCH_UP_ACCELERATION
    207                 + WAVE_TOUCH_DOWN_ACCELERATION) * mDensityScale) + 0.5);
    208     }
    209 
    210     private float getCurrentRadius() {
    211         return MathUtils.lerp(0, mTargetRadius, mTweenRadius);
    212     }
    213 
    214     private int getOpacityExitDuration() {
    215         return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
    216     }
    217 
    218     /**
    219      * Compute target values that are dependent on bounding.
    220      */
    221     private void computeBoundedTargetValues() {
    222         mTargetX = (mClampedStartingX - mBounds.exactCenterX()) * .7f;
    223         mTargetY = (mClampedStartingY - mBounds.exactCenterY()) * .7f;
    224         mTargetRadius = mBoundedRadius;
    225     }
    226 
    227     @Override
    228     protected Animator createSoftwareExit() {
    229         final int radiusDuration;
    230         final int originDuration;
    231         final int opacityDuration;
    232         if (mIsBounded) {
    233             computeBoundedTargetValues();
    234 
    235             radiusDuration = BOUNDED_RADIUS_EXIT_DURATION;
    236             originDuration = BOUNDED_ORIGIN_EXIT_DURATION;
    237             opacityDuration = BOUNDED_OPACITY_EXIT_DURATION;
    238         } else {
    239             radiusDuration = getRadiusExitDuration();
    240             originDuration = radiusDuration;
    241             opacityDuration = getOpacityExitDuration();
    242         }
    243 
    244         final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
    245         tweenRadius.setAutoCancel(true);
    246         tweenRadius.setDuration(radiusDuration);
    247         tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
    248 
    249         final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
    250         tweenOrigin.setAutoCancel(true);
    251         tweenOrigin.setDuration(originDuration);
    252         tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR);
    253 
    254         final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0);
    255         opacity.setAutoCancel(true);
    256         opacity.setDuration(opacityDuration);
    257         opacity.setInterpolator(LINEAR_INTERPOLATOR);
    258 
    259         final AnimatorSet set = new AnimatorSet();
    260         set.play(tweenOrigin).with(tweenRadius).with(opacity);
    261         set.addListener(mAnimationListener);
    262 
    263         return set;
    264     }
    265 
    266     @Override
    267     protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
    268         final int radiusDuration;
    269         final int originDuration;
    270         final int opacityDuration;
    271         if (mIsBounded) {
    272             computeBoundedTargetValues();
    273 
    274             radiusDuration = BOUNDED_RADIUS_EXIT_DURATION;
    275             originDuration = BOUNDED_ORIGIN_EXIT_DURATION;
    276             opacityDuration = BOUNDED_OPACITY_EXIT_DURATION;
    277         } else {
    278             radiusDuration = getRadiusExitDuration();
    279             originDuration = radiusDuration;
    280             opacityDuration = getOpacityExitDuration();
    281         }
    282 
    283         final float startX = getCurrentX();
    284         final float startY = getCurrentY();
    285         final float startRadius = getCurrentRadius();
    286 
    287         p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f));
    288 
    289         mPropPaint = CanvasProperty.createPaint(p);
    290         mPropRadius = CanvasProperty.createFloat(startRadius);
    291         mPropX = CanvasProperty.createFloat(startX);
    292         mPropY = CanvasProperty.createFloat(startY);
    293 
    294         final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
    295         radius.setDuration(radiusDuration);
    296         radius.setInterpolator(DECELERATE_INTERPOLATOR);
    297 
    298         final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX);
    299         x.setDuration(originDuration);
    300         x.setInterpolator(DECELERATE_INTERPOLATOR);
    301 
    302         final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
    303         y.setDuration(originDuration);
    304         y.setInterpolator(DECELERATE_INTERPOLATOR);
    305 
    306         final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
    307                 RenderNodeAnimator.PAINT_ALPHA, 0);
    308         opacity.setDuration(opacityDuration);
    309         opacity.setInterpolator(LINEAR_INTERPOLATOR);
    310         opacity.addListener(mAnimationListener);
    311 
    312         final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
    313         set.add(radius);
    314         set.add(opacity);
    315         set.add(x);
    316         set.add(y);
    317 
    318         return set;
    319     }
    320 
    321     @Override
    322     protected void jumpValuesToExit() {
    323         mOpacity = 0;
    324         mTweenX = 1;
    325         mTweenY = 1;
    326         mTweenRadius = 1;
    327     }
    328 
    329     /**
    330      * Clamps the starting position to fit within the ripple bounds.
    331      */
    332     private void clampStartingPosition() {
    333         final float cX = mBounds.exactCenterX();
    334         final float cY = mBounds.exactCenterY();
    335         final float dX = mStartingX - cX;
    336         final float dY = mStartingY - cY;
    337         final float r = mTargetRadius;
    338         if (dX * dX + dY * dY > r * r) {
    339             // Point is outside the circle, clamp to the perimeter.
    340             final double angle = Math.atan2(dY, dX);
    341             mClampedStartingX = cX + (float) (Math.cos(angle) * r);
    342             mClampedStartingY = cY + (float) (Math.sin(angle) * r);
    343         } else {
    344             mClampedStartingX = mStartingX;
    345             mClampedStartingY = mStartingY;
    346         }
    347     }
    348 
    349     private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
    350         @Override
    351         public void onAnimationEnd(Animator animator) {
    352             mHasFinishedExit = true;
    353         }
    354     };
    355 
    356     /**
    357     * Interpolator with a smooth log deceleration.
    358     */
    359     private static final class LogDecelerateInterpolator implements TimeInterpolator {
    360         private final float mBase;
    361         private final float mDrift;
    362         private final float mTimeScale;
    363         private final float mOutputScale;
    364 
    365         public LogDecelerateInterpolator(float base, float timeScale, float drift) {
    366             mBase = base;
    367             mDrift = drift;
    368             mTimeScale = 1f / timeScale;
    369 
    370             mOutputScale = 1f / computeLog(1f);
    371         }
    372 
    373         private float computeLog(float t) {
    374             return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t);
    375         }
    376 
    377         @Override
    378         public float getInterpolation(float t) {
    379             return computeLog(t) * mOutputScale;
    380         }
    381     }
    382 
    383     /**
    384      * Property for animating radius between its initial and target values.
    385      */
    386     private static final FloatProperty<RippleForeground> TWEEN_RADIUS =
    387             new FloatProperty<RippleForeground>("tweenRadius") {
    388         @Override
    389         public void setValue(RippleForeground object, float value) {
    390             object.mTweenRadius = value;
    391             object.invalidateSelf();
    392         }
    393 
    394         @Override
    395         public Float get(RippleForeground object) {
    396             return object.mTweenRadius;
    397         }
    398     };
    399 
    400     /**
    401      * Property for animating origin between its initial and target values.
    402      */
    403     private static final FloatProperty<RippleForeground> TWEEN_ORIGIN =
    404             new FloatProperty<RippleForeground>("tweenOrigin") {
    405                 @Override
    406                 public void setValue(RippleForeground object, float value) {
    407                     object.mTweenX = value;
    408                     object.mTweenY = value;
    409                     object.invalidateSelf();
    410                 }
    411 
    412                 @Override
    413                 public Float get(RippleForeground object) {
    414                     return object.mTweenX;
    415                 }
    416             };
    417 
    418     /**
    419      * Property for animating opacity between 0 and its target value.
    420      */
    421     private static final FloatProperty<RippleForeground> OPACITY =
    422             new FloatProperty<RippleForeground>("opacity") {
    423         @Override
    424         public void setValue(RippleForeground object, float value) {
    425             object.mOpacity = value;
    426             object.invalidateSelf();
    427         }
    428 
    429         @Override
    430         public Float get(RippleForeground object) {
    431             return object.mOpacity;
    432         }
    433     };
    434 }
    435