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.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.FloatProperty;
     28 import android.util.MathUtils;
     29 import android.view.DisplayListCanvas;
     30 import android.view.RenderNodeAnimator;
     31 import android.view.animation.AnimationUtils;
     32 import android.view.animation.LinearInterpolator;
     33 import android.view.animation.PathInterpolator;
     34 
     35 import java.util.ArrayList;
     36 
     37 /**
     38  * Draws a ripple foreground.
     39  */
     40 class RippleForeground extends RippleComponent {
     41     private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
     42     // Matches R.interpolator.fast_out_slow_in but as we have no context we can't just import that
     43     private static final TimeInterpolator DECELERATE_INTERPOLATOR =
     44             new PathInterpolator(0.4f, 0f, 0.2f, 1f);
     45 
     46     // Time it takes for the ripple to expand
     47     private static final int RIPPLE_ENTER_DURATION = 225;
     48     // Time it takes for the ripple to slide from the touch to the center point
     49     private static final int RIPPLE_ORIGIN_DURATION = 225;
     50 
     51     private static final int OPACITY_ENTER_DURATION = 75;
     52     private static final int OPACITY_EXIT_DURATION = 150;
     53     private static final int OPACITY_HOLD_DURATION = OPACITY_ENTER_DURATION + 150;
     54 
     55     // Parent-relative values for starting position.
     56     private float mStartingX;
     57     private float mStartingY;
     58     private float mClampedStartingX;
     59     private float mClampedStartingY;
     60 
     61     // Hardware rendering properties.
     62     private CanvasProperty<Paint> mPropPaint;
     63     private CanvasProperty<Float> mPropRadius;
     64     private CanvasProperty<Float> mPropX;
     65     private CanvasProperty<Float> mPropY;
     66 
     67     // Target values for tween animations.
     68     private float mTargetX = 0;
     69     private float mTargetY = 0;
     70 
     71     // Software rendering properties.
     72     private float mOpacity = 0;
     73 
     74     // Values used to tween between the start and end positions.
     75     private float mTweenRadius = 0;
     76     private float mTweenX = 0;
     77     private float mTweenY = 0;
     78 
     79     /** Whether this ripple has finished its exit animation. */
     80     private boolean mHasFinishedExit;
     81 
     82     /** Whether we can use hardware acceleration for the exit animation. */
     83     private boolean mUsingProperties;
     84 
     85     private long mEnterStartedAtMillis;
     86 
     87     private ArrayList<RenderNodeAnimator> mPendingHwAnimators = new ArrayList<>();
     88     private ArrayList<RenderNodeAnimator> mRunningHwAnimators = new ArrayList<>();
     89 
     90     private ArrayList<Animator> mRunningSwAnimators = new ArrayList<>();
     91 
     92     /**
     93      * If set, force all ripple animations to not run on RenderThread, even if it would be
     94      * available.
     95      */
     96     private final boolean mForceSoftware;
     97 
     98     /**
     99      * If we have a bound, don't start from 0. Start from 60% of the max out of width and height.
    100      */
    101     private float mStartRadius = 0;
    102 
    103     public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY,
    104             boolean forceSoftware) {
    105         super(owner, bounds);
    106 
    107         mForceSoftware = forceSoftware;
    108         mStartingX = startingX;
    109         mStartingY = startingY;
    110 
    111         // Take 60% of the maximum of the width and height, then divided half to get the radius.
    112         mStartRadius = Math.max(bounds.width(), bounds.height()) * 0.3f;
    113         clampStartingPosition();
    114     }
    115 
    116     @Override
    117     protected void onTargetRadiusChanged(float targetRadius) {
    118         clampStartingPosition();
    119         switchToUiThreadAnimation();
    120     }
    121 
    122     private void drawSoftware(Canvas c, Paint p) {
    123         final int origAlpha = p.getAlpha();
    124         final int alpha = (int) (origAlpha * mOpacity + 0.5f);
    125         final float radius = getCurrentRadius();
    126         if (alpha > 0 && radius > 0) {
    127             final float x = getCurrentX();
    128             final float y = getCurrentY();
    129             p.setAlpha(alpha);
    130             c.drawCircle(x, y, radius, p);
    131             p.setAlpha(origAlpha);
    132         }
    133     }
    134 
    135     private void startPending(DisplayListCanvas c) {
    136         if (!mPendingHwAnimators.isEmpty()) {
    137             for (int i = 0; i < mPendingHwAnimators.size(); i++) {
    138                 RenderNodeAnimator animator = mPendingHwAnimators.get(i);
    139                 animator.setTarget(c);
    140                 animator.start();
    141                 mRunningHwAnimators.add(animator);
    142             }
    143             mPendingHwAnimators.clear();
    144         }
    145     }
    146 
    147     private void pruneHwFinished() {
    148         if (!mRunningHwAnimators.isEmpty()) {
    149             for (int i = mRunningHwAnimators.size() - 1; i >= 0; i--) {
    150                 if (!mRunningHwAnimators.get(i).isRunning()) {
    151                     mRunningHwAnimators.remove(i);
    152                 }
    153             }
    154         }
    155     }
    156 
    157     private void pruneSwFinished() {
    158         if (!mRunningSwAnimators.isEmpty()) {
    159             for (int i = mRunningSwAnimators.size() - 1; i >= 0; i--) {
    160                 if (!mRunningSwAnimators.get(i).isRunning()) {
    161                     mRunningSwAnimators.remove(i);
    162                 }
    163             }
    164         }
    165     }
    166 
    167     private void drawHardware(DisplayListCanvas c, Paint p) {
    168         startPending(c);
    169         pruneHwFinished();
    170         if (mPropPaint != null) {
    171             mUsingProperties = true;
    172             c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
    173         } else {
    174             mUsingProperties = false;
    175             drawSoftware(c, p);
    176         }
    177     }
    178 
    179     /**
    180      * Returns the maximum bounds of the ripple relative to the ripple center.
    181      */
    182     public void getBounds(Rect bounds) {
    183         final int outerX = (int) mTargetX;
    184         final int outerY = (int) mTargetY;
    185         final int r = (int) mTargetRadius + 1;
    186         bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
    187     }
    188 
    189     /**
    190      * Specifies the starting position relative to the drawable bounds. No-op if
    191      * the ripple has already entered.
    192      */
    193     public void move(float x, float y) {
    194         mStartingX = x;
    195         mStartingY = y;
    196 
    197         clampStartingPosition();
    198     }
    199 
    200     /**
    201      * @return {@code true} if this ripple has finished its exit animation
    202      */
    203     public boolean hasFinishedExit() {
    204         return mHasFinishedExit;
    205     }
    206 
    207     private long computeFadeOutDelay() {
    208         long timeSinceEnter = AnimationUtils.currentAnimationTimeMillis() - mEnterStartedAtMillis;
    209         if (timeSinceEnter > 0 && timeSinceEnter < OPACITY_HOLD_DURATION) {
    210             return OPACITY_HOLD_DURATION - timeSinceEnter;
    211         }
    212         return 0;
    213     }
    214 
    215     private void startSoftwareEnter() {
    216         for (int i = 0; i < mRunningSwAnimators.size(); i++) {
    217             mRunningSwAnimators.get(i).cancel();
    218         }
    219         mRunningSwAnimators.clear();
    220 
    221         final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
    222         tweenRadius.setDuration(RIPPLE_ENTER_DURATION);
    223         tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
    224         tweenRadius.start();
    225         mRunningSwAnimators.add(tweenRadius);
    226 
    227         final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
    228         tweenOrigin.setDuration(RIPPLE_ORIGIN_DURATION);
    229         tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR);
    230         tweenOrigin.start();
    231         mRunningSwAnimators.add(tweenOrigin);
    232 
    233         final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
    234         opacity.setDuration(OPACITY_ENTER_DURATION);
    235         opacity.setInterpolator(LINEAR_INTERPOLATOR);
    236         opacity.start();
    237         mRunningSwAnimators.add(opacity);
    238     }
    239 
    240     private void startSoftwareExit() {
    241         final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0);
    242         opacity.setDuration(OPACITY_EXIT_DURATION);
    243         opacity.setInterpolator(LINEAR_INTERPOLATOR);
    244         opacity.addListener(mAnimationListener);
    245         opacity.setStartDelay(computeFadeOutDelay());
    246         opacity.start();
    247         mRunningSwAnimators.add(opacity);
    248     }
    249 
    250     private void startHardwareEnter() {
    251         if (mForceSoftware) { return; }
    252         mPropX = CanvasProperty.createFloat(getCurrentX());
    253         mPropY = CanvasProperty.createFloat(getCurrentY());
    254         mPropRadius = CanvasProperty.createFloat(getCurrentRadius());
    255         final Paint paint = mOwner.getRipplePaint();
    256         mPropPaint = CanvasProperty.createPaint(paint);
    257 
    258         final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
    259         radius.setDuration(RIPPLE_ORIGIN_DURATION);
    260         radius.setInterpolator(DECELERATE_INTERPOLATOR);
    261         mPendingHwAnimators.add(radius);
    262 
    263         final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX);
    264         x.setDuration(RIPPLE_ORIGIN_DURATION);
    265         x.setInterpolator(DECELERATE_INTERPOLATOR);
    266         mPendingHwAnimators.add(x);
    267 
    268         final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
    269         y.setDuration(RIPPLE_ORIGIN_DURATION);
    270         y.setInterpolator(DECELERATE_INTERPOLATOR);
    271         mPendingHwAnimators.add(y);
    272 
    273         final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
    274                 RenderNodeAnimator.PAINT_ALPHA, paint.getAlpha());
    275         opacity.setDuration(OPACITY_ENTER_DURATION);
    276         opacity.setInterpolator(LINEAR_INTERPOLATOR);
    277         opacity.setStartValue(0);
    278         mPendingHwAnimators.add(opacity);
    279 
    280         invalidateSelf();
    281     }
    282 
    283     private void startHardwareExit() {
    284         // Only run a hardware exit if we had a hardware enter to continue from
    285         if (mForceSoftware || mPropPaint == null) return;
    286 
    287         final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
    288                 RenderNodeAnimator.PAINT_ALPHA, 0);
    289         opacity.setDuration(OPACITY_EXIT_DURATION);
    290         opacity.setInterpolator(LINEAR_INTERPOLATOR);
    291         opacity.addListener(mAnimationListener);
    292         opacity.setStartDelay(computeFadeOutDelay());
    293         opacity.setStartValue(mOwner.getRipplePaint().getAlpha());
    294         mPendingHwAnimators.add(opacity);
    295         invalidateSelf();
    296     }
    297 
    298     /**
    299      * Starts a ripple enter animation.
    300      */
    301     public final void enter() {
    302         mEnterStartedAtMillis = AnimationUtils.currentAnimationTimeMillis();
    303         startSoftwareEnter();
    304         startHardwareEnter();
    305     }
    306 
    307     /**
    308      * Starts a ripple exit animation.
    309      */
    310     public final void exit() {
    311         startSoftwareExit();
    312         startHardwareExit();
    313     }
    314 
    315     private float getCurrentX() {
    316         return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX);
    317     }
    318 
    319     private float getCurrentY() {
    320         return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY);
    321     }
    322 
    323     private float getCurrentRadius() {
    324         return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius);
    325     }
    326 
    327     /**
    328      * Draws the ripple to the canvas, inheriting the paint's color and alpha
    329      * properties.
    330      *
    331      * @param c the canvas to which the ripple should be drawn
    332      * @param p the paint used to draw the ripple
    333      */
    334     public void draw(Canvas c, Paint p) {
    335         final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof DisplayListCanvas;
    336 
    337         pruneSwFinished();
    338         if (hasDisplayListCanvas) {
    339             final DisplayListCanvas hw = (DisplayListCanvas) c;
    340             drawHardware(hw, p);
    341         } else {
    342             drawSoftware(c, p);
    343         }
    344     }
    345 
    346     /**
    347      * Clamps the starting position to fit within the ripple bounds.
    348      */
    349     private void clampStartingPosition() {
    350         final float cX = mBounds.exactCenterX();
    351         final float cY = mBounds.exactCenterY();
    352         final float dX = mStartingX - cX;
    353         final float dY = mStartingY - cY;
    354         final float r = mTargetRadius - mStartRadius;
    355         if (dX * dX + dY * dY > r * r) {
    356             // Point is outside the circle, clamp to the perimeter.
    357             final double angle = Math.atan2(dY, dX);
    358             mClampedStartingX = cX + (float) (Math.cos(angle) * r);
    359             mClampedStartingY = cY + (float) (Math.sin(angle) * r);
    360         } else {
    361             mClampedStartingX = mStartingX;
    362             mClampedStartingY = mStartingY;
    363         }
    364     }
    365 
    366     /**
    367      * Ends all animations, jumping values to the end state.
    368      */
    369     public void end() {
    370         for (int i = 0; i < mRunningSwAnimators.size(); i++) {
    371             mRunningSwAnimators.get(i).end();
    372         }
    373         mRunningSwAnimators.clear();
    374         for (int i = 0; i < mRunningHwAnimators.size(); i++) {
    375             mRunningHwAnimators.get(i).end();
    376         }
    377         mRunningHwAnimators.clear();
    378     }
    379 
    380     private void onAnimationPropertyChanged() {
    381         if (!mUsingProperties) {
    382             invalidateSelf();
    383         }
    384     }
    385 
    386     private void clearHwProps() {
    387         mPropPaint = null;
    388         mPropRadius = null;
    389         mPropX = null;
    390         mPropY = null;
    391         mUsingProperties = false;
    392     }
    393 
    394     private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
    395         @Override
    396         public void onAnimationEnd(Animator animator) {
    397             mHasFinishedExit = true;
    398             pruneHwFinished();
    399             pruneSwFinished();
    400 
    401             if (mRunningHwAnimators.isEmpty()) {
    402                 clearHwProps();
    403             }
    404         }
    405     };
    406 
    407     private void switchToUiThreadAnimation() {
    408         for (int i = 0; i < mRunningHwAnimators.size(); i++) {
    409             Animator animator = mRunningHwAnimators.get(i);
    410             animator.removeListener(mAnimationListener);
    411             animator.end();
    412         }
    413         mRunningHwAnimators.clear();
    414         clearHwProps();
    415         invalidateSelf();
    416     }
    417 
    418     /**
    419      * Property for animating radius between its initial and target values.
    420      */
    421     private static final FloatProperty<RippleForeground> TWEEN_RADIUS =
    422             new FloatProperty<RippleForeground>("tweenRadius") {
    423         @Override
    424         public void setValue(RippleForeground object, float value) {
    425             object.mTweenRadius = value;
    426             object.onAnimationPropertyChanged();
    427         }
    428 
    429         @Override
    430         public Float get(RippleForeground object) {
    431             return object.mTweenRadius;
    432         }
    433     };
    434 
    435     /**
    436      * Property for animating origin between its initial and target values.
    437      */
    438     private static final FloatProperty<RippleForeground> TWEEN_ORIGIN =
    439             new FloatProperty<RippleForeground>("tweenOrigin") {
    440         @Override
    441         public void setValue(RippleForeground object, float value) {
    442             object.mTweenX = value;
    443             object.mTweenY = value;
    444             object.onAnimationPropertyChanged();
    445         }
    446 
    447         @Override
    448         public Float get(RippleForeground object) {
    449             return object.mTweenX;
    450         }
    451     };
    452 
    453     /**
    454      * Property for animating opacity between 0 and its target value.
    455      */
    456     private static final FloatProperty<RippleForeground> OPACITY =
    457             new FloatProperty<RippleForeground>("opacity") {
    458         @Override
    459         public void setValue(RippleForeground object, float value) {
    460             object.mOpacity = value;
    461             object.onAnimationPropertyChanged();
    462         }
    463 
    464         @Override
    465         public Float get(RippleForeground object) {
    466             return object.mOpacity;
    467         }
    468     };
    469 }
    470