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