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