OSDN Git Service

Separate background from ripple for better focus/press UX
authorAlan Viverette <alanv@google.com>
Thu, 24 Jul 2014 23:31:42 +0000 (16:31 -0700)
committerAlan Viverette <alanv@google.com>
Thu, 24 Jul 2014 23:31:42 +0000 (16:31 -0700)
Regresses the "jump" when focus changes, since the background position
is static and we don't have any reasonable UX spec for this yet.

BUG: 16323640
Change-Id: I7152546ed08375864174049b342653b3c4d3d9fb

graphics/java/android/graphics/drawable/Ripple.java
graphics/java/android/graphics/drawable/RippleBackground.java [new file with mode: 0644]
graphics/java/android/graphics/drawable/RippleDrawable.java

index 2d49365..6f21f2e 100644 (file)
@@ -28,7 +28,6 @@ import android.graphics.Rect;
 import android.util.MathUtils;
 import android.view.HardwareCanvas;
 import android.view.RenderNodeAnimator;
-import android.view.animation.DecelerateInterpolator;
 import android.view.animation.LinearInterpolator;
 
 import java.util.ArrayList;
@@ -44,16 +43,14 @@ class Ripple {
     private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED;
     private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED;
     private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
-    private static final float WAVE_OUTER_OPACITY_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
-    private static final float WAVE_OUTER_OPACITY_VELOCITY_MIN = 1.5f * GLOBAL_SPEED;
-    private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f;
-    private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f;
 
     private static final long RIPPLE_ENTER_DELAY = 80;
 
     // Hardware animators.
-    private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>();
-    private final ArrayList<RenderNodeAnimator> mPendingAnimations = new ArrayList<>();
+    private final ArrayList<RenderNodeAnimator> mRunningAnimations =
+            new ArrayList<RenderNodeAnimator>();
+    private final ArrayList<RenderNodeAnimator> mPendingAnimations =
+            new ArrayList<RenderNodeAnimator>();
 
     private final RippleDrawable mOwner;
 
@@ -79,20 +76,17 @@ class Ripple {
     private CanvasProperty<Float> mPropRadius;
     private CanvasProperty<Float> mPropX;
     private CanvasProperty<Float> mPropY;
-    private CanvasProperty<Paint> mPropOuterPaint;
-    private CanvasProperty<Float> mPropOuterRadius;
-    private CanvasProperty<Float> mPropOuterX;
-    private CanvasProperty<Float> mPropOuterY;
 
     // Software animators.
     private ObjectAnimator mAnimRadius;
     private ObjectAnimator mAnimOpacity;
-    private ObjectAnimator mAnimOuterOpacity;
     private ObjectAnimator mAnimX;
     private ObjectAnimator mAnimY;
 
+    // Temporary paint used for creating canvas properties.
+    private Paint mTempPaint;
+
     // Software rendering properties.
-    private float mOuterOpacity = 0;
     private float mOpacity = 1;
     private float mOuterX;
     private float mOuterY;
@@ -177,38 +171,35 @@ class Ripple {
         return mOpacity;
     }
 
-    public void setOuterOpacity(float a) {
-        mOuterOpacity = a;
-        invalidateSelf();
-    }
-
-    public float getOuterOpacity() {
-        return mOuterOpacity;
-    }
-
+    @SuppressWarnings("unused")
     public void setRadiusGravity(float r) {
         mTweenRadius = r;
         invalidateSelf();
     }
 
+    @SuppressWarnings("unused")
     public float getRadiusGravity() {
         return mTweenRadius;
     }
 
+    @SuppressWarnings("unused")
     public void setXGravity(float x) {
         mTweenX = x;
         invalidateSelf();
     }
 
+    @SuppressWarnings("unused")
     public float getXGravity() {
         return mTweenX;
     }
 
+    @SuppressWarnings("unused")
     public void setYGravity(float y) {
         mTweenY = y;
         invalidateSelf();
     }
 
+    @SuppressWarnings("unused")
     public float getYGravity() {
         return mTweenY;
     }
@@ -238,7 +229,7 @@ class Ripple {
         // If we have any pending hardware animations, cancel any running
         // animations and start those now.
         final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
-        final int N = pendingAnimations == null ? 0 : pendingAnimations.size();
+        final int N = pendingAnimations.size();
         if (N > 0) {
             cancelHardwareAnimations();
 
@@ -251,7 +242,6 @@ class Ripple {
             pendingAnimations.clear();
         }
 
-        c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
         c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
 
         return true;
@@ -262,15 +252,6 @@ class Ripple {
 
         // Cache the paint alpha so we can restore it later.
         final int paintAlpha = p.getAlpha();
-
-        final int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
-        if (outerAlpha > 0 && mOuterRadius > 0) {
-            p.setAlpha(outerAlpha);
-            p.setStyle(Style.FILL);
-            c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
-            hasContent = true;
-        }
-
         final int alpha = (int) (paintAlpha * mOpacity + 0.5f);
         final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
         if (alpha > 0 && radius > 0) {
@@ -316,7 +297,6 @@ class Ripple {
     public void enter() {
         final int radiusDuration = (int)
                 (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
-        final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY_MIN);
 
         final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
         radius.setAutoCancel(true);
@@ -336,13 +316,7 @@ class Ripple {
         cY.setInterpolator(LINEAR_INTERPOLATOR);
         cY.setStartDelay(RIPPLE_ENTER_DELAY);
 
-        final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
-        outer.setAutoCancel(true);
-        outer.setDuration(outerDuration);
-        outer.setInterpolator(LINEAR_INTERPOLATOR);
-
         mAnimRadius = radius;
-        mAnimOuterOpacity = outer;
         mAnimX = cX;
         mAnimY = cY;
 
@@ -350,7 +324,6 @@ class Ripple {
         // that anything interesting is happening until the user lifts their
         // finger.
         radius.start();
-        outer.start();
         cX.start();
         cY.start();
     }
@@ -372,51 +345,23 @@ class Ripple {
                 + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
         final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
 
-        // Scale the outer max opacity and opacity velocity based
-        // on the size of the outer radius
-
-        float outerSizeInfluence = MathUtils.constrain(
-                (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
-                / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
-        float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_VELOCITY_MIN,
-                WAVE_OUTER_OPACITY_VELOCITY_MAX, outerSizeInfluence);
-
-        // Determine at what time the inner and outer opacity intersect.
-        // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
-        // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
-
-        final int outerInflection = Math.max(0, (int) (1000 * (mOpacity - mOuterOpacity)
-                / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
-        final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection
-                * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
-
         if (mCanUseHardware) {
-            exitHardware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity);
+            exitHardware(radiusDuration, opacityDuration);
         } else {
-            exitSoftware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity);
+            exitSoftware(radiusDuration, opacityDuration);
         }
     }
 
-    private void exitHardware(int radiusDuration, int opacityDuration, int outerInflection,
-            int inflectionOpacity) {
+    private void exitHardware(int radiusDuration, int opacityDuration) {
         mPendingAnimations.clear();
 
         final float startX = MathUtils.lerp(
                 mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
         final float startY = MathUtils.lerp(
                 mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
-        final Paint outerPaint = new Paint();
-        outerPaint.setAntiAlias(true);
-        outerPaint.setColor(mColor);
-        outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f));
-        outerPaint.setStyle(Style.FILL);
-        mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
-        mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
-        mPropOuterX = CanvasProperty.createFloat(mOuterX);
-        mPropOuterY = CanvasProperty.createFloat(mOuterY);
 
         final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
-        final Paint paint = new Paint();
+        final Paint paint = getTempPaint();
         paint.setAntiAlias(true);
         paint.setColor(mColor);
         paint.setAlpha((int) (255 * mOpacity + 0.5f));
@@ -442,41 +387,10 @@ class Ripple {
                 RenderNodeAnimator.PAINT_ALPHA, 0);
         opacityAnim.setDuration(opacityDuration);
         opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
-
-        final RenderNodeAnimator outerOpacityAnim;
-        if (outerInflection > 0) {
-            // Outer opacity continues to increase for a bit.
-            outerOpacityAnim = new RenderNodeAnimator(
-                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
-            outerOpacityAnim.setDuration(outerInflection);
-            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
-
-            // Chain the outer opacity exit animation.
-            final int outerDuration = opacityDuration - outerInflection;
-            if (outerDuration > 0) {
-                final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
-                        mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
-                outerFadeOutAnim.setDuration(outerDuration);
-                outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
-                outerFadeOutAnim.setStartDelay(outerInflection);
-                outerFadeOutAnim.setStartValue(inflectionOpacity);
-                outerFadeOutAnim.addListener(mAnimationListener);
-
-                mPendingAnimations.add(outerFadeOutAnim);
-            } else {
-                outerOpacityAnim.addListener(mAnimationListener);
-            }
-        } else {
-            outerOpacityAnim = new RenderNodeAnimator(
-                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
-            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
-            outerOpacityAnim.setDuration(opacityDuration);
-            outerOpacityAnim.addListener(mAnimationListener);
-        }
+        opacityAnim.addListener(mAnimationListener);
 
         mPendingAnimations.add(radiusAnim);
         mPendingAnimations.add(opacityAnim);
-        mPendingAnimations.add(outerOpacityAnim);
         mPendingAnimations.add(xAnim);
         mPendingAnimations.add(yAnim);
 
@@ -485,8 +399,14 @@ class Ripple {
         invalidateSelf();
     }
 
-    private void exitSoftware(int radiusDuration, int opacityDuration, int outerInflection,
-            int inflectionOpacity) {
+    private Paint getTempPaint() {
+        if (mTempPaint == null) {
+            mTempPaint = new Paint();
+        }
+        return mTempPaint;
+    }
+
+    private void exitSoftware(int radiusDuration, int opacityDuration) {
         final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
         radiusAnim.setAutoCancel(true);
         radiusAnim.setDuration(radiusDuration);
@@ -506,58 +426,15 @@ class Ripple {
         opacityAnim.setAutoCancel(true);
         opacityAnim.setDuration(opacityDuration);
         opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
-
-        final ObjectAnimator outerOpacityAnim;
-        if (outerInflection > 0) {
-            // Outer opacity continues to increase for a bit.
-            outerOpacityAnim = ObjectAnimator.ofFloat(this,
-                    "outerOpacity", inflectionOpacity / 255.0f);
-            outerOpacityAnim.setAutoCancel(true);
-            outerOpacityAnim.setDuration(outerInflection);
-            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
-
-            // Chain the outer opacity exit animation.
-            final int outerDuration = opacityDuration - outerInflection;
-            if (outerDuration > 0) {
-                outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(Ripple.this,
-                                "outerOpacity", 0);
-                        outerFadeOutAnim.setAutoCancel(true);
-                        outerFadeOutAnim.setDuration(outerDuration);
-                        outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
-                        outerFadeOutAnim.addListener(mAnimationListener);
-
-                        mAnimOuterOpacity = outerFadeOutAnim;
-
-                        outerFadeOutAnim.start();
-                    }
-
-                    @Override
-                    public void onAnimationCancel(Animator animation) {
-                        animation.removeListener(this);
-                    }
-                });
-            } else {
-                outerOpacityAnim.addListener(mAnimationListener);
-            }
-        } else {
-            outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
-            outerOpacityAnim.setAutoCancel(true);
-            outerOpacityAnim.setDuration(opacityDuration);
-            outerOpacityAnim.addListener(mAnimationListener);
-        }
+        opacityAnim.addListener(mAnimationListener);
 
         mAnimRadius = radiusAnim;
         mAnimOpacity = opacityAnim;
-        mAnimOuterOpacity = outerOpacityAnim;
-        mAnimX = opacityAnim;
-        mAnimY = opacityAnim;
+        mAnimX = xAnim;
+        mAnimY = yAnim;
 
         radiusAnim.start();
         opacityAnim.start();
-        outerOpacityAnim.start();
         xAnim.start();
         yAnim.start();
     }
@@ -579,10 +456,6 @@ class Ripple {
             mAnimOpacity.cancel();
         }
 
-        if (mAnimOuterOpacity != null) {
-            mAnimOuterOpacity.cancel();
-        }
-
         if (mAnimX != null) {
             mAnimX.cancel();
         }
@@ -597,7 +470,7 @@ class Ripple {
      */
     private void cancelHardwareAnimations() {
         final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
-        final int N = runningAnimations == null ? 0 : runningAnimations.size();
+        final int N = runningAnimations.size();
         for (int i = 0; i < N; i++) {
             runningAnimations.get(i).cancel();
         }
diff --git a/graphics/java/android/graphics/drawable/RippleBackground.java b/graphics/java/android/graphics/drawable/RippleBackground.java
new file mode 100644 (file)
index 0000000..d404ccd
--- /dev/null
@@ -0,0 +1,535 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.drawable;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.util.MathUtils;
+import android.view.HardwareCanvas;
+import android.view.RenderNodeAnimator;
+import android.view.animation.LinearInterpolator;
+
+import java.util.ArrayList;
+
+/**
+ * Draws a Material ripple.
+ */
+class RippleBackground {
+    private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+    private static final TimeInterpolator DECEL_INTERPOLATOR = new LogInterpolator();
+
+    private static final float GLOBAL_SPEED = 1.0f;
+    private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED;
+    private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
+    private static final float WAVE_OUTER_OPACITY_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
+    private static final float WAVE_OUTER_OPACITY_VELOCITY_MIN = 1.5f * GLOBAL_SPEED;
+    private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f;
+    private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f;
+
+    private static final long RIPPLE_ENTER_DELAY = 80;
+
+    // Hardware animators.
+    private final ArrayList<RenderNodeAnimator> mRunningAnimations =
+            new ArrayList<RenderNodeAnimator>();
+    private final ArrayList<RenderNodeAnimator> mPendingAnimations =
+            new ArrayList<RenderNodeAnimator>();
+
+    private final RippleDrawable mOwner;
+
+    /** Bounds used for computing max radius. */
+    private final Rect mBounds;
+
+    /** Full-opacity color for drawing this ripple. */
+    private int mColor;
+
+    /** Maximum ripple radius. */
+    private float mOuterRadius;
+
+    /** Screen density used to adjust pixel-based velocities. */
+    private float mDensity;
+
+    private float mStartingX;
+    private float mStartingY;
+    private float mClampedStartingX;
+    private float mClampedStartingY;
+
+    // Hardware rendering properties.
+    private CanvasProperty<Paint> mPropOuterPaint;
+    private CanvasProperty<Float> mPropOuterRadius;
+    private CanvasProperty<Float> mPropOuterX;
+    private CanvasProperty<Float> mPropOuterY;
+
+    // Software animators.
+    private ObjectAnimator mAnimOuterOpacity;
+    private ObjectAnimator mAnimX;
+    private ObjectAnimator mAnimY;
+
+    // Temporary paint used for creating canvas properties.
+    private Paint mTempPaint;
+
+    // Software rendering properties.
+    private float mOuterOpacity = 0;
+    private float mOuterX;
+    private float mOuterY;
+
+    // Values used to tween between the start and end positions.
+    private float mTweenX = 0;
+    private float mTweenY = 0;
+
+    /** Whether we should be drawing hardware animations. */
+    private boolean mHardwareAnimating;
+
+    /** Whether we can use hardware acceleration for the exit animation. */
+    private boolean mCanUseHardware;
+
+    /** Whether we have an explicit maximum radius. */
+    private boolean mHasMaxRadius;
+
+    /**
+     * Creates a new ripple.
+     */
+    public RippleBackground(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
+        mOwner = owner;
+        mBounds = bounds;
+
+        mStartingX = startingX;
+        mStartingY = startingY;
+    }
+
+    public void setup(int maxRadius, int color, float density) {
+        mColor = color | 0xFF000000;
+
+        if (maxRadius != RippleDrawable.RADIUS_AUTO) {
+            mHasMaxRadius = true;
+            mOuterRadius = maxRadius;
+        } else {
+            final float halfWidth = mBounds.width() / 2.0f;
+            final float halfHeight = mBounds.height() / 2.0f;
+            mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
+        }
+
+        mOuterX = 0;
+        mOuterY = 0;
+        mDensity = density;
+
+        clampStartingPosition();
+    }
+
+    private void clampStartingPosition() {
+        final float cX = mBounds.exactCenterX();
+        final float cY = mBounds.exactCenterY();
+        final float dX = mStartingX - cX;
+        final float dY = mStartingY - cY;
+        final float r = mOuterRadius;
+        if (dX * dX + dY * dY > r * r) {
+            // Point is outside the circle, clamp to the circumference.
+            final double angle = Math.atan2(dY, dX);
+            mClampedStartingX = cX + (float) (Math.cos(angle) * r);
+            mClampedStartingY = cY + (float) (Math.sin(angle) * r);
+        } else {
+            mClampedStartingX = mStartingX;
+            mClampedStartingY = mStartingY;
+        }
+    }
+
+    public void onHotspotBoundsChanged() {
+        if (!mHasMaxRadius) {
+            final float halfWidth = mBounds.width() / 2.0f;
+            final float halfHeight = mBounds.height() / 2.0f;
+            mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
+
+            clampStartingPosition();
+        }
+    }
+
+    @SuppressWarnings("unused")
+    public void setOuterOpacity(float a) {
+        mOuterOpacity = a;
+        invalidateSelf();
+    }
+
+    @SuppressWarnings("unused")
+    public float getOuterOpacity() {
+        return mOuterOpacity;
+    }
+
+    @SuppressWarnings("unused")
+    public void setXGravity(float x) {
+        mTweenX = x;
+        invalidateSelf();
+    }
+
+    @SuppressWarnings("unused")
+    public float getXGravity() {
+        return mTweenX;
+    }
+
+    @SuppressWarnings("unused")
+    public void setYGravity(float y) {
+        mTweenY = y;
+        invalidateSelf();
+    }
+
+    @SuppressWarnings("unused")
+    public float getYGravity() {
+        return mTweenY;
+    }
+
+    /**
+     * Draws the ripple centered at (0,0) using the specified paint.
+     */
+    public boolean draw(Canvas c, Paint p) {
+        final boolean canUseHardware = c.isHardwareAccelerated();
+        if (mCanUseHardware != canUseHardware && mCanUseHardware) {
+            // We've switched from hardware to non-hardware mode. Panic.
+            cancelHardwareAnimations();
+        }
+        mCanUseHardware = canUseHardware;
+
+        final boolean hasContent;
+        if (canUseHardware && mHardwareAnimating) {
+            hasContent = drawHardware((HardwareCanvas) c);
+        } else {
+            hasContent = drawSoftware(c, p);
+        }
+
+        return hasContent;
+    }
+
+    private boolean drawHardware(HardwareCanvas c) {
+        // If we have any pending hardware animations, cancel any running
+        // animations and start those now.
+        final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
+        final int N = pendingAnimations.size();
+        if (N > 0) {
+            cancelHardwareAnimations();
+
+            for (int i = 0; i < N; i++) {
+                pendingAnimations.get(i).setTarget(c);
+                pendingAnimations.get(i).start();
+            }
+
+            mRunningAnimations.addAll(pendingAnimations);
+            pendingAnimations.clear();
+        }
+
+        c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
+
+        return true;
+    }
+
+    private boolean drawSoftware(Canvas c, Paint p) {
+        boolean hasContent = false;
+
+        // Cache the paint alpha so we can restore it later.
+        final int paintAlpha = p.getAlpha();
+
+        final int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
+        if (outerAlpha > 0 && mOuterRadius > 0) {
+            p.setAlpha(outerAlpha);
+            p.setStyle(Style.FILL);
+            c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
+            hasContent = true;
+        }
+
+        p.setAlpha(paintAlpha);
+
+        return hasContent;
+    }
+
+    /**
+     * Returns the maximum bounds of the ripple relative to the ripple center.
+     */
+    public void getBounds(Rect bounds) {
+        final int outerX = (int) mOuterX;
+        final int outerY = (int) mOuterY;
+        final int r = (int) mOuterRadius;
+        bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
+    }
+
+    /**
+     * Specifies the starting position relative to the drawable bounds. No-op if
+     * the ripple has already entered.
+     */
+    public void move(float x, float y) {
+        mStartingX = x;
+        mStartingY = y;
+
+        clampStartingPosition();
+    }
+
+    /**
+     * Starts the enter animation.
+     */
+    public void enter() {
+        final int radiusDuration = (int)
+                (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
+        final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY_MIN);
+
+        final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
+        cX.setAutoCancel(true);
+        cX.setDuration(radiusDuration);
+        cX.setInterpolator(LINEAR_INTERPOLATOR);
+        cX.setStartDelay(RIPPLE_ENTER_DELAY);
+
+        final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
+        cY.setAutoCancel(true);
+        cY.setDuration(radiusDuration);
+        cY.setInterpolator(LINEAR_INTERPOLATOR);
+        cY.setStartDelay(RIPPLE_ENTER_DELAY);
+
+        final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
+        outer.setAutoCancel(true);
+        outer.setDuration(outerDuration);
+        outer.setInterpolator(LINEAR_INTERPOLATOR);
+
+        mAnimOuterOpacity = outer;
+        mAnimX = cX;
+        mAnimY = cY;
+
+        // Enter animations always run on the UI thread, since it's unlikely
+        // that anything interesting is happening until the user lifts their
+        // finger.
+        outer.start();
+        cX.start();
+        cY.start();
+    }
+
+    /**
+     * Starts the exit animation.
+     */
+    public void exit() {
+        cancelSoftwareAnimations();
+
+        // Scale the outer max opacity and opacity velocity based
+        // on the size of the outer radius.
+        final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
+        final float outerSizeInfluence = MathUtils.constrain(
+                (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
+                / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
+        final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_VELOCITY_MIN,
+                WAVE_OUTER_OPACITY_VELOCITY_MAX, outerSizeInfluence);
+
+        // Determine at what time the inner and outer opacity intersect.
+        // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
+        // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
+        final int outerInflection = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
+                / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
+        final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection
+                * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
+
+        if (mCanUseHardware) {
+            exitHardware(opacityDuration, outerInflection, inflectionOpacity);
+        } else {
+            exitSoftware(opacityDuration, outerInflection, inflectionOpacity);
+        }
+    }
+
+    private void exitHardware(int opacityDuration, int outerInflection, int inflectionOpacity) {
+        mPendingAnimations.clear();
+
+        // TODO: Adjust background by starting position.
+        final float startX = MathUtils.lerp(
+                mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
+        final float startY = MathUtils.lerp(
+                mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
+
+        final Paint outerPaint = getTempPaint();
+        outerPaint.setAntiAlias(true);
+        outerPaint.setColor(mColor);
+        outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f));
+        outerPaint.setStyle(Style.FILL);
+        mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
+        mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
+        mPropOuterX = CanvasProperty.createFloat(mOuterX);
+        mPropOuterY = CanvasProperty.createFloat(mOuterY);
+
+        final RenderNodeAnimator outerOpacityAnim;
+        if (outerInflection > 0) {
+            // Outer opacity continues to increase for a bit.
+            outerOpacityAnim = new RenderNodeAnimator(
+                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
+            outerOpacityAnim.setDuration(outerInflection);
+            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
+
+            // Chain the outer opacity exit animation.
+            final int outerDuration = opacityDuration - outerInflection;
+            if (outerDuration > 0) {
+                final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
+                        mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
+                outerFadeOutAnim.setDuration(outerDuration);
+                outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
+                outerFadeOutAnim.setStartDelay(outerInflection);
+                outerFadeOutAnim.setStartValue(inflectionOpacity);
+                outerFadeOutAnim.addListener(mAnimationListener);
+
+                mPendingAnimations.add(outerFadeOutAnim);
+            } else {
+                outerOpacityAnim.addListener(mAnimationListener);
+            }
+        } else {
+            outerOpacityAnim = new RenderNodeAnimator(
+                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
+            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
+            outerOpacityAnim.setDuration(opacityDuration);
+            outerOpacityAnim.addListener(mAnimationListener);
+        }
+
+        mPendingAnimations.add(outerOpacityAnim);
+
+        mHardwareAnimating = true;
+
+        invalidateSelf();
+    }
+
+    private Paint getTempPaint() {
+        if (mTempPaint == null) {
+            mTempPaint = new Paint();
+        }
+        return mTempPaint;
+    }
+
+    private void exitSoftware(int opacityDuration, int outerInflection, int inflectionOpacity) {
+        final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
+        xAnim.setAutoCancel(true);
+        xAnim.setDuration(opacityDuration);
+        xAnim.setInterpolator(DECEL_INTERPOLATOR);
+
+        final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
+        yAnim.setAutoCancel(true);
+        yAnim.setDuration(opacityDuration);
+        yAnim.setInterpolator(DECEL_INTERPOLATOR);
+
+        final ObjectAnimator outerOpacityAnim;
+        if (outerInflection > 0) {
+            // Outer opacity continues to increase for a bit.
+            outerOpacityAnim = ObjectAnimator.ofFloat(this,
+                    "outerOpacity", inflectionOpacity / 255.0f);
+            outerOpacityAnim.setAutoCancel(true);
+            outerOpacityAnim.setDuration(outerInflection);
+            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
+
+            // Chain the outer opacity exit animation.
+            final int outerDuration = opacityDuration - outerInflection;
+            if (outerDuration > 0) {
+                outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
+                                RippleBackground.this, "outerOpacity", 0);
+                        outerFadeOutAnim.setAutoCancel(true);
+                        outerFadeOutAnim.setDuration(outerDuration);
+                        outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
+                        outerFadeOutAnim.addListener(mAnimationListener);
+
+                        mAnimOuterOpacity = outerFadeOutAnim;
+
+                        outerFadeOutAnim.start();
+                    }
+
+                    @Override
+                    public void onAnimationCancel(Animator animation) {
+                        animation.removeListener(this);
+                    }
+                });
+            } else {
+                outerOpacityAnim.addListener(mAnimationListener);
+            }
+        } else {
+            outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
+            outerOpacityAnim.setAutoCancel(true);
+            outerOpacityAnim.setDuration(opacityDuration);
+            outerOpacityAnim.addListener(mAnimationListener);
+        }
+
+        mAnimOuterOpacity = outerOpacityAnim;
+        mAnimX = xAnim;
+        mAnimY = yAnim;
+
+        outerOpacityAnim.start();
+        xAnim.start();
+        yAnim.start();
+    }
+
+    /**
+     * Cancel all animations.
+     */
+    public void cancel() {
+        cancelSoftwareAnimations();
+        cancelHardwareAnimations();
+    }
+
+    private void cancelSoftwareAnimations() {
+        if (mAnimOuterOpacity != null) {
+            mAnimOuterOpacity.cancel();
+        }
+
+        if (mAnimX != null) {
+            mAnimX.cancel();
+        }
+
+        if (mAnimY != null) {
+            mAnimY.cancel();
+        }
+    }
+
+    /**
+     * Cancels any running hardware animations.
+     */
+    private void cancelHardwareAnimations() {
+        final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
+        final int N = runningAnimations.size();
+        for (int i = 0; i < N; i++) {
+            runningAnimations.get(i).cancel();
+        }
+
+        runningAnimations.clear();
+    }
+
+    private void removeSelf() {
+        // The owner will invalidate itself.
+        mOwner.removeBackground(this);
+    }
+
+    private void invalidateSelf() {
+        mOwner.invalidateSelf();
+    }
+
+    private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            removeSelf();
+        }
+    };
+
+    /**
+    * Interpolator with a smooth log deceleration
+    */
+    private static final class LogInterpolator implements TimeInterpolator {
+        @Override
+        public float getInterpolation(float input) {
+            return 1 - (float) Math.pow(400, -input * 1.4);
+        }
+    }
+}
index 0e719ee..0c9c558 100644 (file)
@@ -120,8 +120,22 @@ public class RippleDrawable extends LayerDrawable {
     /** The masking layer, e.g. the layer with id R.id.mask. */
     private Drawable mMask;
 
-    /** The current hotspot. May be actively animating or pending entry. */
-    private Ripple mHotspot;
+    /** The current background. May be actively animating or pending entry. */
+    private RippleBackground mBackground;
+
+    /** Whether we expect to draw a background when visible. */
+    private boolean mBackgroundActive;
+
+    /** The current ripple. May be actively animating or pending entry. */
+    private Ripple mRipple;
+
+    /** Whether we expect to draw a ripple when visible. */
+    private boolean mRippleActive;
+
+    // Hotspot coordinates that are awaiting activation.
+    private float mPendingX;
+    private float mPendingY;
+    private boolean mHasPending;
 
     /**
      * Lazily-created array of actively animating ripples. Inactive ripples are
@@ -142,9 +156,6 @@ public class RippleDrawable extends LayerDrawable {
     /** Whether bounds are being overridden. */
     private boolean mOverrideBounds;
 
-    /** Whether the hotspot is currently active (e.g. focused or pressed). */
-    private boolean mActive;
-
     /**
      * Constructor used for drawable inflation.
      */
@@ -205,20 +216,26 @@ public class RippleDrawable extends LayerDrawable {
     protected boolean onStateChange(int[] stateSet) {
         super.onStateChange(stateSet);
 
-        // TODO: This would make more sense in a StateListDrawable.
-        boolean active = false;
         boolean enabled = false;
+        boolean pressed = false;
+        boolean focused = false;
+
         final int N = stateSet.length;
         for (int i = 0; i < N; i++) {
             if (stateSet[i] == R.attr.state_enabled) {
                 enabled = true;
             }
             if (stateSet[i] == R.attr.state_focused
-                    || stateSet[i] == R.attr.state_pressed) {
-                active = true;
+                    || stateSet[i] == R.attr.state_selected) {
+                focused = true;
+            }
+            if (stateSet[i] == R.attr.state_pressed) {
+                pressed = true;
             }
         }
-        setActive(active && enabled);
+
+        setRippleActive(enabled && pressed);
+        setBackgroundActive(focused || (enabled && pressed));
 
         // Update the paint color. Only applicable when animated in software.
         if (mRipplePaint != null && mState.mColor != null) {
@@ -235,14 +252,24 @@ public class RippleDrawable extends LayerDrawable {
         return false;
     }
 
-    private void setActive(boolean active) {
-        if (mActive != active) {
-            mActive = active;
+    private void setRippleActive(boolean active) {
+        if (mRippleActive != active) {
+            mRippleActive = active;
+            if (active) {
+                activateRipple();
+            } else {
+                removeRipple();
+            }
+        }
+    }
 
+    private void setBackgroundActive(boolean active) {
+        if (mBackgroundActive != active) {
+            mBackgroundActive = active;
             if (active) {
-                activateHotspot();
+                activateBackground();
             } else {
-                removeHotspot();
+                removeBackground();
             }
         }
     }
@@ -261,11 +288,23 @@ public class RippleDrawable extends LayerDrawable {
 
     @Override
     public boolean setVisible(boolean visible, boolean restart) {
+        final boolean changed = super.setVisible(visible, restart);
+
         if (!visible) {
             clearHotspots();
+        } else if (changed) {
+            // If we just became visible, ensure the background and ripple
+            // visibilities are consistent with their internal states.
+            if (mRippleActive) {
+                activateRipple();
+            }
+
+            if (mBackgroundActive) {
+                activateBackground();
+            }
         }
 
-        return super.setVisible(visible, restart);
+        return changed;
     }
 
     /**
@@ -399,54 +438,101 @@ public class RippleDrawable extends LayerDrawable {
 
     @Override
     public void setHotspot(float x, float y) {
-        if (mHotspot == null) {
-            mHotspot = new Ripple(this, mHotspotBounds, x, y);
+        if (mRipple == null || mBackground == null) {
+            mPendingX = x;
+            mPendingY = y;
+            mHasPending = true;
+        }
+
+        if (mRipple != null) {
+            mRipple.move(x, y);
+        }
 
-            if (mActive) {
-                activateHotspot();
+        if (mBackground != null) {
+            mBackground.move(x, y);
+        }
+    }
+
+    /**
+     * Creates an active hotspot at the specified location.
+     */
+    private void activateBackground() {
+        if (mBackground == null) {
+            final float x;
+            final float y;
+            if (mHasPending) {
+                mHasPending = false;
+                x = mPendingX;
+                y = mPendingY;
+            } else {
+                x = mHotspotBounds.exactCenterX();
+                y = mHotspotBounds.exactCenterY();
             }
-        } else {
-            mHotspot.move(x, y);
+            mBackground = new RippleBackground(this, mHotspotBounds, x, y);
+        }
+
+        final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
+        mBackground.setup(mState.mMaxRadius, color, mDensity);
+        mBackground.enter();
+    }
+
+    private void removeBackground() {
+        if (mBackground != null) {
+            // Don't null out the background, we need it to draw!
+            mBackground.exit();
         }
     }
 
     /**
      * Creates an active hotspot at the specified location.
      */
-    private void activateHotspot() {
+    private void activateRipple() {
         if (mAnimatingRipplesCount >= MAX_RIPPLES) {
             // This should never happen unless the user is tapping like a maniac
             // or there is a bug that's preventing ripples from being removed.
             return;
         }
 
-        if (mHotspot == null) {
-            final float x = mHotspotBounds.exactCenterX();
-            final float y = mHotspotBounds.exactCenterY();
-            mHotspot = new Ripple(this, mHotspotBounds, x, y);
+        if (mRipple == null) {
+            final float x;
+            final float y;
+            if (mHasPending) {
+                mHasPending = false;
+                x = mPendingX;
+                y = mPendingY;
+            } else {
+                x = mHotspotBounds.exactCenterX();
+                y = mHotspotBounds.exactCenterY();
+            }
+            mRipple = new Ripple(this, mHotspotBounds, x, y);
         }
 
         final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
-        mHotspot.setup(mState.mMaxRadius, color, mDensity);
-        mHotspot.enter();
+        mRipple.setup(mState.mMaxRadius, color, mDensity);
+        mRipple.enter();
 
         if (mAnimatingRipples == null) {
             mAnimatingRipples = new Ripple[MAX_RIPPLES];
         }
-        mAnimatingRipples[mAnimatingRipplesCount++] = mHotspot;
+        mAnimatingRipples[mAnimatingRipplesCount++] = mRipple;
     }
 
-    private void removeHotspot() {
-        if (mHotspot != null) {
-            mHotspot.exit();
-            mHotspot = null;
+    private void removeRipple() {
+        if (mRipple != null) {
+            mRipple.exit();
+            mRipple = null;
         }
     }
 
     private void clearHotspots() {
-        if (mHotspot != null) {
-            mHotspot.cancel();
-            mHotspot = null;
+        if (mRipple != null) {
+            mRipple.cancel();
+            mRipple = null;
+        }
+
+        if (mBackground != null) {
+            mBackground.cancel();
+            mBackground = null;
         }
 
         final int count = mAnimatingRipplesCount;
@@ -486,6 +572,10 @@ public class RippleDrawable extends LayerDrawable {
         for (int i = 0; i < count; i++) {
             ripples[i].onHotspotBoundsChanged();
         }
+
+        if (mBackground != null) {
+            mBackground.onHotspotBoundsChanged();
+        }
     }
 
     /**
@@ -524,18 +614,28 @@ public class RippleDrawable extends LayerDrawable {
         // Next, try to draw the ripples (into a layer if necessary). If we need
         // to mask against the underlying content, set the xfermode to SRC_ATOP.
         final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP;
-        final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode);
+
+        // If we have a background and a non-opaque mask, draw the masking layer.
+        final int backgroundLayer = drawBackgroundLayer(canvas, bounds, xfermode);
+        if (backgroundLayer >= 0) {
+            if (drawMask) {
+                drawMaskingLayer(canvas, bounds, DST_IN);
+            }
+            canvas.restoreToCount(backgroundLayer);
+        }
 
         // If we have ripples and a non-opaque mask, draw the masking layer.
-        if (rippleLayer >= 0 && drawMask) {
-            drawMaskingLayer(canvas, bounds, DST_IN);
+        final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode);
+        if (rippleLayer >= 0) {
+            if (drawMask) {
+                drawMaskingLayer(canvas, bounds, DST_IN);
+            }
+            canvas.restoreToCount(rippleLayer);
         }
 
         // Composite the layers if needed.
         if (contentLayer >= 0) {
             canvas.restoreToCount(contentLayer);
-        } else if (rippleLayer >= 0) {
-            canvas.restoreToCount(rippleLayer);
         }
     }
 
@@ -550,15 +650,20 @@ public class RippleDrawable extends LayerDrawable {
         final int count = mAnimatingRipplesCount;
         final int index = getRippleIndex(ripple);
         if (index >= 0) {
-            for (int i = index + 1; i < count; i++) {
-                ripples[i - 1] = ripples[i];
-            }
+            System.arraycopy(ripples, index + 1, ripples, index + 1 - 1, count - (index + 1));
             ripples[count - 1] = null;
             mAnimatingRipplesCount--;
             invalidateSelf();
         }
     }
 
+    void removeBackground(RippleBackground background) {
+        if (mBackground == background) {
+            mBackground = null;
+            invalidateSelf();
+        }
+    }
+
     private int getRippleIndex(Ripple ripple) {
         final Ripple[] ripples = mAnimatingRipples;
         final int count = mAnimatingRipplesCount;
@@ -577,7 +682,7 @@ public class RippleDrawable extends LayerDrawable {
         // We don't need a layer if we don't expect to draw any ripples, we have
         // an explicit mask, or if the non-mask content is all opaque.
         boolean needsLayer = false;
-        if (mAnimatingRipplesCount > 0 && mMask == null) {
+        if ((mAnimatingRipplesCount > 0 || mBackground != null) && mMask == null) {
             for (int i = 0; i < count; i++) {
                 if (array[i].mId != R.id.mask
                         && array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
@@ -601,12 +706,62 @@ public class RippleDrawable extends LayerDrawable {
         return restoreToCount;
     }
 
-    private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
-        final int count = mAnimatingRipplesCount;
-        if (count == 0) {
-            return -1;
+    private int drawBackgroundLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
+        // Separate the ripple color and alpha channel. The alpha will be
+        // applied when we merge the ripples down to the canvas.
+        final int rippleARGB;
+        if (mState.mColor != null) {
+            rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
+        } else {
+            rippleARGB = Color.TRANSPARENT;
+        }
+
+        if (mRipplePaint == null) {
+            mRipplePaint = new Paint();
+            mRipplePaint.setAntiAlias(true);
+        }
+
+        final int rippleAlpha = Color.alpha(rippleARGB);
+        final Paint ripplePaint = mRipplePaint;
+        ripplePaint.setColor(rippleARGB);
+        ripplePaint.setAlpha(0xFF);
+
+        boolean drewRipples = false;
+        int restoreToCount = -1;
+        int restoreTranslate = -1;
+
+        // Draw background.
+        final RippleBackground background = mBackground;
+        if (background != null) {
+            // If we're masking the ripple layer, make sure we have a layer
+            // first. This will merge SRC_OVER (directly) onto the canvas.
+            final Paint maskingPaint = getMaskingPaint(mode);
+            maskingPaint.setAlpha(rippleAlpha);
+            restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
+                    bounds.right, bounds.bottom, maskingPaint);
+
+            restoreTranslate = canvas.save();
+            // Translate the canvas to the current hotspot bounds.
+            canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
+
+            drewRipples = background.draw(canvas, ripplePaint);
         }
 
+        // Always restore the translation.
+        if (restoreTranslate >= 0) {
+            canvas.restoreToCount(restoreTranslate);
+        }
+
+        // If we created a layer with no content, merge it immediately.
+        if (restoreToCount >= 0 && !drewRipples) {
+            canvas.restoreToCount(restoreToCount);
+            restoreToCount = -1;
+        }
+
+        return restoreToCount;
+    }
+
+    private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
         // Separate the ripple color and alpha channel. The alpha will be
         // applied when we merge the ripples down to the canvas.
         final int rippleARGB;
@@ -631,6 +786,7 @@ public class RippleDrawable extends LayerDrawable {
         int restoreTranslate = -1;
 
         // Draw ripples and update the animating ripples array.
+        final int count = mAnimatingRipplesCount;
         final Ripple[] ripples = mAnimatingRipples;
         for (int i = 0; i < count; i++) {
             final Ripple ripple = ripples[i];
@@ -705,6 +861,13 @@ public class RippleDrawable extends LayerDrawable {
                 drawingBounds.union(rippleBounds);
             }
 
+            final RippleBackground background = mBackground;
+            if (background != null) {
+                background.getBounds(rippleBounds);
+                rippleBounds.offset(cX, cY);
+                drawingBounds.union(rippleBounds);
+            }
+
             dirtyBounds.union(drawingBounds);
             dirtyBounds.union(super.getDirtyBounds());
             return dirtyBounds;