Make ripples silky smooth

* Updates press state ripple to match UX spec
* Makes it ungodly silky smooth LIKE BUTTAH
* Update hover & focus states to be closer to UX spec,
  still needs a final pass.

Bug: 63635160
Test: Clicked on a bunch of stuff

Change-Id: I162ab9d8d669002f2ae511f93b5d9fe67f99c533
(cherry picked from commit 0c453ccb87e0b5a4f4b318df01700c9a9a0da545)
diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java
index 9515040..c4a7160 100644
--- a/core/java/android/view/RenderNodeAnimator.java
+++ b/core/java/android/view/RenderNodeAnimator.java
@@ -19,7 +19,6 @@
 import android.animation.Animator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
-import android.graphics.Canvas;
 import android.graphics.CanvasProperty;
 import android.graphics.Paint;
 import android.util.SparseIntArray;
@@ -281,12 +280,9 @@
         setTarget(mViewTarget.mRenderNode);
     }
 
-    public void setTarget(Canvas canvas) {
-        if (!(canvas instanceof DisplayListCanvas)) {
-            throw new IllegalArgumentException("Not a GLES20RecordingCanvas");
-        }
-        final DisplayListCanvas recordingCanvas = (DisplayListCanvas) canvas;
-        setTarget(recordingCanvas.mNode);
+    /** Sets the animation target to the owning view of the DisplayListCanvas */
+    public void setTarget(DisplayListCanvas canvas) {
+        setTarget(canvas.mNode);
     }
 
     private void setTarget(RenderNode node) {
diff --git a/graphics/java/android/graphics/drawable/RippleBackground.java b/graphics/java/android/graphics/drawable/RippleBackground.java
index 3bf4f90..dea194e 100644
--- a/graphics/java/android/graphics/drawable/RippleBackground.java
+++ b/graphics/java/android/graphics/drawable/RippleBackground.java
@@ -36,138 +36,69 @@
 
     private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
 
-    private static final int OPACITY_ENTER_DURATION = 600;
-    private static final int OPACITY_ENTER_DURATION_FAST = 120;
-    private static final int OPACITY_EXIT_DURATION = 480;
+    private static final int OPACITY_DURATION = 80;
 
-    // Hardware rendering properties.
-    private CanvasProperty<Paint> mPropPaint;
-    private CanvasProperty<Float> mPropRadius;
-    private CanvasProperty<Float> mPropX;
-    private CanvasProperty<Float> mPropY;
+    private ObjectAnimator mAnimator;
 
-    // Software rendering properties.
     private float mOpacity = 0;
 
     /** Whether this ripple is bounded. */
     private boolean mIsBounded;
 
-    public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded,
-            boolean forceSoftware) {
-        super(owner, bounds, forceSoftware);
+    private boolean mFocused = false;
+    private boolean mHovered = false;
+
+    public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded) {
+        super(owner, bounds);
 
         mIsBounded = isBounded;
     }
 
     public boolean isVisible() {
-        return mOpacity > 0 || isHardwareAnimating();
+        return mOpacity > 0;
     }
 
-    @Override
-    protected boolean drawSoftware(Canvas c, Paint p) {
-        boolean hasContent = false;
-
+    public void draw(Canvas c, Paint p) {
         final int origAlpha = p.getAlpha();
-        final int alpha = (int) (origAlpha * mOpacity + 0.5f);
+        final int alpha = Math.min((int) (origAlpha * mOpacity + 0.5f), 255);
         if (alpha > 0) {
             p.setAlpha(alpha);
             c.drawCircle(0, 0, mTargetRadius, p);
             p.setAlpha(origAlpha);
-            hasContent = true;
         }
-
-        return hasContent;
     }
 
-    @Override
-    protected boolean drawHardware(DisplayListCanvas c) {
-        c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
-        return true;
-    }
-
-    @Override
-    protected Animator createSoftwareEnter(boolean fast) {
-        // Linear enter based on current opacity.
-        final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION;
-        final int duration = (int) ((1 - mOpacity) * maxDuration);
-
-        final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
-        opacity.setAutoCancel(true);
-        opacity.setDuration(duration);
-        opacity.setInterpolator(LINEAR_INTERPOLATOR);
-
-        return opacity;
-    }
-
-    @Override
-    protected Animator createSoftwareExit() {
-        final AnimatorSet set = new AnimatorSet();
-
-        // Linear exit after enter is completed.
-        final ObjectAnimator exit = ObjectAnimator.ofFloat(this, OPACITY, 0);
-        exit.setInterpolator(LINEAR_INTERPOLATOR);
-        exit.setDuration(OPACITY_EXIT_DURATION);
-        exit.setAutoCancel(true);
-
-        final AnimatorSet.Builder builder = set.play(exit);
-
-        // Linear "fast" enter based on current opacity.
-        final int fastEnterDuration = mIsBounded ?
-                (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0;
-        if (fastEnterDuration > 0) {
-            final ObjectAnimator enter = ObjectAnimator.ofFloat(this, OPACITY, 1);
-            enter.setInterpolator(LINEAR_INTERPOLATOR);
-            enter.setDuration(fastEnterDuration);
-            enter.setAutoCancel(true);
-
-            builder.after(enter);
+    public void setState(boolean focused, boolean hovered, boolean animateChanged) {
+        if (mHovered != hovered || mFocused != focused) {
+            mHovered = hovered;
+            mFocused = focused;
+            onStateChanged(animateChanged);
         }
-
-        return set;
     }
 
-    @Override
-    protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
-        final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
-
-        final int targetAlpha = p.getAlpha();
-        final int currentAlpha = (int) (mOpacity * targetAlpha + 0.5f);
-        p.setAlpha(currentAlpha);
-
-        mPropPaint = CanvasProperty.createPaint(p);
-        mPropRadius = CanvasProperty.createFloat(mTargetRadius);
-        mPropX = CanvasProperty.createFloat(0);
-        mPropY = CanvasProperty.createFloat(0);
-
-        final int fastEnterDuration = mIsBounded ?
-                (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0;
-
-        // Linear exit after enter is completed.
-        final RenderNodeAnimator exit = new RenderNodeAnimator(
-                mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
-        exit.setInterpolator(LINEAR_INTERPOLATOR);
-        exit.setDuration(OPACITY_EXIT_DURATION);
-        if (fastEnterDuration > 0) {
-            exit.setStartDelay(fastEnterDuration);
-            exit.setStartValue(targetAlpha);
+    private void onStateChanged(boolean animateChanged) {
+        float newOpacity = 0.0f;
+        if (mHovered) newOpacity += 1.0f;
+        if (mFocused) newOpacity += 1.0f;
+        if (mAnimator != null) {
+            mAnimator.cancel();
+            mAnimator = null;
         }
-        set.add(exit);
-
-        // Linear "fast" enter based on current opacity.
-        if (fastEnterDuration > 0) {
-            final RenderNodeAnimator enter = new RenderNodeAnimator(
-                    mPropPaint, RenderNodeAnimator.PAINT_ALPHA, targetAlpha);
-            enter.setInterpolator(LINEAR_INTERPOLATOR);
-            enter.setDuration(fastEnterDuration);
-            set.add(enter);
+        if (animateChanged) {
+            mAnimator = ObjectAnimator.ofFloat(this, OPACITY, newOpacity);
+            mAnimator.setDuration(OPACITY_DURATION);
+            mAnimator.setInterpolator(LINEAR_INTERPOLATOR);
+            mAnimator.start();
+        } else {
+            mOpacity = newOpacity;
         }
-
-        return set;
     }
 
-    @Override
-    protected void jumpValuesToExit() {
-        mOpacity = 0;
+    public void jumpToFinal() {
+        if (mAnimator != null) {
+            mAnimator.end();
+            mAnimator = null;
+        }
     }
 
     private static abstract class BackgroundProperty extends FloatProperty<RippleBackground> {
diff --git a/graphics/java/android/graphics/drawable/RippleComponent.java b/graphics/java/android/graphics/drawable/RippleComponent.java
index e83513c..0e38826 100644
--- a/graphics/java/android/graphics/drawable/RippleComponent.java
+++ b/graphics/java/android/graphics/drawable/RippleComponent.java
@@ -27,23 +27,14 @@
 import java.util.ArrayList;
 
 /**
- * Abstract class that handles hardware/software hand-off and lifecycle for
- * animated ripple foreground and background components.
+ * Abstract class that handles size & positioning common to the ripple & focus states.
  */
 abstract class RippleComponent {
-    private final RippleDrawable mOwner;
+    protected final RippleDrawable mOwner;
 
     /** Bounds used for computing max radius. May be modified by the owner. */
     protected final Rect mBounds;
 
-    /** Whether we can use hardware acceleration for the exit animation. */
-    private boolean mHasDisplayListCanvas;
-
-    private boolean mHasPendingHardwareAnimator;
-    private RenderNodeAnimatorSet mHardwareAnimator;
-
-    private Animator mSoftwareAnimator;
-
     /** Whether we have an explicit maximum radius. */
     private boolean mHasMaxRadius;
 
@@ -53,16 +44,9 @@
     /** Screen density used to adjust pixel-based constants. */
     protected float mDensityScale;
 
-    /**
-     * If set, force all ripple animations to not run on RenderThread, even if it would be
-     * available.
-     */
-    private final boolean mForceSoftware;
-
-    public RippleComponent(RippleDrawable owner, Rect bounds, boolean forceSoftware) {
+    public RippleComponent(RippleDrawable owner, Rect bounds) {
         mOwner = owner;
         mBounds = bounds;
-        mForceSoftware = forceSoftware;
     }
 
     public void onBoundsChange() {
@@ -92,89 +76,6 @@
     }
 
     /**
-     * Starts a ripple enter animation.
-     *
-     * @param fast whether the ripple should enter quickly
-     */
-    public final void enter(boolean fast) {
-        cancel();
-
-        mSoftwareAnimator = createSoftwareEnter(fast);
-
-        if (mSoftwareAnimator != null) {
-            mSoftwareAnimator.start();
-        }
-    }
-
-    /**
-     * Starts a ripple exit animation.
-     */
-    public final void exit() {
-        cancel();
-
-        if (mHasDisplayListCanvas) {
-            // We don't have access to a canvas here, but we expect one on the
-            // next frame. We'll start the render thread animation then.
-            mHasPendingHardwareAnimator = true;
-
-            // Request another frame.
-            invalidateSelf();
-        } else {
-            mSoftwareAnimator = createSoftwareExit();
-            mSoftwareAnimator.start();
-        }
-    }
-
-    /**
-     * Cancels all animations. Software animation values are left in the
-     * current state, while hardware animation values jump to the end state.
-     */
-    public void cancel() {
-        cancelSoftwareAnimations();
-        endHardwareAnimations();
-    }
-
-    /**
-     * Ends all animations, jumping values to the end state.
-     */
-    public void end() {
-        endSoftwareAnimations();
-        endHardwareAnimations();
-    }
-
-    /**
-     * Draws the ripple to the canvas, inheriting the paint's color and alpha
-     * properties.
-     *
-     * @param c the canvas to which the ripple should be drawn
-     * @param p the paint used to draw the ripple
-     * @return {@code true} if something was drawn, {@code false} otherwise
-     */
-    public boolean draw(Canvas c, Paint p) {
-        final boolean hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated()
-                && c instanceof DisplayListCanvas;
-        if (mHasDisplayListCanvas != hasDisplayListCanvas) {
-            mHasDisplayListCanvas = hasDisplayListCanvas;
-
-            if (!hasDisplayListCanvas) {
-                // We've switched from hardware to non-hardware mode. Panic.
-                endHardwareAnimations();
-            }
-        }
-
-        if (hasDisplayListCanvas) {
-            final DisplayListCanvas hw = (DisplayListCanvas) c;
-            startPendingAnimation(hw, p);
-
-            if (mHardwareAnimator != null) {
-                return drawHardware(hw);
-            }
-        }
-
-        return drawSoftware(c, p);
-    }
-
-    /**
      * Populates {@code bounds} with the maximum drawing bounds of the ripple
      * relative to its center. The resulting bounds should be translated into
      * parent drawable coordinates before use.
@@ -186,77 +87,10 @@
         bounds.set(-r, -r, r, r);
     }
 
-    /**
-     * Starts the pending hardware animation, if available.
-     *
-     * @param hw hardware canvas on which the animation should draw
-     * @param p paint whose properties the hardware canvas should use
-     */
-    private void startPendingAnimation(DisplayListCanvas hw, Paint p) {
-        if (mHasPendingHardwareAnimator) {
-            mHasPendingHardwareAnimator = false;
-
-            mHardwareAnimator = createHardwareExit(new Paint(p));
-            mHardwareAnimator.start(hw);
-
-            // Preemptively jump the software values to the end state now that
-            // the hardware exit has read whatever values it needs.
-            jumpValuesToExit();
-        }
-    }
-
-    /**
-     * Cancels any current software animations, leaving the values in their
-     * current state.
-     */
-    private void cancelSoftwareAnimations() {
-        if (mSoftwareAnimator != null) {
-            mSoftwareAnimator.cancel();
-            mSoftwareAnimator = null;
-        }
-    }
-
-    /**
-     * Ends any current software animations, jumping the values to their end
-     * state.
-     */
-    private void endSoftwareAnimations() {
-        if (mSoftwareAnimator != null) {
-            mSoftwareAnimator.end();
-            mSoftwareAnimator = null;
-        }
-    }
-
-    /**
-     * Ends any pending or current hardware animations.
-     * <p>
-     * Hardware animations can't synchronize values back to the software
-     * thread, so there is no "cancel" equivalent.
-     */
-    private void endHardwareAnimations() {
-        if (mHardwareAnimator != null) {
-            mHardwareAnimator.end();
-            mHardwareAnimator = null;
-        }
-
-        if (mHasPendingHardwareAnimator) {
-            mHasPendingHardwareAnimator = false;
-
-            // Manually jump values to their exited state. Normally we'd do that
-            // later when starting the hardware exit, but we're aborting early.
-            jumpValuesToExit();
-        }
-    }
-
     protected final void invalidateSelf() {
         mOwner.invalidateSelf(false);
     }
 
-    protected final boolean isHardwareAnimating() {
-        return mHardwareAnimator != null && mHardwareAnimator.isRunning()
-                || mHasPendingHardwareAnimator;
-    }
-
     protected final void onHotspotBoundsChanged() {
         if (!mHasMaxRadius) {
             final float halfWidth = mBounds.width() / 2.0f;
@@ -276,76 +110,4 @@
     protected void onTargetRadiusChanged(float targetRadius) {
         // Stub.
     }
-
-    protected abstract Animator createSoftwareEnter(boolean fast);
-
-    protected abstract Animator createSoftwareExit();
-
-    protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p);
-
-    protected abstract boolean drawHardware(DisplayListCanvas c);
-
-    protected abstract boolean drawSoftware(Canvas c, Paint p);
-
-    /**
-     * Called when the hardware exit is cancelled. Jumps software values to end
-     * state to ensure that software and hardware values are synchronized.
-     */
-    protected abstract void jumpValuesToExit();
-
-    public static class RenderNodeAnimatorSet {
-        private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>();
-
-        public void add(RenderNodeAnimator anim) {
-            mAnimators.add(anim);
-        }
-
-        public void clear() {
-            mAnimators.clear();
-        }
-
-        public void start(DisplayListCanvas target) {
-            if (target == null) {
-                throw new IllegalArgumentException("Hardware canvas must be non-null");
-            }
-
-            final ArrayList<RenderNodeAnimator> animators = mAnimators;
-            final int N = animators.size();
-            for (int i = 0; i < N; i++) {
-                final RenderNodeAnimator anim = animators.get(i);
-                anim.setTarget(target);
-                anim.start();
-            }
-        }
-
-        public void cancel() {
-            final ArrayList<RenderNodeAnimator> animators = mAnimators;
-            final int N = animators.size();
-            for (int i = 0; i < N; i++) {
-                final RenderNodeAnimator anim = animators.get(i);
-                anim.cancel();
-            }
-        }
-
-        public void end() {
-            final ArrayList<RenderNodeAnimator> animators = mAnimators;
-            final int N = animators.size();
-            for (int i = 0; i < N; i++) {
-                final RenderNodeAnimator anim = animators.get(i);
-                anim.end();
-            }
-        }
-
-        public boolean isRunning() {
-            final ArrayList<RenderNodeAnimator> animators = mAnimators;
-            final int N = animators.size();
-            for (int i = 0; i < N; i++) {
-                final RenderNodeAnimator anim = animators.get(i);
-                if (anim.isRunning()) {
-                    return true;
-                }
-            }
-            return false;
-        }
-    }
 }
diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java
index 1727eca..8b185f2 100644
--- a/graphics/java/android/graphics/drawable/RippleDrawable.java
+++ b/graphics/java/android/graphics/drawable/RippleDrawable.java
@@ -16,11 +16,6 @@
 
 package android.graphics.drawable;
 
-import com.android.internal.R;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.pm.ActivityInfo.Config;
@@ -42,6 +37,11 @@
 import android.graphics.Shader;
 import android.util.AttributeSet;
 
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
 import java.io.IOException;
 import java.util.Arrays;
 
@@ -135,9 +135,6 @@
     private PorterDuffColorFilter mMaskColorFilter;
     private boolean mHasValidMask;
 
-    /** Whether we expect to draw a background when visible. */
-    private boolean mBackgroundActive;
-
     /** The current ripple. May be actively animating or pending entry. */
     private RippleForeground mRipple;
 
@@ -217,7 +214,7 @@
         }
 
         if (mBackground != null) {
-            mBackground.end();
+            mBackground.jumpToFinal();
         }
 
         cancelExitingRipples();
@@ -266,9 +263,9 @@
             }
         }
 
-        setRippleActive(focused || (enabled && pressed));
+        setRippleActive(enabled && pressed);
 
-        setBackgroundActive(hovered, hovered);
+        setBackgroundActive(hovered, focused);
         return changed;
     }
 
@@ -283,14 +280,13 @@
         }
     }
 
-    private void setBackgroundActive(boolean active, boolean focused) {
-        if (mBackgroundActive != active) {
-            mBackgroundActive = active;
-            if (active) {
-                tryBackgroundEnter(focused);
-            } else {
-                tryBackgroundExit();
-            }
+    private void setBackgroundActive(boolean hovered, boolean focused) {
+        if (mBackground == null && (hovered || focused)) {
+            mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
+            mBackground.setup(mState.mMaxRadius, mDensity);
+        }
+        if (mBackground != null) {
+            mBackground.setState(focused, hovered, true);
         }
     }
 
@@ -327,10 +323,6 @@
                 tryRippleEnter();
             }
 
-            if (mBackgroundActive) {
-                tryBackgroundEnter(false);
-            }
-
             // Skip animations, just show the correct final states.
             jumpToCurrentState();
         }
@@ -546,26 +538,6 @@
     }
 
     /**
-     * Creates an active hotspot at the specified location.
-     */
-    private void tryBackgroundEnter(boolean focused) {
-        if (mBackground == null) {
-            final boolean isBounded = isBounded();
-            mBackground = new RippleBackground(this, mHotspotBounds, isBounded, mForceSoftware);
-        }
-
-        mBackground.setup(mState.mMaxRadius, mDensity);
-        mBackground.enter(focused);
-    }
-
-    private void tryBackgroundExit() {
-        if (mBackground != null) {
-            // Don't null out the background, we need it to draw!
-            mBackground.exit();
-        }
-    }
-
-    /**
      * Attempts to start an enter animation for the active hotspot. Fails if
      * there are too many animating ripples.
      */
@@ -593,7 +565,7 @@
         }
 
         mRipple.setup(mState.mMaxRadius, mDensity);
-        mRipple.enter(false);
+        mRipple.enter();
     }
 
     /**
@@ -623,9 +595,7 @@
         }
 
         if (mBackground != null) {
-            mBackground.end();
-            mBackground = null;
-            mBackgroundActive = false;
+            mBackground.setState(false, false, false);
         }
 
         cancelExitingRipples();
@@ -858,38 +828,8 @@
         final float y = mHotspotBounds.exactCenterY();
         canvas.translate(x, y);
 
-        updateMaskShaderIfNeeded();
-
-        // Position the shader to account for canvas translation.
-        if (mMaskShader != null) {
-            final Rect bounds = getBounds();
-            mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
-            mMaskShader.setLocalMatrix(mMaskMatrix);
-        }
-
-        // Grab the color for the current state and cut the alpha channel in
-        // half so that the ripple and background together yield full alpha.
-        final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
-        final int halfAlpha = (Color.alpha(color) / 2) << 24;
         final Paint p = getRipplePaint();
 
-        if (mMaskColorFilter != null) {
-            // The ripple timing depends on the paint's alpha value, so we need
-            // to push just the alpha channel into the paint and let the filter
-            // handle the full-alpha color.
-            final int fullAlphaColor = color | (0xFF << 24);
-            mMaskColorFilter.setColor(fullAlphaColor);
-
-            p.setColor(halfAlpha);
-            p.setColorFilter(mMaskColorFilter);
-            p.setShader(mMaskShader);
-        } else {
-            final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
-            p.setColor(halfAlphaColor);
-            p.setColorFilter(null);
-            p.setShader(null);
-        }
-
         if (background != null && background.isVisible()) {
             background.draw(canvas, p);
         }
@@ -912,13 +852,49 @@
         mMask.draw(canvas);
     }
 
-    private Paint getRipplePaint() {
+    Paint getRipplePaint() {
         if (mRipplePaint == null) {
             mRipplePaint = new Paint();
             mRipplePaint.setAntiAlias(true);
             mRipplePaint.setStyle(Paint.Style.FILL);
         }
-        return mRipplePaint;
+
+        final float x = mHotspotBounds.exactCenterX();
+        final float y = mHotspotBounds.exactCenterY();
+
+        updateMaskShaderIfNeeded();
+
+        // Position the shader to account for canvas translation.
+        if (mMaskShader != null) {
+            final Rect bounds = getBounds();
+            mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
+            mMaskShader.setLocalMatrix(mMaskMatrix);
+        }
+
+        // Grab the color for the current state and cut the alpha channel in
+        // half so that the ripple and background together yield full alpha.
+        final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
+        final int halfAlpha = (Color.alpha(color) / 2) << 24;
+        final Paint p = mRipplePaint;
+
+        if (mMaskColorFilter != null) {
+            // The ripple timing depends on the paint's alpha value, so we need
+            // to push just the alpha channel into the paint and let the filter
+            // handle the full-alpha color.
+            final int fullAlphaColor = color | (0xFF << 24);
+            mMaskColorFilter.setColor(fullAlphaColor);
+
+            p.setColor(halfAlpha);
+            p.setColorFilter(mMaskColorFilter);
+            p.setShader(mMaskShader);
+        } else {
+            final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
+            p.setColor(halfAlphaColor);
+            p.setColorFilter(null);
+            p.setShader(null);
+        }
+
+        return p;
     }
 
     @Override
diff --git a/graphics/java/android/graphics/drawable/RippleForeground.java b/graphics/java/android/graphics/drawable/RippleForeground.java
index a675eaf..0b5020c 100644
--- a/graphics/java/android/graphics/drawable/RippleForeground.java
+++ b/graphics/java/android/graphics/drawable/RippleForeground.java
@@ -18,7 +18,6 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.graphics.Canvas;
@@ -29,8 +28,11 @@
 import android.util.MathUtils;
 import android.view.DisplayListCanvas;
 import android.view.RenderNodeAnimator;
+import android.view.animation.AnimationUtils;
 import android.view.animation.LinearInterpolator;
 
+import java.util.ArrayList;
+
 /**
  * Draws a ripple foreground.
  */
@@ -40,7 +42,7 @@
             400f, 1.4f, 0);
 
     // Pixel-based accelerations and velocities.
-    private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024;
+    private static final float WAVE_TOUCH_DOWN_ACCELERATION = 2048;
     private static final float WAVE_OPACITY_DECAY_VELOCITY = 3;
 
     // Bounded ripple animation properties.
@@ -49,8 +51,9 @@
     private static final int BOUNDED_OPACITY_EXIT_DURATION = 400;
     private static final float MAX_BOUNDED_RADIUS = 350;
 
-    private static final int RIPPLE_ENTER_DELAY = 80;
-    private static final int OPACITY_ENTER_DURATION_FAST = 120;
+    private static final int OPACITY_ENTER_DURATION = 75;
+    private static final int OPACITY_EXIT_DURATION = 150;
+    private static final int OPACITY_HOLD_DURATION = OPACITY_ENTER_DURATION + 150;
 
     // Parent-relative values for starting position.
     private float mStartingX;
@@ -72,7 +75,7 @@
     private float mBoundedRadius = 0;
 
     // Software rendering properties.
-    private float mOpacity = 1;
+    private float mOpacity = 0;
 
     // Values used to tween between the start and end positions.
     private float mTweenRadius = 0;
@@ -82,6 +85,22 @@
     /** Whether this ripple has finished its exit animation. */
     private boolean mHasFinishedExit;
 
+    /** Whether we can use hardware acceleration for the exit animation. */
+    private boolean mUsingProperties;
+
+    private long mEnterStartedAtMillis;
+
+    private ArrayList<RenderNodeAnimator> mPendingHwAnimators = new ArrayList<>();
+    private ArrayList<RenderNodeAnimator> mRunningHwAnimators = new ArrayList<>();
+
+    private ArrayList<Animator> mRunningSwAnimators = new ArrayList<>();
+
+    /**
+     * If set, force all ripple animations to not run on RenderThread, even if it would be
+     * available.
+     */
+    private final boolean mForceSoftware;
+
     /**
      * If we have a bound, don't start from 0. Start from 60% of the max out of width and height.
      */
@@ -89,8 +108,9 @@
 
     public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY,
             boolean isBounded, boolean forceSoftware) {
-        super(owner, bounds, forceSoftware);
+        super(owner, bounds);
 
+        mForceSoftware = forceSoftware;
         mStartingX = startingX;
         mStartingY = startingY;
 
@@ -109,10 +129,7 @@
         clampStartingPosition();
     }
 
-    @Override
-    protected boolean drawSoftware(Canvas c, Paint p) {
-        boolean hasContent = false;
-
+    private void drawSoftware(Canvas c, Paint p) {
         final int origAlpha = p.getAlpha();
         final int alpha = (int) (origAlpha * mOpacity + 0.5f);
         final float radius = getCurrentRadius();
@@ -122,16 +139,51 @@
             p.setAlpha(alpha);
             c.drawCircle(x, y, radius, p);
             p.setAlpha(origAlpha);
-            hasContent = true;
         }
-
-        return hasContent;
     }
 
-    @Override
-    protected boolean drawHardware(DisplayListCanvas c) {
-        c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
-        return true;
+    private void startPending(DisplayListCanvas c) {
+        if (!mPendingHwAnimators.isEmpty()) {
+            for (int i = 0; i < mPendingHwAnimators.size(); i++) {
+                RenderNodeAnimator animator = mPendingHwAnimators.get(i);
+                animator.setTarget(c);
+                animator.start();
+                mRunningHwAnimators.add(animator);
+            }
+            mPendingHwAnimators.clear();
+        }
+    }
+
+    private void pruneHwFinished() {
+        if (!mRunningHwAnimators.isEmpty()) {
+            for (int i = mRunningHwAnimators.size() - 1; i >= 0; i--) {
+                if (!mRunningHwAnimators.get(i).isRunning()) {
+                    mRunningHwAnimators.remove(i);
+                }
+            }
+        }
+    }
+
+    private void pruneSwFinished() {
+        if (!mRunningSwAnimators.isEmpty()) {
+            for (int i = mRunningSwAnimators.size() - 1; i >= 0; i--) {
+                if (!mRunningSwAnimators.get(i).isRunning()) {
+                    mRunningSwAnimators.remove(i);
+                }
+            }
+        }
+    }
+
+    private void drawHardware(DisplayListCanvas c, Paint p) {
+        startPending(c);
+        pruneHwFinished();
+        if (mPropPaint != null) {
+            mUsingProperties = true;
+            c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
+        } else {
+            mUsingProperties = false;
+            drawSoftware(c, p);
+        }
     }
 
     /**
@@ -162,31 +214,115 @@
         return mHasFinishedExit;
     }
 
-    @Override
-    protected Animator createSoftwareEnter(boolean fast) {
+    private long computeFadeOutDelay() {
+        long timeSinceEnter = AnimationUtils.currentAnimationTimeMillis() - mEnterStartedAtMillis;
+        if (timeSinceEnter > 0 && timeSinceEnter < OPACITY_HOLD_DURATION) {
+            return OPACITY_HOLD_DURATION - timeSinceEnter;
+        }
+        return 0;
+    }
+
+    private void startSoftwareEnter() {
+        for (int i = 0; i < mRunningSwAnimators.size(); i++) {
+            mRunningSwAnimators.get(i).cancel();
+        }
+        mRunningSwAnimators.clear();
+
         final int duration = getRadiusDuration();
 
         final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
-        tweenRadius.setAutoCancel(true);
         tweenRadius.setDuration(duration);
         tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
-        tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY);
+        tweenRadius.start();
+        mRunningSwAnimators.add(tweenRadius);
 
         final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
-        tweenOrigin.setAutoCancel(true);
         tweenOrigin.setDuration(duration);
         tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR);
-        tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY);
+        tweenOrigin.start();
+        mRunningSwAnimators.add(tweenOrigin);
 
         final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
-        opacity.setAutoCancel(true);
-        opacity.setDuration(OPACITY_ENTER_DURATION_FAST);
+        opacity.setDuration(OPACITY_ENTER_DURATION);
         opacity.setInterpolator(LINEAR_INTERPOLATOR);
+        opacity.start();
+        mRunningSwAnimators.add(opacity);
+    }
 
-        final AnimatorSet set = new AnimatorSet();
-        set.play(tweenOrigin).with(tweenRadius).with(opacity);
+    private void startSoftwareExit() {
+        final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0);
+        opacity.setDuration(OPACITY_EXIT_DURATION);
+        opacity.setInterpolator(LINEAR_INTERPOLATOR);
+        opacity.addListener(mAnimationListener);
+        opacity.setStartDelay(computeFadeOutDelay());
+        opacity.start();
+        mRunningSwAnimators.add(opacity);
+    }
 
-        return set;
+    private void startHardwareEnter() {
+        if (mForceSoftware) { return; }
+        mPropX = CanvasProperty.createFloat(getCurrentX());
+        mPropY = CanvasProperty.createFloat(getCurrentY());
+        mPropRadius = CanvasProperty.createFloat(getCurrentRadius());
+        final Paint paint = mOwner.getRipplePaint();
+        mPropPaint = CanvasProperty.createPaint(paint);
+
+        final int radiusDuration = getRadiusDuration();
+
+        final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
+        radius.setDuration(radiusDuration);
+        radius.setInterpolator(DECELERATE_INTERPOLATOR);
+        mPendingHwAnimators.add(radius);
+
+        final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX);
+        x.setDuration(radiusDuration);
+        x.setInterpolator(DECELERATE_INTERPOLATOR);
+        mPendingHwAnimators.add(x);
+
+        final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
+        y.setDuration(radiusDuration);
+        y.setInterpolator(DECELERATE_INTERPOLATOR);
+        mPendingHwAnimators.add(y);
+
+        final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
+                RenderNodeAnimator.PAINT_ALPHA, paint.getAlpha());
+        opacity.setDuration(OPACITY_ENTER_DURATION);
+        opacity.setInterpolator(LINEAR_INTERPOLATOR);
+        opacity.setStartValue(0);
+        mPendingHwAnimators.add(opacity);
+
+        invalidateSelf();
+    }
+
+    private void startHardwareExit() {
+        // Only run a hardware exit if we had a hardware enter to continue from
+        if (mForceSoftware || mPropPaint == null) return;
+
+        final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
+                RenderNodeAnimator.PAINT_ALPHA, 0);
+        opacity.setDuration(OPACITY_EXIT_DURATION);
+        opacity.setInterpolator(LINEAR_INTERPOLATOR);
+        opacity.addListener(mAnimationListener);
+        opacity.setStartDelay(computeFadeOutDelay());
+        mPendingHwAnimators.add(opacity);
+        invalidateSelf();
+    }
+
+    /**
+     * Starts a ripple enter animation.
+     */
+    public final void enter() {
+        mEnterStartedAtMillis = AnimationUtils.currentAnimationTimeMillis();
+        startSoftwareEnter();
+        startHardwareEnter();
+    }
+
+    /**
+     * Starts a ripple exit animation.
+     */
+    public final void exit() {
+        startSoftwareExit();
+        startHardwareExit();
     }
 
     private float getCurrentX() {
@@ -207,96 +343,23 @@
         return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius);
     }
 
-    private int getOpacityExitDuration() {
-        return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
-    }
+    /**
+     * Draws the ripple to the canvas, inheriting the paint's color and alpha
+     * properties.
+     *
+     * @param c the canvas to which the ripple should be drawn
+     * @param p the paint used to draw the ripple
+     */
+    public void draw(Canvas c, Paint p) {
+        final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof DisplayListCanvas;
 
-    @Override
-    protected Animator createSoftwareExit() {
-        final int radiusDuration;
-        final int originDuration;
-        final int opacityDuration;
-
-        radiusDuration = getRadiusDuration();
-        originDuration = radiusDuration;
-        opacityDuration = getOpacityExitDuration();
-
-        final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
-        tweenRadius.setAutoCancel(true);
-        tweenRadius.setDuration(radiusDuration);
-        tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
-
-        final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
-        tweenOrigin.setAutoCancel(true);
-        tweenOrigin.setDuration(originDuration);
-        tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR);
-
-        final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0);
-        opacity.setAutoCancel(true);
-        opacity.setDuration(opacityDuration);
-        opacity.setInterpolator(LINEAR_INTERPOLATOR);
-
-        final AnimatorSet set = new AnimatorSet();
-        set.play(tweenOrigin).with(tweenRadius).with(opacity);
-        set.addListener(mAnimationListener);
-
-        return set;
-    }
-
-    @Override
-    protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
-        final int radiusDuration;
-        final int originDuration;
-        final int opacityDuration;
-
-        radiusDuration = getRadiusDuration();
-        originDuration = radiusDuration;
-        opacityDuration = getOpacityExitDuration();
-
-        final float startX = getCurrentX();
-        final float startY = getCurrentY();
-        final float startRadius = getCurrentRadius();
-
-        p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f));
-
-        mPropPaint = CanvasProperty.createPaint(p);
-        mPropRadius = CanvasProperty.createFloat(startRadius);
-        mPropX = CanvasProperty.createFloat(startX);
-        mPropY = CanvasProperty.createFloat(startY);
-
-        final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
-        radius.setDuration(radiusDuration);
-        radius.setInterpolator(DECELERATE_INTERPOLATOR);
-
-        final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX);
-        x.setDuration(originDuration);
-        x.setInterpolator(DECELERATE_INTERPOLATOR);
-
-        final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
-        y.setDuration(originDuration);
-        y.setInterpolator(DECELERATE_INTERPOLATOR);
-
-        final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
-                RenderNodeAnimator.PAINT_ALPHA, 0);
-        opacity.setDuration(opacityDuration);
-        opacity.setInterpolator(LINEAR_INTERPOLATOR);
-        opacity.addListener(mAnimationListener);
-
-        final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
-        set.add(radius);
-        set.add(opacity);
-        set.add(x);
-        set.add(y);
-
-        return set;
-    }
-
-    @Override
-    protected void jumpValuesToExit() {
-        mOpacity = 0;
-        mTweenX = 1;
-        mTweenY = 1;
-        mTweenRadius = 1;
+        pruneSwFinished();
+        if (hasDisplayListCanvas) {
+            final DisplayListCanvas hw = (DisplayListCanvas) c;
+            drawHardware(hw, p);
+        } else {
+            drawSoftware(c, p);
+        }
     }
 
     /**
@@ -319,10 +382,39 @@
         }
     }
 
+    /**
+     * Ends all animations, jumping values to the end state.
+     */
+    public void end() {
+        for (int i = 0; i < mRunningSwAnimators.size(); i++) {
+            mRunningSwAnimators.get(i).end();
+        }
+        mRunningSwAnimators.clear();
+        for (int i = 0; i < mRunningHwAnimators.size(); i++) {
+            mRunningHwAnimators.get(i).end();
+        }
+        mRunningHwAnimators.clear();
+    }
+
+    private void onAnimationPropertyChanged() {
+        if (!mUsingProperties) {
+            invalidateSelf();
+        }
+    }
+
     private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
         @Override
         public void onAnimationEnd(Animator animator) {
             mHasFinishedExit = true;
+            pruneHwFinished();
+            pruneSwFinished();
+
+            if (mRunningHwAnimators.isEmpty()) {
+                mPropPaint = null;
+                mPropRadius = null;
+                mPropX = null;
+                mPropY = null;
+            }
         }
     };
 
@@ -361,7 +453,7 @@
         @Override
         public void setValue(RippleForeground object, float value) {
             object.mTweenRadius = value;
-            object.invalidateSelf();
+            object.onAnimationPropertyChanged();
         }
 
         @Override
@@ -375,18 +467,18 @@
      */
     private static final FloatProperty<RippleForeground> TWEEN_ORIGIN =
             new FloatProperty<RippleForeground>("tweenOrigin") {
-                @Override
-                public void setValue(RippleForeground object, float value) {
-                    object.mTweenX = value;
-                    object.mTweenY = value;
-                    object.invalidateSelf();
-                }
+        @Override
+        public void setValue(RippleForeground object, float value) {
+            object.mTweenX = value;
+            object.mTweenY = value;
+            object.onAnimationPropertyChanged();
+        }
 
-                @Override
-                public Float get(RippleForeground object) {
-                    return object.mTweenX;
-                }
-            };
+        @Override
+        public Float get(RippleForeground object) {
+            return object.mTweenX;
+        }
+    };
 
     /**
      * Property for animating opacity between 0 and its target value.
@@ -396,7 +488,7 @@
         @Override
         public void setValue(RippleForeground object, float value) {
             object.mOpacity = value;
-            object.invalidateSelf();
+            object.onAnimationPropertyChanged();
         }
 
         @Override