1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.graphics.drawable;
18 
19 import android.annotation.TestApi;
20 
21 import static java.lang.annotation.ElementType.FIELD;
22 import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
23 import static java.lang.annotation.ElementType.METHOD;
24 import static java.lang.annotation.ElementType.PARAMETER;
25 import static java.lang.annotation.RetentionPolicy.SOURCE;
26 
27 import android.animation.ValueAnimator;
28 import android.annotation.IntDef;
29 import android.annotation.NonNull;
30 import android.annotation.Nullable;
31 import android.compat.annotation.UnsupportedAppUsage;
32 import android.content.pm.ActivityInfo.Config;
33 import android.content.res.ColorStateList;
34 import android.content.res.Resources;
35 import android.content.res.Resources.Theme;
36 import android.content.res.TypedArray;
37 import android.graphics.Bitmap;
38 import android.graphics.BitmapShader;
39 import android.graphics.Canvas;
40 import android.graphics.CanvasProperty;
41 import android.graphics.Color;
42 import android.graphics.ColorFilter;
43 import android.graphics.Matrix;
44 import android.graphics.Outline;
45 import android.graphics.Paint;
46 import android.graphics.PixelFormat;
47 import android.graphics.PorterDuff;
48 import android.graphics.PorterDuffColorFilter;
49 import android.graphics.RecordingCanvas;
50 import android.graphics.Rect;
51 import android.graphics.Shader;
52 import android.os.Build;
53 import android.os.Looper;
54 import android.util.AttributeSet;
55 import android.util.Log;
56 import android.view.animation.AnimationUtils;
57 import android.view.animation.LinearInterpolator;
58 
59 import com.android.internal.R;
60 
61 import org.xmlpull.v1.XmlPullParser;
62 import org.xmlpull.v1.XmlPullParserException;
63 
64 import java.io.IOException;
65 import java.lang.annotation.Retention;
66 import java.lang.annotation.Target;
67 import java.util.ArrayList;
68 import java.util.Arrays;
69 
70 /**
71  * Drawable that shows a ripple effect in response to state changes. The
72  * anchoring position of the ripple for a given state may be specified by
73  * calling {@link #setHotspot(float, float)} with the corresponding state
74  * attribute identifier.
75  * <p>
76  * A touch feedback drawable may contain multiple child layers, including a
77  * special mask layer that is not drawn to the screen. A single layer may be
78  * set as the mask from XML by specifying its {@code android:id} value as
79  * {@link android.R.id#mask}. At run time, a single layer may be set as the
80  * mask using {@code setId(..., android.R.id.mask)} or an existing mask layer
81  * may be replaced using {@code setDrawableByLayerId(android.R.id.mask, ...)}.
82  * <pre>
83  * <code>&lt;!-- A red ripple masked against an opaque rectangle. -->
84  * &lt;ripple android:color="#ffff0000">
85  *   &lt;item android:id="@android:id/mask"
86  *         android:drawable="@android:color/white" />
87  * &lt;/ripple></code>
88  * </pre>
89  * <p>
90  * If a mask layer is set, the ripple effect will be masked against that layer
91  * before it is drawn over the composite of the remaining child layers.
92  * <p>
93  * If no mask layer is set, the ripple effect is masked against the composite
94  * of the child layers.
95  * <pre>
96  * <code>&lt;!-- A green ripple drawn atop a black rectangle. -->
97  * &lt;ripple android:color="#ff00ff00">
98  *   &lt;item android:drawable="@android:color/black" />
99  * &lt;/ripple>
100  *
101  * &lt;!-- A blue ripple drawn atop a drawable resource. -->
102  * &lt;ripple android:color="#ff0000ff">
103  *   &lt;item android:drawable="@drawable/my_drawable" />
104  * &lt;/ripple></code>
105  * </pre>
106  * <p>
107  * If no child layers or mask is specified and the ripple is set as a View
108  * background, the ripple will be drawn atop the first available parent
109  * background within the View's hierarchy. In this case, the drawing region
110  * may extend outside of the Drawable bounds.
111  * <pre>
112  * <code>&lt;!-- An unbounded red ripple. -->
113  * &lt;ripple android:color="#ffff0000" /></code>
114  * </pre>
115  *
116  * @attr ref android.R.styleable#RippleDrawable_color
117  */
118 public class RippleDrawable extends LayerDrawable {
119     private static final String TAG = "RippleDrawable";
120     /**
121      * Radius value that specifies the ripple radius should be computed based
122      * on the size of the ripple's container.
123      */
124     public static final int RADIUS_AUTO = -1;
125 
126     /**
127      * Ripple style where a solid circle is drawn. This is also the default style
128      * @see #setRippleStyle(int)
129      * @hide
130      */
131     public static final int STYLE_SOLID = 0;
132     /**
133      * Ripple style where a circle shape with a patterned,
134      * noisy interior expands from the hotspot to the bounds".
135      * @see #setRippleStyle(int)
136      * @hide
137      */
138     public static final int STYLE_PATTERNED = 1;
139 
140     /**
141      * Ripple drawing style
142      * @hide
143      */
144     @Retention(SOURCE)
145     @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
146     @IntDef({STYLE_SOLID, STYLE_PATTERNED})
147     public @interface RippleStyle {
148     }
149 
150     private static final int BACKGROUND_OPACITY_DURATION = 80;
151     private static final int MASK_UNKNOWN = -1;
152     private static final int MASK_NONE = 0;
153     private static final int MASK_CONTENT = 1;
154     private static final int MASK_EXPLICIT = 2;
155 
156     /** The maximum number of ripples supported. */
157     private static final int MAX_RIPPLES = 10;
158     private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
159     private static final int DEFAULT_EFFECT_COLOR = 0x8dffffff;
160     /** Temporary flag for teamfood. **/
161     private static final boolean FORCE_PATTERNED_STYLE = true;
162 
163     private final Rect mTempRect = new Rect();
164 
165     /** Current ripple effect bounds, used to constrain ripple effects. */
166     private final Rect mHotspotBounds = new Rect();
167 
168     /** Current drawing bounds, used to compute dirty region. */
169     private final Rect mDrawingBounds = new Rect();
170 
171     /** Current dirty bounds, union of current and previous drawing bounds. */
172     private final Rect mDirtyBounds = new Rect();
173 
174     /** Mirrors mLayerState with some extra information. */
175     @UnsupportedAppUsage(trackingBug = 175939224)
176     private RippleState mState;
177 
178     /** The masking layer, e.g. the layer with id R.id.mask. */
179     private Drawable mMask;
180 
181     /** The current background. May be actively animating or pending entry. */
182     private RippleBackground mBackground;
183 
184     private Bitmap mMaskBuffer;
185     private BitmapShader mMaskShader;
186     private Canvas mMaskCanvas;
187     private Matrix mMaskMatrix;
188     private PorterDuffColorFilter mMaskColorFilter;
189     private PorterDuffColorFilter mFocusColorFilter;
190     private boolean mHasValidMask;
191 
192     /** The current ripple. May be actively animating or pending entry. */
193     private RippleForeground mRipple;
194 
195     /** Whether we expect to draw a ripple when visible. */
196     private boolean mRippleActive;
197 
198     // Hotspot coordinates that are awaiting activation.
199     private float mPendingX;
200     private float mPendingY;
201     private boolean mHasPending;
202 
203     /**
204      * Lazily-created array of actively animating ripples. Inactive ripples are
205      * pruned during draw(). The locations of these will not change.
206      */
207     private RippleForeground[] mExitingRipples;
208     private int mExitingRipplesCount = 0;
209 
210     /** Paint used to control appearance of ripples. */
211     private Paint mRipplePaint;
212 
213     /** Target density of the display into which ripples are drawn. */
214     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
215     private int mDensity;
216 
217     /** Whether bounds are being overridden. */
218     private boolean mOverrideBounds;
219 
220     /**
221      * If set, force all ripple animations to not run on RenderThread, even if it would be
222      * available.
223      */
224     private boolean mForceSoftware;
225 
226     // Patterned
227     private boolean mAddRipple = false;
228     private float mTargetBackgroundOpacity;
229     private ValueAnimator mBackgroundAnimation;
230     private float mBackgroundOpacity;
231     private boolean mRunBackgroundAnimation;
232     private boolean mExitingAnimation;
233     private ArrayList<RippleAnimationSession> mRunningAnimations = new ArrayList<>();
234 
235     /**
236      * Constructor used for drawable inflation.
237      */
RippleDrawable()238     RippleDrawable() {
239         this(new RippleState(null, null, null), null);
240     }
241 
242     /**
243      * Creates a new ripple drawable with the specified ripple color and
244      * optional content and mask drawables.
245      *
246      * @param color The ripple color
247      * @param content The content drawable, may be {@code null}
248      * @param mask The mask drawable, may be {@code null}
249      */
RippleDrawable(@onNull ColorStateList color, @Nullable Drawable content, @Nullable Drawable mask)250     public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content,
251             @Nullable Drawable mask) {
252         this(new RippleState(null, null, null), null);
253 
254         if (color == null) {
255             throw new IllegalArgumentException("RippleDrawable requires a non-null color");
256         }
257 
258         if (content != null) {
259             addLayer(content, null, 0, 0, 0, 0, 0);
260         }
261 
262         if (mask != null) {
263             addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0);
264         }
265 
266         setColor(color);
267         ensurePadding();
268         refreshPadding();
269         updateLocalState();
270     }
271 
272     @Override
jumpToCurrentState()273     public void jumpToCurrentState() {
274         super.jumpToCurrentState();
275 
276         if (mRipple != null) {
277             mRipple.end();
278         }
279 
280         if (mBackground != null) {
281             mBackground.jumpToFinal();
282         }
283 
284         cancelExitingRipples();
285         endPatternedAnimations();
286     }
287 
endPatternedAnimations()288     private void endPatternedAnimations() {
289         for (int i = 0; i < mRunningAnimations.size(); i++) {
290             RippleAnimationSession session = mRunningAnimations.get(i);
291             session.end();
292         }
293         mRunningAnimations.clear();
294     }
295 
cancelExitingRipples()296     private void cancelExitingRipples() {
297         final int count = mExitingRipplesCount;
298         final RippleForeground[] ripples = mExitingRipples;
299         for (int i = 0; i < count; i++) {
300             ripples[i].end();
301         }
302 
303         if (ripples != null) {
304             Arrays.fill(ripples, 0, count, null);
305         }
306         mExitingRipplesCount = 0;
307         // Always draw an additional "clean" frame after canceling animations.
308         invalidateSelf(false);
309     }
310 
311     @Override
getOpacity()312     public int getOpacity() {
313         // Worst-case scenario.
314         return PixelFormat.TRANSLUCENT;
315     }
316 
317     @Override
onStateChange(int[] stateSet)318     protected boolean onStateChange(int[] stateSet) {
319         final boolean changed = super.onStateChange(stateSet);
320 
321         boolean enabled = false;
322         boolean pressed = false;
323         boolean focused = false;
324         boolean hovered = false;
325         boolean windowFocused = false;
326 
327         for (int state : stateSet) {
328             if (state == R.attr.state_enabled) {
329                 enabled = true;
330             } else if (state == R.attr.state_focused) {
331                 focused = true;
332             } else if (state == R.attr.state_pressed) {
333                 pressed = true;
334             } else if (state == R.attr.state_hovered) {
335                 hovered = true;
336             } else if (state == R.attr.state_window_focused) {
337                 windowFocused = true;
338             }
339         }
340         setRippleActive(enabled && pressed);
341         setBackgroundActive(hovered, focused, pressed, windowFocused);
342 
343         return changed;
344     }
345 
setRippleActive(boolean active)346     private void setRippleActive(boolean active) {
347         if (mRippleActive != active) {
348             mRippleActive = active;
349             if (mState.mRippleStyle == STYLE_SOLID) {
350                 if (active) {
351                     tryRippleEnter();
352                 } else {
353                     tryRippleExit();
354                 }
355             } else {
356                 if (active) {
357                     startPatternedAnimation();
358                 } else {
359                     exitPatternedAnimation();
360                 }
361             }
362         }
363     }
364 
365     /** @hide */
366     @TestApi
setBackgroundActive(boolean hovered, boolean focused, boolean pressed, boolean windowFocused)367     public void setBackgroundActive(boolean hovered, boolean focused, boolean pressed,
368             boolean windowFocused) {
369         if (mState.mRippleStyle == STYLE_SOLID) {
370             if (mBackground == null && (hovered || focused)) {
371                 mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
372                 mBackground.setup(mState.mMaxRadius, mDensity);
373             }
374             if (mBackground != null) {
375                 mBackground.setState(focused, hovered, pressed);
376             }
377         } else {
378             if (focused || hovered) {
379                 if (!pressed) {
380                     enterPatternedBackgroundAnimation(focused, hovered, windowFocused);
381                 }
382             } else {
383                 exitPatternedBackgroundAnimation();
384             }
385         }
386     }
387 
388     @Override
onBoundsChange(Rect bounds)389     protected void onBoundsChange(Rect bounds) {
390         super.onBoundsChange(bounds);
391 
392         if (!mOverrideBounds) {
393             mHotspotBounds.set(bounds);
394             onHotspotBoundsChanged();
395         }
396 
397         final int count = mExitingRipplesCount;
398         final RippleForeground[] ripples = mExitingRipples;
399         for (int i = 0; i < count; i++) {
400             ripples[i].onBoundsChange();
401         }
402 
403         if (mBackground != null) {
404             mBackground.onBoundsChange();
405         }
406 
407         if (mRipple != null) {
408             mRipple.onBoundsChange();
409         }
410         invalidateSelf();
411     }
412 
413     @Override
setVisible(boolean visible, boolean restart)414     public boolean setVisible(boolean visible, boolean restart) {
415         final boolean changed = super.setVisible(visible, restart);
416 
417         if (!visible) {
418             clearHotspots();
419         } else if (changed) {
420             // If we just became visible, ensure the background and ripple
421             // visibilities are consistent with their internal states.
422             if (mRippleActive) {
423                 if (mState.mRippleStyle == STYLE_SOLID) {
424                     tryRippleEnter();
425                 } else {
426                     invalidateSelf();
427                 }
428             }
429 
430             // Skip animations, just show the correct final states.
431             jumpToCurrentState();
432         }
433 
434         return changed;
435     }
436 
437     /**
438      * @hide
439      */
440     @Override
isProjected()441     public boolean isProjected() {
442         // If the layer is bounded, then we don't need to project.
443         if (isBounded()) {
444             return false;
445         }
446 
447         // Otherwise, if the maximum radius is contained entirely within the
448         // bounds then we don't need to project. This is sort of a hack to
449         // prevent check box ripples from being projected across the edges of
450         // scroll views. It does not impact rendering performance, and it can
451         // be removed once we have better handling of projection in scrollable
452         // views.
453         final int radius = mState.mMaxRadius;
454         final Rect drawableBounds = getBounds();
455         final Rect hotspotBounds = mHotspotBounds;
456         if (radius != RADIUS_AUTO
457                 && radius <= hotspotBounds.width() / 2
458                 && radius <= hotspotBounds.height() / 2
459                 && (drawableBounds.equals(hotspotBounds)
460                         || drawableBounds.contains(hotspotBounds))) {
461             return false;
462         }
463 
464         return true;
465     }
466 
isBounded()467     private boolean isBounded() {
468         return getNumberOfLayers() > 0;
469     }
470 
471     @Override
isStateful()472     public boolean isStateful() {
473         return true;
474     }
475 
476     @Override
hasFocusStateSpecified()477     public boolean hasFocusStateSpecified() {
478         return true;
479     }
480 
481     /**
482      * Sets the ripple color.
483      *
484      * @param color Ripple color as a color state list.
485      *
486      * @attr ref android.R.styleable#RippleDrawable_color
487      */
setColor(@onNull ColorStateList color)488     public void setColor(@NonNull ColorStateList color) {
489         if (color == null) {
490             throw new IllegalArgumentException("color cannot be null");
491         }
492         mState.mColor = color;
493         invalidateSelf(false);
494     }
495 
496     /**
497      * Sets the ripple effect color.
498      *
499      * @param color Ripple color as a color state list.
500      *
501      * @attr ref android.R.styleable#RippleDrawable_effectColor
502      */
setEffectColor(@onNull ColorStateList color)503     public void setEffectColor(@NonNull ColorStateList color) {
504         if (color == null) {
505             throw new IllegalArgumentException("color cannot be null");
506         }
507         mState.mEffectColor = color;
508         invalidateSelf(false);
509     }
510 
511     /**
512      * @return The ripple effect color as a color state list.
513      */
getEffectColor()514     public @NonNull ColorStateList getEffectColor() {
515         return mState.mEffectColor;
516     }
517 
518     /**
519      * Sets the radius in pixels of the fully expanded ripple.
520      *
521      * @param radius ripple radius in pixels, or {@link #RADIUS_AUTO} to
522      *               compute the radius based on the container size
523      * @attr ref android.R.styleable#RippleDrawable_radius
524      */
setRadius(int radius)525     public void setRadius(int radius) {
526         mState.mMaxRadius = radius;
527         invalidateSelf(false);
528     }
529 
530     /**
531      * @return the radius in pixels of the fully expanded ripple if an explicit
532      *         radius has been set, or {@link #RADIUS_AUTO} if the radius is
533      *         computed based on the container size
534      * @attr ref android.R.styleable#RippleDrawable_radius
535      */
getRadius()536     public int getRadius() {
537         return mState.mMaxRadius;
538     }
539 
540     @Override
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)541     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
542             @NonNull AttributeSet attrs, @Nullable Theme theme)
543             throws XmlPullParserException, IOException {
544         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
545 
546         // Force padding default to STACK before inflating.
547         setPaddingMode(PADDING_MODE_STACK);
548 
549         // Inflation will advance the XmlPullParser and AttributeSet.
550         super.inflate(r, parser, attrs, theme);
551 
552         updateStateFromTypedArray(a);
553         verifyRequiredAttributes(a);
554         a.recycle();
555 
556         updateLocalState();
557     }
558 
559     @Override
setDrawableByLayerId(int id, Drawable drawable)560     public boolean setDrawableByLayerId(int id, Drawable drawable) {
561         if (super.setDrawableByLayerId(id, drawable)) {
562             if (id == R.id.mask) {
563                 mMask = drawable;
564                 mHasValidMask = false;
565             }
566 
567             return true;
568         }
569 
570         return false;
571     }
572 
573     /**
574      * Specifies how layer padding should affect the bounds of subsequent
575      * layers. The default and recommended value for RippleDrawable is
576      * {@link #PADDING_MODE_STACK}.
577      *
578      * @param mode padding mode, one of:
579      *            <ul>
580      *            <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
581      *            padding of the previous layer
582      *            <li>{@link #PADDING_MODE_STACK} to stack each layer directly
583      *            atop the previous layer
584      *            </ul>
585      * @see #getPaddingMode()
586      */
587     @Override
setPaddingMode(int mode)588     public void setPaddingMode(int mode) {
589         super.setPaddingMode(mode);
590     }
591 
592     /**
593      * Initializes the constant state from the values in the typed array.
594      */
updateStateFromTypedArray(@onNull TypedArray a)595     private void updateStateFromTypedArray(@NonNull TypedArray a) throws XmlPullParserException {
596         final RippleState state = mState;
597 
598         // Account for any configuration changes.
599         state.mChangingConfigurations |= a.getChangingConfigurations();
600 
601         // Extract the theme attributes, if any.
602         state.mTouchThemeAttrs = a.extractThemeAttrs();
603 
604         final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
605         if (color != null) {
606             mState.mColor = color;
607         }
608 
609         final ColorStateList effectColor =
610                 a.getColorStateList(R.styleable.RippleDrawable_effectColor);
611         if (effectColor != null) {
612             mState.mEffectColor = effectColor;
613         }
614 
615         mState.mMaxRadius = a.getDimensionPixelSize(
616                 R.styleable.RippleDrawable_radius, mState.mMaxRadius);
617     }
618 
verifyRequiredAttributes(@onNull TypedArray a)619     private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
620         if (mState.mColor == null && (mState.mTouchThemeAttrs == null
621                 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
622             throw new XmlPullParserException(a.getPositionDescription() +
623                     ": <ripple> requires a valid color attribute");
624         }
625     }
626 
627     @Override
applyTheme(@onNull Theme t)628     public void applyTheme(@NonNull Theme t) {
629         super.applyTheme(t);
630 
631         final RippleState state = mState;
632         if (state == null) {
633             return;
634         }
635 
636         if (state.mTouchThemeAttrs != null) {
637             final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
638                     R.styleable.RippleDrawable);
639             try {
640                 updateStateFromTypedArray(a);
641                 verifyRequiredAttributes(a);
642             } catch (XmlPullParserException e) {
643                 rethrowAsRuntimeException(e);
644             } finally {
645                 a.recycle();
646             }
647         }
648 
649         if (state.mColor != null && state.mColor.canApplyTheme()) {
650             state.mColor = state.mColor.obtainForTheme(t);
651         }
652 
653         updateLocalState();
654     }
655 
656     @Override
canApplyTheme()657     public boolean canApplyTheme() {
658         return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
659     }
660 
661     @Override
setHotspot(float x, float y)662     public void setHotspot(float x, float y) {
663         mPendingX = x;
664         mPendingY = y;
665         if (mRipple == null || mBackground == null) {
666             mHasPending = true;
667         }
668 
669         if (mRipple != null) {
670             mRipple.move(x, y);
671         }
672     }
673 
674     /**
675      * Attempts to start an enter animation for the active hotspot. Fails if
676      * there are too many animating ripples.
677      */
tryRippleEnter()678     private void tryRippleEnter() {
679         if (mExitingRipplesCount >= MAX_RIPPLES) {
680             // This should never happen unless the user is tapping like a maniac
681             // or there is a bug that's preventing ripples from being removed.
682             return;
683         }
684 
685         if (mRipple == null) {
686             final float x;
687             final float y;
688             if (mHasPending) {
689                 mHasPending = false;
690                 x = mPendingX;
691                 y = mPendingY;
692             } else {
693                 x = mHotspotBounds.exactCenterX();
694                 y = mHotspotBounds.exactCenterY();
695             }
696 
697             mRipple = new RippleForeground(this, mHotspotBounds, x, y, mForceSoftware);
698         }
699 
700         mRipple.setup(mState.mMaxRadius, mDensity);
701         mRipple.enter();
702     }
703 
704     /**
705      * Attempts to start an exit animation for the active hotspot. Fails if
706      * there is no active hotspot.
707      */
tryRippleExit()708     private void tryRippleExit() {
709         if (mRipple != null) {
710             if (mExitingRipples == null) {
711                 mExitingRipples = new RippleForeground[MAX_RIPPLES];
712             }
713             mExitingRipples[mExitingRipplesCount++] = mRipple;
714             mRipple.exit();
715             mRipple = null;
716         }
717     }
718 
719     /**
720      * Cancels and removes the active ripple, all exiting ripples, and the
721      * background. Nothing will be drawn after this method is called.
722      */
clearHotspots()723     private void clearHotspots() {
724         if (mRipple != null) {
725             mRipple.end();
726             mRipple = null;
727             mRippleActive = false;
728         }
729 
730         if (mBackground != null) {
731             mBackground.setState(false, false, false);
732         }
733 
734         cancelExitingRipples();
735         endPatternedAnimations();
736     }
737 
738     @Override
setHotspotBounds(int left, int top, int right, int bottom)739     public void setHotspotBounds(int left, int top, int right, int bottom) {
740         mOverrideBounds = true;
741         mHotspotBounds.set(left, top, right, bottom);
742 
743         onHotspotBoundsChanged();
744     }
745 
746     @Override
getHotspotBounds(Rect outRect)747     public void getHotspotBounds(Rect outRect) {
748         outRect.set(mHotspotBounds);
749     }
750 
751     /**
752      * Notifies all the animating ripples that the hotspot bounds have changed and modify sessions.
753      */
onHotspotBoundsChanged()754     private void onHotspotBoundsChanged() {
755         final int count = mExitingRipplesCount;
756         final RippleForeground[] ripples = mExitingRipples;
757         for (int i = 0; i < count; i++) {
758             ripples[i].onHotspotBoundsChanged();
759         }
760 
761         if (mRipple != null) {
762             mRipple.onHotspotBoundsChanged();
763         }
764 
765         if (mBackground != null) {
766             mBackground.onHotspotBoundsChanged();
767         }
768         float newRadius = getComputedRadius();
769         for (int i = 0; i < mRunningAnimations.size(); i++) {
770             RippleAnimationSession s = mRunningAnimations.get(i);
771             s.setRadius(newRadius);
772             s.getProperties().getShader()
773                     .setResolution(mHotspotBounds.width(), mHotspotBounds.height());
774             float cx = mHotspotBounds.centerX(), cy = mHotspotBounds.centerY();
775             s.getProperties().getShader().setOrigin(cx, cy);
776             s.getProperties().setOrigin(cx, cy);
777             if (!s.isForceSoftware()) {
778                 s.getCanvasProperties()
779                         .setOrigin(CanvasProperty.createFloat(cx), CanvasProperty.createFloat(cy));
780             }
781         }
782     }
783 
784     /**
785      * Populates <code>outline</code> with the first available layer outline,
786      * excluding the mask layer.
787      *
788      * @param outline Outline in which to place the first available layer outline
789      */
790     @Override
getOutline(@onNull Outline outline)791     public void getOutline(@NonNull Outline outline) {
792         final LayerState state = mLayerState;
793         final ChildDrawable[] children = state.mChildren;
794         final int N = state.mNumChildren;
795         for (int i = 0; i < N; i++) {
796             if (children[i].mId != R.id.mask) {
797                 children[i].mDrawable.getOutline(outline);
798                 if (!outline.isEmpty()) return;
799             }
800         }
801     }
802 
803     /**
804      * Optimized for drawing ripples with a mask layer and optional content.
805      */
806     @Override
draw(@onNull Canvas canvas)807     public void draw(@NonNull Canvas canvas) {
808         if (mState.mRippleStyle == STYLE_SOLID) {
809             drawSolid(canvas);
810         } else {
811             drawPatterned(canvas);
812         }
813     }
814 
drawSolid(Canvas canvas)815     private void drawSolid(Canvas canvas) {
816         pruneRipples();
817 
818         // Clip to the dirty bounds, which will be the drawable bounds if we
819         // have a mask or content and the ripple bounds if we're projecting.
820         final Rect bounds = getDirtyBounds();
821         final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
822         if (isBounded()) {
823             canvas.clipRect(bounds);
824         }
825 
826         drawContent(canvas);
827         drawBackgroundAndRipples(canvas);
828 
829         canvas.restoreToCount(saveCount);
830     }
831 
exitPatternedBackgroundAnimation()832     private void exitPatternedBackgroundAnimation() {
833         mTargetBackgroundOpacity = 0;
834         if (mBackgroundAnimation != null) mBackgroundAnimation.cancel();
835         // after cancel
836         mRunBackgroundAnimation = true;
837         invalidateSelf(false);
838     }
839 
startPatternedAnimation()840     private void startPatternedAnimation() {
841         mAddRipple = true;
842         invalidateSelf(false);
843     }
844 
exitPatternedAnimation()845     private void exitPatternedAnimation() {
846         mExitingAnimation = true;
847         invalidateSelf(false);
848     }
849 
850     /** @hide */
851     @TestApi
getTargetBackgroundOpacity()852     public float getTargetBackgroundOpacity() {
853         return mTargetBackgroundOpacity;
854     }
855 
enterPatternedBackgroundAnimation(boolean focused, boolean hovered, boolean windowFocused)856     private void enterPatternedBackgroundAnimation(boolean focused, boolean hovered,
857             boolean windowFocused) {
858         mBackgroundOpacity = 0;
859         if (focused) {
860             mTargetBackgroundOpacity = windowFocused ? .6f : .2f;
861         } else {
862             mTargetBackgroundOpacity = hovered ? .2f : 0f;
863         }
864         if (mBackgroundAnimation != null) mBackgroundAnimation.cancel();
865         // after cancel
866         mRunBackgroundAnimation = true;
867         invalidateSelf(false);
868     }
869 
startBackgroundAnimation()870     private void startBackgroundAnimation() {
871         mRunBackgroundAnimation = false;
872         if (Looper.myLooper() == null) {
873             Log.w(TAG, "Thread doesn't have a looper. Skipping animation.");
874             return;
875         }
876         mBackgroundAnimation = ValueAnimator.ofFloat(mBackgroundOpacity, mTargetBackgroundOpacity);
877         mBackgroundAnimation.setInterpolator(LINEAR_INTERPOLATOR);
878         mBackgroundAnimation.setDuration(BACKGROUND_OPACITY_DURATION);
879         mBackgroundAnimation.addUpdateListener(update -> {
880             mBackgroundOpacity = (float) update.getAnimatedValue();
881             invalidateSelf(false);
882         });
883         mBackgroundAnimation.start();
884     }
885 
drawPatterned(@onNull Canvas canvas)886     private void drawPatterned(@NonNull Canvas canvas) {
887         final Rect bounds = mHotspotBounds;
888         final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
889         boolean useCanvasProps = !mForceSoftware;
890         if (isBounded()) {
891             canvas.clipRect(getDirtyBounds());
892         }
893         final float x, y, cx, cy, w, h;
894         boolean addRipple = mAddRipple;
895         cx = bounds.centerX();
896         cy = bounds.centerY();
897         boolean shouldExit = mExitingAnimation;
898         mExitingAnimation = false;
899         mAddRipple = false;
900         if (mRunningAnimations.size() > 0 && !addRipple) {
901             // update paint when view is invalidated
902             updateRipplePaint();
903         }
904         drawContent(canvas);
905         drawPatternedBackground(canvas, cx, cy);
906         if (addRipple && mRunningAnimations.size() <= MAX_RIPPLES) {
907             if (mHasPending) {
908                 x = mPendingX;
909                 y = mPendingY;
910                 mHasPending = false;
911             } else {
912                 x = bounds.exactCenterX();
913                 y = bounds.exactCenterY();
914             }
915             h = bounds.height();
916             w = bounds.width();
917             RippleAnimationSession.AnimationProperties<Float, Paint> properties =
918                     createAnimationProperties(x, y, cx, cy, w, h);
919             mRunningAnimations.add(new RippleAnimationSession(properties, !useCanvasProps)
920                     .setOnAnimationUpdated(() -> invalidateSelf(false))
921                     .setOnSessionEnd(session -> {
922                         mRunningAnimations.remove(session);
923                     })
924                     .setForceSoftwareAnimation(!useCanvasProps)
925                     .enter(canvas));
926         }
927         if (shouldExit) {
928             for (int i = 0; i < mRunningAnimations.size(); i++) {
929                 RippleAnimationSession s = mRunningAnimations.get(i);
930                 s.exit(canvas);
931             }
932         }
933         for (int i = 0; i < mRunningAnimations.size(); i++) {
934             RippleAnimationSession s = mRunningAnimations.get(i);
935             if (!canvas.isHardwareAccelerated()) {
936                 Log.e(TAG, "The RippleDrawable.STYLE_PATTERNED animation is not supported for a "
937                         + "non-hardware accelerated Canvas. Skipping animation.");
938                 break;
939             } else if (useCanvasProps) {
940                 RippleAnimationSession.AnimationProperties<CanvasProperty<Float>,
941                         CanvasProperty<Paint>>
942                         p = s.getCanvasProperties();
943                 RecordingCanvas can = (RecordingCanvas) canvas;
944                 can.drawRipple(p.getX(), p.getY(), p.getMaxRadius(), p.getPaint(),
945                         p.getProgress(), p.getNoisePhase(), p.getColor(), p.getShader());
946             } else {
947                 RippleAnimationSession.AnimationProperties<Float, Paint> p =
948                         s.getProperties();
949                 float radius = p.getMaxRadius();
950                 canvas.drawCircle(p.getX(), p.getY(), radius, p.getPaint());
951             }
952         }
953         canvas.restoreToCount(saveCount);
954     }
955 
drawPatternedBackground(Canvas c, float cx, float cy)956     private void drawPatternedBackground(Canvas c, float cx, float cy) {
957         if (mRunBackgroundAnimation) {
958             startBackgroundAnimation();
959         }
960         if (mBackgroundOpacity == 0) return;
961         Paint p = updateRipplePaint();
962         float newOpacity = mBackgroundOpacity;
963         final int origAlpha = p.getAlpha();
964         final int alpha = Math.min((int) (origAlpha * newOpacity + 0.5f), 255);
965         if (alpha > 0) {
966             ColorFilter origFilter = p.getColorFilter();
967             p.setColorFilter(mFocusColorFilter);
968             p.setAlpha(alpha);
969             c.drawCircle(cx, cy, getComputedRadius(), p);
970             p.setAlpha(origAlpha);
971             p.setColorFilter(origFilter);
972         }
973     }
974 
computeRadius()975     private float computeRadius() {
976         final float halfWidth = mHotspotBounds.width() / 2.0f;
977         final float halfHeight = mHotspotBounds.height() / 2.0f;
978         return (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
979     }
980 
getComputedRadius()981     private int getComputedRadius() {
982         if (mState.mMaxRadius >= 0) return mState.mMaxRadius;
983         return (int) computeRadius();
984     }
985 
986     @NonNull
createAnimationProperties( float x, float y, float cx, float cy, float w, float h)987     private RippleAnimationSession.AnimationProperties<Float, Paint> createAnimationProperties(
988             float x, float y, float cx, float cy, float w, float h) {
989         Paint p = new Paint(updateRipplePaint());
990         float radius = getComputedRadius();
991         RippleAnimationSession.AnimationProperties<Float, Paint> properties;
992         RippleShader shader = new RippleShader();
993         // Grab the color for the current state and cut the alpha channel in
994         // half so that the ripple and background together yield full alpha.
995         final int color = mMaskColorFilter == null
996                 ? mState.mColor.getColorForState(getState(), Color.BLACK)
997                 : mMaskColorFilter.getColor();
998         final int effectColor = mState.mEffectColor.getColorForState(getState(), Color.MAGENTA);
999         final float noisePhase = AnimationUtils.currentAnimationTimeMillis();
1000         shader.setColor(color, effectColor);
1001         shader.setOrigin(cx, cy);
1002         shader.setTouch(x, y);
1003         shader.setResolution(w, h);
1004         shader.setNoisePhase(noisePhase);
1005         shader.setRadius(radius);
1006         shader.setProgress(.0f);
1007         properties = new RippleAnimationSession.AnimationProperties<>(
1008                 cx, cy, radius, noisePhase, p, 0f, color, shader);
1009         if (mMaskShader == null) {
1010             shader.setShader(null);
1011         } else {
1012             shader.setShader(mMaskShader);
1013         }
1014         p.setShader(shader);
1015         p.setColorFilter(null);
1016         // Alpha is handled by the shader (and color is a no-op because there's a shader)
1017         p.setColor(0xFF000000);
1018         return properties;
1019     }
1020 
1021     @Override
invalidateSelf()1022     public void invalidateSelf() {
1023         invalidateSelf(true);
1024     }
1025 
invalidateSelf(boolean invalidateMask)1026     void invalidateSelf(boolean invalidateMask) {
1027         super.invalidateSelf();
1028 
1029         if (invalidateMask) {
1030             // Force the mask to update on the next draw().
1031             mHasValidMask = false;
1032         }
1033 
1034     }
1035 
pruneRipples()1036     private void pruneRipples() {
1037         int remaining = 0;
1038 
1039         // Move remaining entries into pruned spaces.
1040         final RippleForeground[] ripples = mExitingRipples;
1041         final int count = mExitingRipplesCount;
1042         for (int i = 0; i < count; i++) {
1043             if (!ripples[i].hasFinishedExit()) {
1044                 ripples[remaining++] = ripples[i];
1045             }
1046         }
1047 
1048         // Null out the remaining entries.
1049         for (int i = remaining; i < count; i++) {
1050             ripples[i] = null;
1051         }
1052 
1053         mExitingRipplesCount = remaining;
1054     }
1055 
1056     /**
1057      * @return whether we need to use a mask
1058      */
updateMaskShaderIfNeeded()1059     private void updateMaskShaderIfNeeded() {
1060         if (mHasValidMask) {
1061             return;
1062         }
1063 
1064         final int maskType = getMaskType();
1065         if (maskType == MASK_UNKNOWN) {
1066             return;
1067         }
1068 
1069         mHasValidMask = true;
1070 
1071         final Rect bounds = getBounds();
1072         if (maskType == MASK_NONE || bounds.isEmpty()) {
1073             if (mMaskBuffer != null) {
1074                 mMaskBuffer.recycle();
1075                 mMaskBuffer = null;
1076                 mMaskShader = null;
1077                 mMaskCanvas = null;
1078             }
1079             mMaskMatrix = null;
1080             mMaskColorFilter = null;
1081             return;
1082         }
1083 
1084         // Ensure we have a correctly-sized buffer.
1085         if (mMaskBuffer == null
1086                 || mMaskBuffer.getWidth() != bounds.width()
1087                 || mMaskBuffer.getHeight() != bounds.height()) {
1088             if (mMaskBuffer != null) {
1089                 mMaskBuffer.recycle();
1090             }
1091 
1092             mMaskBuffer = Bitmap.createBitmap(
1093                     bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
1094             mMaskShader = new BitmapShader(mMaskBuffer,
1095                     Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
1096             mMaskCanvas = new Canvas(mMaskBuffer);
1097         } else {
1098             mMaskBuffer.eraseColor(Color.TRANSPARENT);
1099         }
1100 
1101         if (mMaskMatrix == null) {
1102             mMaskMatrix = new Matrix();
1103         } else {
1104             mMaskMatrix.reset();
1105         }
1106 
1107         if (mMaskColorFilter == null) {
1108             mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
1109             mFocusColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
1110         }
1111 
1112         // Draw the appropriate mask anchored to (0,0).
1113         final int saveCount = mMaskCanvas.save();
1114         final int left = bounds.left;
1115         final int top = bounds.top;
1116         mMaskCanvas.translate(-left, -top);
1117         if (maskType == MASK_EXPLICIT) {
1118             drawMask(mMaskCanvas);
1119         } else if (maskType == MASK_CONTENT) {
1120             drawContent(mMaskCanvas);
1121         }
1122         mMaskCanvas.restoreToCount(saveCount);
1123     }
1124 
getMaskType()1125     private int getMaskType() {
1126         if (mRipple == null && mExitingRipplesCount <= 0
1127                 && (mBackground == null || !mBackground.isVisible())
1128                 && mState.mRippleStyle == STYLE_SOLID) {
1129             // We might need a mask later.
1130             return MASK_UNKNOWN;
1131         }
1132 
1133         if (mMask != null) {
1134             if (mMask.getOpacity() == PixelFormat.OPAQUE) {
1135                 // Clipping handles opaque explicit masks.
1136                 return MASK_NONE;
1137             } else {
1138                 return MASK_EXPLICIT;
1139             }
1140         }
1141 
1142         // Check for non-opaque, non-mask content.
1143         final ChildDrawable[] array = mLayerState.mChildren;
1144         final int count = mLayerState.mNumChildren;
1145         for (int i = 0; i < count; i++) {
1146             if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
1147                 return MASK_CONTENT;
1148             }
1149         }
1150 
1151         // Clipping handles opaque content.
1152         return MASK_NONE;
1153     }
1154 
drawContent(Canvas canvas)1155     private void drawContent(Canvas canvas) {
1156         // Draw everything except the mask.
1157         final ChildDrawable[] array = mLayerState.mChildren;
1158         final int count = mLayerState.mNumChildren;
1159         for (int i = 0; i < count; i++) {
1160             if (array[i].mId != R.id.mask) {
1161                 array[i].mDrawable.draw(canvas);
1162             }
1163         }
1164     }
1165 
drawBackgroundAndRipples(Canvas canvas)1166     private void drawBackgroundAndRipples(Canvas canvas) {
1167         final RippleForeground active = mRipple;
1168         final RippleBackground background = mBackground;
1169         final int count = mExitingRipplesCount;
1170         if (active == null && count <= 0 && (background == null || !background.isVisible())) {
1171             // Move along, nothing to draw here.
1172             return;
1173         }
1174 
1175         final float x = mHotspotBounds.exactCenterX();
1176         final float y = mHotspotBounds.exactCenterY();
1177         canvas.translate(x, y);
1178 
1179         final Paint p = updateRipplePaint();
1180 
1181         if (background != null && background.isVisible()) {
1182             background.draw(canvas, p);
1183         }
1184 
1185         if (count > 0) {
1186             final RippleForeground[] ripples = mExitingRipples;
1187             for (int i = 0; i < count; i++) {
1188                 ripples[i].draw(canvas, p);
1189             }
1190         }
1191 
1192         if (active != null) {
1193             active.draw(canvas, p);
1194         }
1195 
1196         canvas.translate(-x, -y);
1197     }
1198 
drawMask(Canvas canvas)1199     private void drawMask(Canvas canvas) {
1200         mMask.draw(canvas);
1201     }
1202 
1203     @UnsupportedAppUsage
updateRipplePaint()1204     Paint updateRipplePaint() {
1205         if (mRipplePaint == null) {
1206             mRipplePaint = new Paint();
1207             mRipplePaint.setAntiAlias(true);
1208             mRipplePaint.setStyle(Paint.Style.FILL);
1209         }
1210 
1211         final float x = mHotspotBounds.exactCenterX();
1212         final float y = mHotspotBounds.exactCenterY();
1213 
1214         updateMaskShaderIfNeeded();
1215 
1216         // Position the shader to account for canvas translation.
1217         if (mMaskShader != null) {
1218             final Rect bounds = getBounds();
1219             if (mState.mRippleStyle == STYLE_PATTERNED) {
1220                 mMaskMatrix.setTranslate(bounds.left, bounds.top);
1221             } else {
1222                 mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
1223             }
1224             mMaskShader.setLocalMatrix(mMaskMatrix);
1225 
1226             if (mState.mRippleStyle == STYLE_PATTERNED) {
1227                 for (int i = 0; i < mRunningAnimations.size(); i++) {
1228                     mRunningAnimations.get(i).getProperties().getShader().setShader(mMaskShader);
1229                 }
1230             }
1231         }
1232 
1233         // Grab the color for the current state and cut the alpha channel in
1234         // half so that the ripple and background together yield full alpha.
1235         final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
1236         final Paint p = mRipplePaint;
1237 
1238         if (mMaskColorFilter != null) {
1239             // The ripple timing depends on the paint's alpha value, so we need
1240             // to push just the alpha channel into the paint and let the filter
1241             // handle the full-alpha color.
1242             int maskColor = mState.mRippleStyle == STYLE_PATTERNED ? color : color | 0xFF000000;
1243             if (mMaskColorFilter.getColor() != maskColor) {
1244                 mMaskColorFilter = new PorterDuffColorFilter(maskColor, mMaskColorFilter.getMode());
1245                 mFocusColorFilter = new PorterDuffColorFilter(color | 0xFF000000,
1246                         mFocusColorFilter.getMode());
1247             }
1248             p.setColor(color & 0xFF000000);
1249             p.setColorFilter(mMaskColorFilter);
1250             p.setShader(mMaskShader);
1251         } else {
1252             p.setColor(color);
1253             p.setColorFilter(null);
1254             p.setShader(null);
1255         }
1256 
1257         return p;
1258     }
1259 
1260     @Override
getDirtyBounds()1261     public Rect getDirtyBounds() {
1262         if (!isBounded()) {
1263             final Rect drawingBounds = mDrawingBounds;
1264             final Rect dirtyBounds = mDirtyBounds;
1265             dirtyBounds.set(drawingBounds);
1266             drawingBounds.setEmpty();
1267 
1268             final int cX = (int) mHotspotBounds.exactCenterX();
1269             final int cY = (int) mHotspotBounds.exactCenterY();
1270             final Rect rippleBounds = mTempRect;
1271 
1272             final RippleForeground[] activeRipples = mExitingRipples;
1273             final int N = mExitingRipplesCount;
1274             for (int i = 0; i < N; i++) {
1275                 activeRipples[i].getBounds(rippleBounds);
1276                 rippleBounds.offset(cX, cY);
1277                 drawingBounds.union(rippleBounds);
1278             }
1279 
1280             final RippleBackground background = mBackground;
1281             if (background != null) {
1282                 background.getBounds(rippleBounds);
1283                 rippleBounds.offset(cX, cY);
1284                 drawingBounds.union(rippleBounds);
1285             }
1286 
1287             dirtyBounds.union(drawingBounds);
1288             dirtyBounds.union(super.getDirtyBounds());
1289             return dirtyBounds;
1290         } else {
1291             return getBounds();
1292         }
1293     }
1294 
1295     /**
1296      * Sets whether to disable RenderThread animations for this ripple.
1297      *
1298      * @param forceSoftware true if RenderThread animations should be disabled, false otherwise
1299      * @hide
1300      */
1301     @UnsupportedAppUsage
setForceSoftware(boolean forceSoftware)1302     public void setForceSoftware(boolean forceSoftware) {
1303         mForceSoftware = forceSoftware;
1304     }
1305 
1306     @Override
getConstantState()1307     public ConstantState getConstantState() {
1308         return mState;
1309     }
1310 
1311     @Override
mutate()1312     public Drawable mutate() {
1313         super.mutate();
1314 
1315         // LayerDrawable creates a new state using createConstantState, so
1316         // this should always be a safe cast.
1317         mState = (RippleState) mLayerState;
1318 
1319         // The locally cached drawable may have changed.
1320         mMask = findDrawableByLayerId(R.id.mask);
1321 
1322         return this;
1323     }
1324 
1325     @Override
createConstantState(LayerState state, Resources res)1326     RippleState createConstantState(LayerState state, Resources res) {
1327         return new RippleState(state, this, res);
1328     }
1329 
1330     static class RippleState extends LayerState {
1331         int[] mTouchThemeAttrs;
1332         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1333         ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
1334         ColorStateList mEffectColor = ColorStateList.valueOf(DEFAULT_EFFECT_COLOR);
1335         int mMaxRadius = RADIUS_AUTO;
1336         int mRippleStyle = FORCE_PATTERNED_STYLE ? STYLE_PATTERNED : STYLE_SOLID;
1337 
RippleState(LayerState orig, RippleDrawable owner, Resources res)1338         public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
1339             super(orig, owner, res);
1340 
1341             if (orig != null && orig instanceof RippleState) {
1342                 final RippleState origs = (RippleState) orig;
1343                 mTouchThemeAttrs = origs.mTouchThemeAttrs;
1344                 mColor = origs.mColor;
1345                 mMaxRadius = origs.mMaxRadius;
1346                 mRippleStyle = origs.mRippleStyle;
1347                 mEffectColor = origs.mEffectColor;
1348 
1349                 if (origs.mDensity != mDensity) {
1350                     applyDensityScaling(orig.mDensity, mDensity);
1351                 }
1352             }
1353         }
1354 
1355         @Override
onDensityChanged(int sourceDensity, int targetDensity)1356         protected void onDensityChanged(int sourceDensity, int targetDensity) {
1357             super.onDensityChanged(sourceDensity, targetDensity);
1358 
1359             applyDensityScaling(sourceDensity, targetDensity);
1360         }
1361 
applyDensityScaling(int sourceDensity, int targetDensity)1362         private void applyDensityScaling(int sourceDensity, int targetDensity) {
1363             if (mMaxRadius != RADIUS_AUTO) {
1364                 mMaxRadius = Drawable.scaleFromDensity(
1365                         mMaxRadius, sourceDensity, targetDensity, true);
1366             }
1367         }
1368 
1369         @Override
canApplyTheme()1370         public boolean canApplyTheme() {
1371             return mTouchThemeAttrs != null
1372                     || (mColor != null && mColor.canApplyTheme())
1373                     || super.canApplyTheme();
1374         }
1375 
1376         @Override
newDrawable()1377         public Drawable newDrawable() {
1378             return new RippleDrawable(this, null);
1379         }
1380 
1381         @Override
newDrawable(Resources res)1382         public Drawable newDrawable(Resources res) {
1383             return new RippleDrawable(this, res);
1384         }
1385 
1386         @Override
getChangingConfigurations()1387         public @Config int getChangingConfigurations() {
1388             return super.getChangingConfigurations()
1389                     | (mColor != null ? mColor.getChangingConfigurations() : 0);
1390         }
1391     }
1392 
RippleDrawable(RippleState state, Resources res)1393     private RippleDrawable(RippleState state, Resources res) {
1394         mState = new RippleState(state, this, res);
1395         mLayerState = mState;
1396         mDensity = Drawable.resolveDensity(res, mState.mDensity);
1397 
1398         if (mState.mNumChildren > 0) {
1399             ensurePadding();
1400             refreshPadding();
1401         }
1402 
1403         updateLocalState();
1404     }
1405 
updateLocalState()1406     private void updateLocalState() {
1407         // Initialize from constant state.
1408         mMask = findDrawableByLayerId(R.id.mask);
1409     }
1410 }
1411