Implementing the new material ripple

Fixes: 167177623
Test: RippleDrawableTest
Change-Id: Ic3962760bc9a53c82f66b267bd5aa5006c09fa00
diff --git a/core/api/current.txt b/core/api/current.txt
index 9ef3eb4..66df78b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1185,6 +1185,7 @@
     field public static final int right = 16843183; // 0x10101af
     field public static final int ringtonePreferenceStyle = 16842899; // 0x1010093
     field public static final int ringtoneType = 16843257; // 0x10101f9
+    field public static final int rippleStyle = 16844337; // 0x1010631
     field public static final int rollbackDataPolicy = 16844311; // 0x1010617
     field public static final int rotation = 16843558; // 0x1010326
     field public static final int rotationAnimation = 16844090; // 0x101053a
@@ -16524,9 +16525,13 @@
   public class RippleDrawable extends android.graphics.drawable.LayerDrawable {
     ctor public RippleDrawable(@NonNull android.content.res.ColorStateList, @Nullable android.graphics.drawable.Drawable, @Nullable android.graphics.drawable.Drawable);
     method public int getRadius();
+    method public int getRippleStyle();
     method public void setColor(android.content.res.ColorStateList);
     method public void setRadius(int);
+    method public void setRippleStyle(int) throws java.lang.IllegalArgumentException;
     field public static final int RADIUS_AUTO = -1; // 0xffffffff
+    field public static final int STYLE_PATTERNED = 1; // 0x1
+    field public static final int STYLE_SOLID = 0; // 0x0
   }
 
   public class RotateDrawable extends android.graphics.drawable.DrawableWrapper {
diff --git a/core/res/res/drawable/btn_borderless_material.xml b/core/res/res/drawable/btn_borderless_material.xml
index 08e1060..1a0912e 100644
--- a/core/res/res/drawable/btn_borderless_material.xml
+++ b/core/res/res/drawable/btn_borderless_material.xml
@@ -15,7 +15,8 @@
 -->
 
 <ripple xmlns:android="http://schemas.android.com/apk/res/android"
-        android:color="?attr/colorControlHighlight">
+        android:color="?attr/colorControlHighlight"
+        android:rippleStyle="?attr/rippleStyle">
     <item android:id="@id/mask"
           android:drawable="@drawable/btn_default_mtrl_shape" />
 </ripple>
diff --git a/core/res/res/drawable/btn_colored_material.xml b/core/res/res/drawable/btn_colored_material.xml
index 7ba21e8..5274ef2 100644
--- a/core/res/res/drawable/btn_colored_material.xml
+++ b/core/res/res/drawable/btn_colored_material.xml
@@ -19,7 +19,8 @@
        android:insetTop="@dimen/button_inset_vertical_material"
        android:insetRight="@dimen/button_inset_horizontal_material"
        android:insetBottom="@dimen/button_inset_vertical_material">
-    <ripple android:color="?attr/colorControlHighlight">
+    <ripple android:color="?attr/colorControlHighlight"
+            android:rippleStyle="?attr/rippleStyle">
         <item>
             <shape android:shape="rectangle"
                    android:tint="@color/btn_colored_background_material">
diff --git a/core/res/res/drawable/btn_default_material.xml b/core/res/res/drawable/btn_default_material.xml
index ed2b5aa..6a9e621 100644
--- a/core/res/res/drawable/btn_default_material.xml
+++ b/core/res/res/drawable/btn_default_material.xml
@@ -15,6 +15,7 @@
 -->
 
 <ripple xmlns:android="http://schemas.android.com/apk/res/android"
-        android:color="?attr/colorControlHighlight">
+        android:color="?attr/colorControlHighlight"
+        android:rippleStyle="?attr/rippleStyle">
     <item android:drawable="@drawable/btn_default_mtrl_shape" />
 </ripple>
diff --git a/core/res/res/drawable/btn_toggle_material.xml b/core/res/res/drawable/btn_toggle_material.xml
index 8b19e4a..7cee382 100644
--- a/core/res/res/drawable/btn_toggle_material.xml
+++ b/core/res/res/drawable/btn_toggle_material.xml
@@ -21,7 +21,8 @@
        android:insetBottom="@dimen/button_inset_vertical_material">
     <layer-list android:paddingMode="stack">
         <item>
-            <ripple android:color="?attr/colorControlHighlight">
+            <ripple android:color="?attr/colorControlHighlight"
+                android:rippleStyle="?attr/rippleStyle">
                 <item>
                     <shape android:shape="rectangle"
                            android:tint="?attr/colorButtonNormal">
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 927a8b4..67b810e 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -6389,6 +6389,13 @@
         <!-- The radius of the ripple when fully expanded. By default, the
              radius is computed based on the size of the ripple's container. -->
         <attr name="radius" />
+        <!-- The style of the ripple drawable is solid by default -->
+        <attr name="rippleStyle">
+            <!-- Solid is the default style -->
+            <enum name="solid" value="0" />
+            <!-- Patterned style-->
+            <enum name="patterned" value="1" />
+        </attr>
     </declare-styleable>
 
     <declare-styleable name="ScaleDrawable">
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 278708a..d22bdde 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -3071,6 +3071,7 @@
     <public name="windowSplashScreenAnimationDuration"/>
     <public name="windowSplashScreenBrandingImage"/>
     <public name="splashScreenTheme" />
+    <public name="rippleStyle" />
   </public-group>
 
   <public-group type="drawable" first-id="0x010800b5">
diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml
index ce4ee87..e7e049da 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -224,6 +224,9 @@
         <item name="textColorTertiary">@color/text_color_tertiary_device_default_dark</item>
         <item name="colorForeground">@color/foreground_device_default_dark</item>
         <item name="colorForegroundInverse">@color/foreground_device_default_light</item>
+
+        <!-- Ripple style-->
+        <item name="rippleStyle">solid</item>
     </style>
 
     <style name="Theme.DeviceDefault" parent="Theme.DeviceDefaultBase" />
diff --git a/graphics/java/android/graphics/drawable/RippleAnimationSession.java b/graphics/java/android/graphics/drawable/RippleAnimationSession.java
new file mode 100644
index 0000000..80f65f9
--- /dev/null
+++ b/graphics/java/android/graphics/drawable/RippleAnimationSession.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2020 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.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.Paint;
+import android.graphics.RecordingCanvas;
+import android.graphics.animation.RenderNodeAnimator;
+import android.util.ArraySet;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+
+import java.util.function.Consumer;
+
+/**
+ * @hide
+ */
+public final class RippleAnimationSession {
+    private static final int ENTER_ANIM_DURATION = 350;
+    private static final int EXIT_ANIM_OFFSET = ENTER_ANIM_DURATION;
+    private static final int EXIT_ANIM_DURATION = 350;
+    private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+    // Matches R.interpolator.fast_out_slow_in but as we have no context we can't just import that
+    private static final TimeInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
+
+    private Consumer<RippleAnimationSession> mOnSessionEnd;
+    private AnimationProperties<Float, Paint> mProperties;
+    private AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> mCanvasProperties;
+    private Runnable mOnUpdate;
+    private long mStartTime;
+    private boolean mForceSoftware;
+    private ArraySet<Animator> mActiveAnimations = new ArraySet(3);
+
+    RippleAnimationSession(@NonNull AnimationProperties<Float, Paint> properties,
+            boolean forceSoftware) {
+        mProperties = properties;
+        mForceSoftware = forceSoftware;
+    }
+
+    void end() {
+        for (Animator anim: mActiveAnimations) {
+            if (anim != null) anim.end();
+        }
+        mActiveAnimations.clear();
+    }
+
+    @NonNull RippleAnimationSession enter(Canvas canvas) {
+        if (isHwAccelerated(canvas)) {
+            enterHardware((RecordingCanvas) canvas);
+        } else {
+            enterSoftware();
+        }
+        mStartTime = System.nanoTime();
+        return this;
+    }
+
+    @NonNull RippleAnimationSession exit(Canvas canvas) {
+        if (isHwAccelerated(canvas)) exitHardware((RecordingCanvas) canvas);
+        else exitSoftware();
+        return this;
+    }
+
+    private void onAnimationEnd(Animator anim) {
+        mActiveAnimations.remove(anim);
+    }
+
+    @NonNull RippleAnimationSession setOnSessionEnd(
+            @Nullable Consumer<RippleAnimationSession> onSessionEnd) {
+        mOnSessionEnd = onSessionEnd;
+        return this;
+    }
+
+    RippleAnimationSession setOnAnimationUpdated(@Nullable Runnable run) {
+        mOnUpdate = run;
+        mProperties.setOnChange(mOnUpdate);
+        return this;
+    }
+
+    private boolean isHwAccelerated(Canvas canvas) {
+        return canvas.isHardwareAccelerated() && !mForceSoftware;
+    }
+
+    private void exitSoftware() {
+        ValueAnimator expand = ValueAnimator.ofFloat(.5f, 1f);
+        expand.setDuration(EXIT_ANIM_DURATION);
+        expand.setStartDelay(computeDelay());
+        expand.addUpdateListener(updatedAnimation -> {
+            notifyUpdate();
+            mProperties.getShader().setProgress((Float) expand.getAnimatedValue());
+        });
+        expand.addListener(new AnimatorListener(this) {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                Consumer<RippleAnimationSession> onEnd = mOnSessionEnd;
+                if (onEnd != null) onEnd.accept(RippleAnimationSession.this);
+            }
+        });
+        expand.setInterpolator(LINEAR_INTERPOLATOR);
+        expand.start();
+        mActiveAnimations.add(expand);
+    }
+
+    private long computeDelay() {
+        long currentTime = System.nanoTime();
+        long timePassed =  (currentTime - mStartTime) / 1_000_000;
+        long difference = EXIT_ANIM_OFFSET;
+        return Math.max(difference - timePassed, 0);
+    }
+    private void notifyUpdate() {
+        Runnable onUpdate = mOnUpdate;
+        if (onUpdate != null) onUpdate.run();
+    }
+
+    RippleAnimationSession setForceSoftwareAnimation(boolean forceSw) {
+        mForceSoftware = forceSw;
+        return this;
+    }
+
+
+    private void exitHardware(RecordingCanvas canvas) {
+        AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>>
+                props = getCanvasProperties();
+        RenderNodeAnimator exit =
+                new RenderNodeAnimator(props.getProgress(), 1f);
+        exit.setDuration(EXIT_ANIM_DURATION);
+        exit.addListener(new AnimatorListener(this) {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                Consumer<RippleAnimationSession> onEnd = mOnSessionEnd;
+                if (onEnd != null) onEnd.accept(RippleAnimationSession.this);
+            }
+        });
+        exit.setTarget(canvas);
+        exit.setInterpolator(DECELERATE_INTERPOLATOR);
+
+        long delay = computeDelay();
+        exit.setStartDelay(delay);
+        exit.start();
+        mActiveAnimations.add(exit);
+    }
+
+    private void enterHardware(RecordingCanvas can) {
+        AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>>
+                props = getCanvasProperties();
+        RenderNodeAnimator expand =
+                new RenderNodeAnimator(props.getProgress(), .5f);
+        expand.setTarget(can);
+        expand.setDuration(ENTER_ANIM_DURATION);
+        expand.addListener(new AnimatorListener(this));
+        expand.setInterpolator(LINEAR_INTERPOLATOR);
+        expand.start();
+        mActiveAnimations.add(expand);
+    }
+
+    private void enterSoftware() {
+        ValueAnimator expand = ValueAnimator.ofFloat(0f, 0.5f);
+        expand.addUpdateListener(updatedAnimation -> {
+            notifyUpdate();
+            mProperties.getShader().setProgress((Float) expand.getAnimatedValue());
+        });
+        expand.addListener(new AnimatorListener(this));
+        expand.setInterpolator(LINEAR_INTERPOLATOR);
+        expand.start();
+        mActiveAnimations.add(expand);
+    }
+
+    @NonNull AnimationProperties<Float, Paint> getProperties() {
+        return mProperties;
+    }
+
+    @NonNull AnimationProperties getCanvasProperties() {
+        if (mCanvasProperties == null) {
+            mCanvasProperties = new AnimationProperties<>(
+                    CanvasProperty.createFloat(mProperties.getX()),
+                    CanvasProperty.createFloat(mProperties.getY()),
+                    CanvasProperty.createFloat(mProperties.getMaxRadius()),
+                    CanvasProperty.createPaint(mProperties.getPaint()),
+                    CanvasProperty.createFloat(mProperties.getProgress()),
+                    mProperties.getShader());
+        }
+        return mCanvasProperties;
+    }
+
+    private static class AnimatorListener implements Animator.AnimatorListener {
+        private final RippleAnimationSession mSession;
+
+        AnimatorListener(RippleAnimationSession session) {
+            mSession = session;
+        }
+        @Override
+        public void onAnimationStart(Animator animation) {
+
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mSession.onAnimationEnd(animation);
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator animation) {
+
+        }
+    }
+
+    static class AnimationProperties<FloatType, PaintType> {
+        private final FloatType mY;
+        private FloatType mProgress;
+        private FloatType mMaxRadius;
+        private final PaintType mPaint;
+        private final FloatType mX;
+        private final RippleShader mShader;
+        private Runnable mOnChange;
+
+        private void onChange() {
+            if (mOnChange != null) mOnChange.run();
+        }
+
+        private void setOnChange(Runnable onChange) {
+            mOnChange = onChange;
+        }
+
+        AnimationProperties(FloatType x, FloatType y, FloatType maxRadius,
+                PaintType paint, FloatType progress, RippleShader shader) {
+            mY = y;
+            mX = x;
+            mMaxRadius = maxRadius;
+            mPaint = paint;
+            mShader = shader;
+            mProgress = progress;
+        }
+
+        FloatType getProgress() {
+            return mProgress;
+        }
+
+        FloatType getX() {
+            return mX;
+        }
+
+        FloatType getY() {
+            return mY;
+        }
+
+        FloatType getMaxRadius() {
+            return mMaxRadius;
+        }
+
+        PaintType getPaint() {
+            return mPaint;
+        }
+
+        RippleShader getShader() {
+            return mShader;
+        }
+    }
+}
diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java
index bab80ce..5024875 100644
--- a/graphics/java/android/graphics/drawable/RippleDrawable.java
+++ b/graphics/java/android/graphics/drawable/RippleDrawable.java
@@ -16,6 +16,14 @@
 
 package android.graphics.drawable;
 
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -27,17 +35,21 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapShader;
 import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
 import android.graphics.Color;
+import android.graphics.ColorFilter;
 import android.graphics.Matrix;
 import android.graphics.Outline;
 import android.graphics.Paint;
 import android.graphics.PixelFormat;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
+import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
 import android.graphics.Shader;
 import android.os.Build;
 import android.util.AttributeSet;
+import android.view.animation.LinearInterpolator;
 
 import com.android.internal.R;
 
@@ -45,6 +57,9 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
 import java.util.Arrays;
 
 /**
@@ -94,6 +109,17 @@
  * </pre>
  *
  * @attr ref android.R.styleable#RippleDrawable_color
+ *
+ * To change the ripple style, assign the value of "solid" or "patterned" to the android:rippleStyle
+ * attribute.
+ *
+ * <pre>
+ * <code>&lt;!-- A red ripple masked against an opaque rectangle. --/>
+ * &lt;ripple android:rippleStyle="patterned">
+ * &lt;/ripple></code>
+ * </pre>
+ *
+ * @attr ref android.R.styleable#RippleDrawable_rippleStyle
  */
 public class RippleDrawable extends LayerDrawable {
     /**
@@ -102,6 +128,29 @@
      */
     public static final int RADIUS_AUTO = -1;
 
+    /**
+     * Ripple style where a solid circle is drawn. This is also the default style
+     * @see #setRippleStyle(int)
+     */
+    public static final int STYLE_SOLID = 0;
+    /**
+     * Ripple style where a circle shape with a patterned,
+     * noisy interior expands from the hotspot to the bounds".
+     * @see #setRippleStyle(int)
+     */
+    public static final int STYLE_PATTERNED = 1;
+
+    /**
+     * Ripple drawing style
+     * @hide
+     */
+    @Retention(SOURCE)
+    @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
+    @IntDef({STYLE_SOLID, STYLE_PATTERNED})
+    public @interface RippleStyle {
+    }
+
+    private static final int BACKGROUND_OPACITY_DURATION = 80;
     private static final int MASK_UNKNOWN = -1;
     private static final int MASK_NONE = 0;
     private static final int MASK_CONTENT = 1;
@@ -109,6 +158,7 @@
 
     /** The maximum number of ripples supported. */
     private static final int MAX_RIPPLES = 10;
+    private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
 
     private final Rect mTempRect = new Rect();
 
@@ -172,6 +222,14 @@
      */
     private boolean mForceSoftware;
 
+    // Patterned
+    private float mTargetBackgroundOpacity;
+    private ValueAnimator mBackgroundAnimation;
+    private float mBackgroundOpacity;
+    private boolean mRunBackgroundAnimation;
+    private boolean mExitingAnimation;
+    private ArrayList<RippleAnimationSession> mRunningAnimations = new ArrayList<>();
+
     /**
      * Constructor used for drawable inflation.
      */
@@ -235,7 +293,7 @@
             Arrays.fill(ripples, 0, count, null);
         }
         mExitingRipplesCount = 0;
-
+        mExitingAnimation = true;
         // Always draw an additional "clean" frame after canceling animations.
         invalidateSelf(false);
     }
@@ -276,21 +334,37 @@
     private void setRippleActive(boolean active) {
         if (mRippleActive != active) {
             mRippleActive = active;
+        }
+        if (mState.mRippleStyle == STYLE_SOLID) {
             if (active) {
                 tryRippleEnter();
             } else {
                 tryRippleExit();
             }
+        } else {
+            if (active) {
+                startPatternedAnimation();
+            } else {
+                exitPatternedAnimation();
+            }
         }
     }
 
     private void setBackgroundActive(boolean hovered, boolean focused, boolean pressed) {
-        if (mBackground == null && (hovered || focused)) {
-            mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
-            mBackground.setup(mState.mMaxRadius, mDensity);
-        }
-        if (mBackground != null) {
-            mBackground.setState(focused, hovered, pressed);
+        if (mState.mRippleStyle == STYLE_SOLID) {
+            if (mBackground == null && (hovered || focused)) {
+                mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
+                mBackground.setup(mState.mMaxRadius, mDensity);
+            }
+            if (mBackground != null) {
+                mBackground.setState(focused, hovered, pressed);
+            }
+        } else {
+            if (focused || hovered) {
+                enterPatternedBackgroundAnimation(focused, hovered);
+            } else {
+                exitPatternedBackgroundAnimation();
+            }
         }
     }
 
@@ -317,6 +391,9 @@
             mRipple.onBoundsChange();
         }
 
+        mState.mMaxRadius = mState.mMaxRadius <= 0 && mState.mRippleStyle != STYLE_SOLID
+                ? (int) computeRadius()
+                : mState.mMaxRadius;
         invalidateSelf();
     }
 
@@ -330,7 +407,11 @@
             // If we just became visible, ensure the background and ripple
             // visibilities are consistent with their internal states.
             if (mRippleActive) {
-                tryRippleEnter();
+                if (mState.mRippleStyle == STYLE_SOLID) {
+                    tryRippleEnter();
+                } else {
+                    invalidateSelf();
+                }
             }
 
             // Skip animations, just show the correct final states.
@@ -489,6 +570,8 @@
 
         mState.mMaxRadius = a.getDimensionPixelSize(
                 R.styleable.RippleDrawable_radius, mState.mMaxRadius);
+
+        mState.mRippleStyle = a.getInteger(R.styleable.RippleDrawable_rippleStyle, STYLE_SOLID);
     }
 
     private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
@@ -535,9 +618,9 @@
 
     @Override
     public void setHotspot(float x, float y) {
+        mPendingX = x;
+        mPendingY = y;
         if (mRipple == null || mBackground == null) {
-            mPendingX = x;
-            mPendingY = y;
             mHasPending = true;
         }
 
@@ -665,6 +748,14 @@
      */
     @Override
     public void draw(@NonNull Canvas canvas) {
+        if (mState.mRippleStyle == STYLE_SOLID) {
+            drawSolid(canvas);
+        } else {
+            drawPatterned(canvas);
+        }
+    }
+
+    private void drawSolid(Canvas canvas) {
         pruneRipples();
 
         // Clip to the dirty bounds, which will be the drawable bounds if we
@@ -681,6 +772,178 @@
         canvas.restoreToCount(saveCount);
     }
 
+    private void exitPatternedBackgroundAnimation() {
+        mTargetBackgroundOpacity = 0;
+        if (mBackgroundAnimation != null) mBackgroundAnimation.cancel();
+        // after cancel
+        mRunBackgroundAnimation = true;
+        invalidateSelf(false);
+    }
+
+    private void startPatternedAnimation() {
+        mRippleActive = true;
+        invalidateSelf(false);
+    }
+
+    private void exitPatternedAnimation() {
+        mExitingAnimation = true;
+        invalidateSelf(false);
+    }
+
+    private void enterPatternedBackgroundAnimation(boolean focused, boolean hovered) {
+        mBackgroundOpacity = 0;
+        mTargetBackgroundOpacity = focused ? .6f : hovered ? .2f : 0f;
+        if (mBackgroundAnimation != null) mBackgroundAnimation.cancel();
+        // after cancel
+        mRunBackgroundAnimation = true;
+        invalidateSelf(false);
+    }
+
+    private void startBackgroundAnimation() {
+        mRunBackgroundAnimation = false;
+        mBackgroundAnimation = ValueAnimator.ofFloat(mBackgroundOpacity, mTargetBackgroundOpacity);
+        mBackgroundAnimation.setInterpolator(LINEAR_INTERPOLATOR);
+        mBackgroundAnimation.setDuration(BACKGROUND_OPACITY_DURATION);
+        mBackgroundAnimation.addUpdateListener(update -> {
+            mBackgroundOpacity = (float) update.getAnimatedValue();
+            invalidateSelf(false);
+        });
+        mBackgroundAnimation.start();
+    }
+
+    private void drawPatterned(@NonNull Canvas canvas) {
+        final Rect bounds = getBounds();
+        final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
+        boolean useCanvasProps = shouldUseCanvasProps(canvas);
+        boolean changedHotspotBounds = !bounds.equals(mHotspotBounds);
+        if (isBounded()) {
+            canvas.clipRect(mHotspotBounds);
+        }
+        float x, y;
+        if (changedHotspotBounds) {
+            x = mHotspotBounds.exactCenterX();
+            y = mHotspotBounds.exactCenterY();
+            useCanvasProps = false;
+        } else {
+            x = mPendingX;
+            y = mPendingY;
+        }
+        boolean shouldAnimate = mRippleActive;
+        boolean shouldExit = mExitingAnimation;
+        mRippleActive = false;
+        mExitingAnimation = false;
+        getRipplePaint();
+        drawContent(canvas);
+        drawPatternedBackground(canvas);
+        if (shouldAnimate && mRunningAnimations.size() <= MAX_RIPPLES) {
+            RippleAnimationSession.AnimationProperties<Float, Paint> properties =
+                    createAnimationProperties(x, y);
+            mRunningAnimations.add(new RippleAnimationSession(properties, !useCanvasProps)
+                    .setOnAnimationUpdated(useCanvasProps ? null : () -> invalidateSelf(false))
+                    .setOnSessionEnd(session -> {
+                        mRunningAnimations.remove(session);
+                    })
+                    .setForceSoftwareAnimation(!useCanvasProps)
+                    .enter(canvas));
+        }
+        if (shouldExit) {
+            for (int i = 0; i < mRunningAnimations.size(); i++) {
+                RippleAnimationSession s = mRunningAnimations.get(i);
+                s.exit(canvas);
+            }
+        }
+        for (int i = 0; i < mRunningAnimations.size(); i++) {
+            RippleAnimationSession s = mRunningAnimations.get(i);
+            if (useCanvasProps) {
+                RippleAnimationSession.AnimationProperties<CanvasProperty<Float>,
+                        CanvasProperty<Paint>>
+                        p = s.getCanvasProperties();
+                RecordingCanvas can = (RecordingCanvas) canvas;
+                can.drawRipple(p.getX(), p.getY(), p.getMaxRadius(), p.getPaint(),
+                        p.getProgress(), p.getShader());
+            } else {
+                RippleAnimationSession.AnimationProperties<Float, Paint> p =
+                        s.getProperties();
+                float posX, posY;
+                if (changedHotspotBounds) {
+                    posX = x;
+                    posY = y;
+                    if (p.getPaint().getShader() instanceof RippleShader) {
+                        RippleShader shader = (RippleShader) p.getPaint().getShader();
+                        shader.setOrigin(posX, posY);
+                    }
+                } else {
+                    posX = p.getX();
+                    posY = p.getY();
+                }
+                float radius = p.getMaxRadius();
+                canvas.drawCircle(posX, posY, radius, p.getPaint());
+            }
+        }
+        canvas.restoreToCount(saveCount);
+    }
+
+    private void drawPatternedBackground(Canvas c) {
+        if (mRunBackgroundAnimation) {
+            startBackgroundAnimation();
+        }
+        if (mBackgroundOpacity == 0) return;
+        Paint p = mRipplePaint;
+        float newOpacity = mBackgroundOpacity;
+        final int origAlpha = p.getAlpha();
+        final int alpha = Math.min((int) (origAlpha * newOpacity + 0.5f), 255);
+        if (alpha > 0) {
+            ColorFilter origFilter = p.getColorFilter();
+            p.setColorFilter(mMaskColorFilter);
+            p.setAlpha(alpha);
+            Rect b = mHotspotBounds;
+            c.drawCircle(b.centerX(), b.centerY(), mState.mMaxRadius, p);
+            p.setAlpha(origAlpha);
+            p.setColorFilter(origFilter);
+        }
+    }
+
+    private float computeRadius() {
+        Rect b = getDirtyBounds();
+        float gap = 0;
+        float radius = (float) Math.sqrt(b.width() * b.width() + b.height() * b.height()) / 2 + gap;
+        return radius;
+    }
+
+    @NonNull
+    private RippleAnimationSession.AnimationProperties<Float, Paint> createAnimationProperties(
+            float x, float y) {
+        Paint p = new Paint(mRipplePaint);
+        float radius = mState.mMaxRadius;
+        RippleAnimationSession.AnimationProperties<Float, Paint> properties;
+        RippleShader shader = new RippleShader();
+        int color = mMaskColorFilter == null
+                ? mState.mColor.getColorForState(getState(), Color.BLACK)
+                : mMaskColorFilter.getColor();
+        color = color | 0xFF000000;
+        shader.setColor(color);
+        shader.setOrigin(x, y);
+        shader.setRadius(radius);
+        shader.setProgress(.0f);
+        properties = new RippleAnimationSession.AnimationProperties<>(
+                x, y, radius, p, 0f,
+                shader);
+        if (mMaskShader == null) {
+            shader.setHasMask(false);
+        } else {
+            shader.setShader(mMaskShader);
+            shader.setHasMask(true);
+        }
+        p.setShader(shader);
+        p.setColorFilter(null);
+        p.setColor(color);
+        return properties;
+    }
+
+    private boolean shouldUseCanvasProps(Canvas c) {
+        return !mForceSoftware && c.isHardwareAccelerated();
+    }
+
     @Override
     public void invalidateSelf() {
         invalidateSelf(true);
@@ -774,18 +1037,23 @@
         // Draw the appropriate mask anchored to (0,0).
         final int left = bounds.left;
         final int top = bounds.top;
-        mMaskCanvas.translate(-left, -top);
+        if (mState.mRippleStyle == STYLE_SOLID) {
+            mMaskCanvas.translate(-left, -top);
+        }
         if (maskType == MASK_EXPLICIT) {
             drawMask(mMaskCanvas);
         } else if (maskType == MASK_CONTENT) {
             drawContent(mMaskCanvas);
         }
-        mMaskCanvas.translate(left, top);
+        if (mState.mRippleStyle == STYLE_SOLID) {
+            mMaskCanvas.translate(left, top);
+        }
     }
 
     private int getMaskType() {
         if (mRipple == null && mExitingRipplesCount <= 0
-                && (mBackground == null || !mBackground.isVisible())) {
+                && (mBackground == null || !mBackground.isVisible())
+                && mState.mRippleStyle == STYLE_SOLID) {
             // We might need a mask later.
             return MASK_UNKNOWN;
         }
@@ -874,7 +1142,7 @@
         updateMaskShaderIfNeeded();
 
         // Position the shader to account for canvas translation.
-        if (mMaskShader != null) {
+        if (mMaskShader != null && mState.mRippleStyle == STYLE_SOLID) {
             final Rect bounds = getBounds();
             mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
             mMaskShader.setLocalMatrix(mMaskMatrix);
@@ -973,6 +1241,31 @@
         return this;
     }
 
+    /**
+     * Sets the visual style of the ripple.
+     *
+     * @see #STYLE_SOLID
+     * @see #STYLE_PATTERNED
+     *
+     * @param style The style of the ripple
+     */
+    public void setRippleStyle(@RippleStyle int style) throws IllegalArgumentException {
+        if (style == STYLE_SOLID || style == STYLE_PATTERNED) {
+            mState.mRippleStyle = style;
+        } else {
+            throw new IllegalArgumentException("Invalid style value " + style);
+        }
+    }
+
+    /**
+     * Get the current ripple style
+     * @return Ripple style
+     */
+    public @RippleStyle int getRippleStyle() {
+        return mState.mRippleStyle;
+    }
+
+
     @Override
     RippleState createConstantState(LayerState state, Resources res) {
         return new RippleState(state, this, res);
@@ -983,6 +1276,7 @@
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
         ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
         int mMaxRadius = RADIUS_AUTO;
+        int mRippleStyle = STYLE_SOLID;
 
         public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
             super(orig, owner, res);
@@ -992,6 +1286,7 @@
                 mTouchThemeAttrs = origs.mTouchThemeAttrs;
                 mColor = origs.mColor;
                 mMaxRadius = origs.mMaxRadius;
+                mRippleStyle = origs.mRippleStyle;
 
                 if (origs.mDensity != mDensity) {
                     applyDensityScaling(orig.mDensity, mDensity);
diff --git a/graphics/java/android/graphics/drawable/RippleShader.java b/graphics/java/android/graphics/drawable/RippleShader.java
new file mode 100644
index 0000000..500efdd
--- /dev/null
+++ b/graphics/java/android/graphics/drawable/RippleShader.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 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.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.graphics.Color;
+import android.graphics.RuntimeShader;
+import android.graphics.Shader;
+
+final class RippleShader extends RuntimeShader {
+    private static final String SHADER = "uniform float2 in_origin;\n"
+            + "uniform float in_maxRadius;\n"
+            + "uniform float in_progress;\n"
+            + "uniform float in_hasMask;\n"
+            + "uniform float4 in_color;\n"
+            + "uniform shader in_shader;\n"
+            + "float dist2(float2 p0, float2 pf) { return sqrt((pf.x - p0.x) * (pf.x - p0.x) + "
+            + "(pf.y - p0.y) * (pf.y - p0.y)); }\n"
+            + "float mod2(float a, float b) { return a - (b * floor(a / b)); }\n"
+            + "float rand(float2 src) { return fract(sin(dot(src.xy, float2(12.9898, 78.233))) * "
+            + "43758.5453123); }\n"
+            + "float4 main(float2 p)\n"
+            + "{\n"
+            + "    float fraction = in_progress;\n"
+            + "    float2 fragCoord = p;//sk_FragCoord.xy;\n"
+            + "    float maxDist = in_maxRadius;\n"
+            + "    float fragDist = dist2(in_origin, fragCoord.xy);\n"
+            + "    float circleRadius = maxDist * fraction;\n"
+            + "    float colorVal = (fragDist - circleRadius) / maxDist;\n"
+            + "    float d = fragDist < circleRadius \n"
+            + "        ? 1. - abs(colorVal * 3. * smoothstep(0., 1., fraction)) \n"
+            + "        : 1. - abs(colorVal * 5.);\n"
+            + "    d = smoothstep(0., 1., d);\n"
+            + "    float divider = 2.;\n"
+            + "    float x = floor(fragCoord.x / divider);\n"
+            + "    float y = floor(fragCoord.y / divider);\n"
+            + "    float density = .95;\n"
+            + "    d = rand(float2(x, y)) > density ? d : d * .2;\n"
+            + "    d = d * rand(float2(fraction, x * y));\n"
+            + "    float alpha = 1. - pow(fraction, 2.);\n"
+            + "    if (in_hasMask != 0.) {return sample(in_shader).a * in_color * d * alpha;}\n"
+            + "    return in_color * d * alpha;\n"
+            + "}\n";
+
+    RippleShader() {
+        super(SHADER, false);
+    }
+
+    public void setShader(@NonNull Shader s) {
+        setInputShader("in_shader", s);
+    }
+
+    public void setRadius(float radius) {
+        setUniform("in_maxRadius", radius);
+    }
+
+    public void setOrigin(float x, float y) {
+        setUniform("in_origin", new float[] {x, y});
+    }
+
+    public void setProgress(float progress) {
+        setUniform("in_progress", progress);
+    }
+
+    public void setHasMask(boolean hasMask) {
+        setUniform("in_hasMask", hasMask ? 1 : 0);
+    }
+
+    public void setColor(@ColorInt int colorIn) {
+        Color color = Color.valueOf(colorIn);
+        this.setUniform("in_color", new float[] {color.red(),
+                color.green(), color.blue(), color.alpha()});
+    }
+}
diff --git a/libs/hwui/Animator.cpp b/libs/hwui/Animator.cpp
index 74cf1fd..20a8a8c 100644
--- a/libs/hwui/Animator.cpp
+++ b/libs/hwui/Animator.cpp
@@ -160,10 +160,6 @@
         }
     }
 
-    if (!mHasStartValue) {
-        doSetStartValue(getValue(mTarget));
-    }
-
     if (!mStagingRequests.empty()) {
         // No interpolator was set, use the default
         if (mPlayState == PlayState::NotStarted && !mInterpolator) {
@@ -270,9 +266,11 @@
     // to call setValue even if the animation isn't yet running or is still
     // being delayed as we need to override the staging value
     if (playTime < 0) {
-        setValue(mTarget, mFromValue);
         return false;
     }
+    if (!this->mHasStartValue) {
+        doSetStartValue(getValue(mTarget));
+    }
 
     float fraction = 1.0f;
     if ((mPlayState == PlayState::Running || mPlayState == PlayState::Reversing) && mDuration > 0) {