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 com.android.internal.R;
20 
21 import org.xmlpull.v1.XmlPullParser;
22 import org.xmlpull.v1.XmlPullParserException;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.content.pm.ActivityInfo.Config;
27 import android.content.res.ColorStateList;
28 import android.content.res.Resources;
29 import android.content.res.Resources.Theme;
30 import android.content.res.TypedArray;
31 import android.graphics.Bitmap;
32 import android.graphics.BitmapShader;
33 import android.graphics.Canvas;
34 import android.graphics.Color;
35 import android.graphics.Matrix;
36 import android.graphics.Outline;
37 import android.graphics.Paint;
38 import android.graphics.PixelFormat;
39 import android.graphics.PorterDuff;
40 import android.graphics.PorterDuffColorFilter;
41 import android.graphics.Rect;
42 import android.graphics.Shader;
43 import android.util.AttributeSet;
44 
45 import java.io.IOException;
46 import java.util.Arrays;
47 
48 /**
49  * Drawable that shows a ripple effect in response to state changes. The
50  * anchoring position of the ripple for a given state may be specified by
51  * calling {@link #setHotspot(float, float)} with the corresponding state
52  * attribute identifier.
53  * <p>
54  * A touch feedback drawable may contain multiple child layers, including a
55  * special mask layer that is not drawn to the screen. A single layer may be
56  * set as the mask from XML by specifying its {@code android:id} value as
57  * {@link android.R.id#mask}. At run time, a single layer may be set as the
58  * mask using {@code setId(..., android.R.id.mask)} or an existing mask layer
59  * may be replaced using {@code setDrawableByLayerId(android.R.id.mask, ...)}.
60  * <pre>
61  * <code>&lt!-- A red ripple masked against an opaque rectangle. --/>
62  * &ltripple android:color="#ffff0000">
63  *   &ltitem android:id="@android:id/mask"
64  *         android:drawable="@android:color/white" />
65  * &lt/ripple></code>
66  * </pre>
67  * <p>
68  * If a mask layer is set, the ripple effect will be masked against that layer
69  * before it is drawn over the composite of the remaining child layers.
70  * <p>
71  * If no mask layer is set, the ripple effect is masked against the composite
72  * of the child layers.
73  * <pre>
74  * <code>&lt!-- A green ripple drawn atop a black rectangle. --/>
75  * &ltripple android:color="#ff00ff00">
76  *   &ltitem android:drawable="@android:color/black" />
77  * &lt/ripple>
78  *
79  * &lt!-- A blue ripple drawn atop a drawable resource. --/>
80  * &ltripple android:color="#ff0000ff">
81  *   &ltitem android:drawable="@drawable/my_drawable" />
82  * &lt/ripple></code>
83  * </pre>
84  * <p>
85  * If no child layers or mask is specified and the ripple is set as a View
86  * background, the ripple will be drawn atop the first available parent
87  * background within the View's hierarchy. In this case, the drawing region
88  * may extend outside of the Drawable bounds.
89  * <pre>
90  * <code>&lt!-- An unbounded red ripple. --/>
91  * &ltripple android:color="#ffff0000" /></code>
92  * </pre>
93  *
94  * @attr ref android.R.styleable#RippleDrawable_color
95  */
96 public class RippleDrawable extends LayerDrawable {
97     /**
98      * Radius value that specifies the ripple radius should be computed based
99      * on the size of the ripple's container.
100      */
101     public static final int RADIUS_AUTO = -1;
102 
103     private static final int MASK_UNKNOWN = -1;
104     private static final int MASK_NONE = 0;
105     private static final int MASK_CONTENT = 1;
106     private static final int MASK_EXPLICIT = 2;
107 
108     /** The maximum number of ripples supported. */
109     private static final int MAX_RIPPLES = 10;
110 
111     private final Rect mTempRect = new Rect();
112 
113     /** Current ripple effect bounds, used to constrain ripple effects. */
114     private final Rect mHotspotBounds = new Rect();
115 
116     /** Current drawing bounds, used to compute dirty region. */
117     private final Rect mDrawingBounds = new Rect();
118 
119     /** Current dirty bounds, union of current and previous drawing bounds. */
120     private final Rect mDirtyBounds = new Rect();
121 
122     /** Mirrors mLayerState with some extra information. */
123     private RippleState mState;
124 
125     /** The masking layer, e.g. the layer with id R.id.mask. */
126     private Drawable mMask;
127 
128     /** The current background. May be actively animating or pending entry. */
129     private RippleBackground mBackground;
130 
131     private Bitmap mMaskBuffer;
132     private BitmapShader mMaskShader;
133     private Canvas mMaskCanvas;
134     private Matrix mMaskMatrix;
135     private PorterDuffColorFilter mMaskColorFilter;
136     private boolean mHasValidMask;
137 
138     /** Whether we expect to draw a background when visible. */
139     private boolean mBackgroundActive;
140 
141     /** The current ripple. May be actively animating or pending entry. */
142     private RippleForeground mRipple;
143 
144     /** Whether we expect to draw a ripple when visible. */
145     private boolean mRippleActive;
146 
147     // Hotspot coordinates that are awaiting activation.
148     private float mPendingX;
149     private float mPendingY;
150     private boolean mHasPending;
151 
152     /**
153      * Lazily-created array of actively animating ripples. Inactive ripples are
154      * pruned during draw(). The locations of these will not change.
155      */
156     private RippleForeground[] mExitingRipples;
157     private int mExitingRipplesCount = 0;
158 
159     /** Paint used to control appearance of ripples. */
160     private Paint mRipplePaint;
161 
162     /** Target density of the display into which ripples are drawn. */
163     private int mDensity;
164 
165     /** Whether bounds are being overridden. */
166     private boolean mOverrideBounds;
167 
168     /**
169      * If set, force all ripple animations to not run on RenderThread, even if it would be
170      * available.
171      */
172     private boolean mForceSoftware;
173 
174     /**
175      * Constructor used for drawable inflation.
176      */
RippleDrawable()177     RippleDrawable() {
178         this(new RippleState(null, null, null), null);
179     }
180 
181     /**
182      * Creates a new ripple drawable with the specified ripple color and
183      * optional content and mask drawables.
184      *
185      * @param color The ripple color
186      * @param content The content drawable, may be {@code null}
187      * @param mask The mask drawable, may be {@code null}
188      */
RippleDrawable(@onNull ColorStateList color, @Nullable Drawable content, @Nullable Drawable mask)189     public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content,
190             @Nullable Drawable mask) {
191         this(new RippleState(null, null, null), null);
192 
193         if (color == null) {
194             throw new IllegalArgumentException("RippleDrawable requires a non-null color");
195         }
196 
197         if (content != null) {
198             addLayer(content, null, 0, 0, 0, 0, 0);
199         }
200 
201         if (mask != null) {
202             addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0);
203         }
204 
205         setColor(color);
206         ensurePadding();
207         refreshPadding();
208         updateLocalState();
209     }
210 
211     @Override
jumpToCurrentState()212     public void jumpToCurrentState() {
213         super.jumpToCurrentState();
214 
215         if (mRipple != null) {
216             mRipple.end();
217         }
218 
219         if (mBackground != null) {
220             mBackground.end();
221         }
222 
223         cancelExitingRipples();
224     }
225 
cancelExitingRipples()226     private void cancelExitingRipples() {
227         final int count = mExitingRipplesCount;
228         final RippleForeground[] ripples = mExitingRipples;
229         for (int i = 0; i < count; i++) {
230             ripples[i].end();
231         }
232 
233         if (ripples != null) {
234             Arrays.fill(ripples, 0, count, null);
235         }
236         mExitingRipplesCount = 0;
237 
238         // Always draw an additional "clean" frame after canceling animations.
239         invalidateSelf(false);
240     }
241 
242     @Override
getOpacity()243     public int getOpacity() {
244         // Worst-case scenario.
245         return PixelFormat.TRANSLUCENT;
246     }
247 
248     @Override
onStateChange(int[] stateSet)249     protected boolean onStateChange(int[] stateSet) {
250         final boolean changed = super.onStateChange(stateSet);
251 
252         boolean enabled = false;
253         boolean pressed = false;
254         boolean focused = false;
255         boolean hovered = false;
256 
257         for (int state : stateSet) {
258             if (state == R.attr.state_enabled) {
259                 enabled = true;
260             } else if (state == R.attr.state_focused) {
261                 focused = true;
262             } else if (state == R.attr.state_pressed) {
263                 pressed = true;
264             } else if (state == R.attr.state_hovered) {
265                 hovered = true;
266             }
267         }
268 
269         setRippleActive(enabled && pressed);
270         setBackgroundActive(hovered || focused || (enabled && pressed), focused || hovered);
271 
272         return changed;
273     }
274 
setRippleActive(boolean active)275     private void setRippleActive(boolean active) {
276         if (mRippleActive != active) {
277             mRippleActive = active;
278             if (active) {
279                 tryRippleEnter();
280             } else {
281                 tryRippleExit();
282             }
283         }
284     }
285 
setBackgroundActive(boolean active, boolean focused)286     private void setBackgroundActive(boolean active, boolean focused) {
287         if (mBackgroundActive != active) {
288             mBackgroundActive = active;
289             if (active) {
290                 tryBackgroundEnter(focused);
291             } else {
292                 tryBackgroundExit();
293             }
294         }
295     }
296 
297     @Override
onBoundsChange(Rect bounds)298     protected void onBoundsChange(Rect bounds) {
299         super.onBoundsChange(bounds);
300 
301         if (!mOverrideBounds) {
302             mHotspotBounds.set(bounds);
303             onHotspotBoundsChanged();
304         }
305 
306         if (mBackground != null) {
307             mBackground.onBoundsChange();
308         }
309 
310         if (mRipple != null) {
311             mRipple.onBoundsChange();
312         }
313 
314         invalidateSelf();
315     }
316 
317     @Override
setVisible(boolean visible, boolean restart)318     public boolean setVisible(boolean visible, boolean restart) {
319         final boolean changed = super.setVisible(visible, restart);
320 
321         if (!visible) {
322             clearHotspots();
323         } else if (changed) {
324             // If we just became visible, ensure the background and ripple
325             // visibilities are consistent with their internal states.
326             if (mRippleActive) {
327                 tryRippleEnter();
328             }
329 
330             if (mBackgroundActive) {
331                 tryBackgroundEnter(false);
332             }
333 
334             // Skip animations, just show the correct final states.
335             jumpToCurrentState();
336         }
337 
338         return changed;
339     }
340 
341     /**
342      * @hide
343      */
344     @Override
isProjected()345     public boolean isProjected() {
346         // If the layer is bounded, then we don't need to project.
347         if (isBounded()) {
348             return false;
349         }
350 
351         // Otherwise, if the maximum radius is contained entirely within the
352         // bounds then we don't need to project. This is sort of a hack to
353         // prevent check box ripples from being projected across the edges of
354         // scroll views. It does not impact rendering performance, and it can
355         // be removed once we have better handling of projection in scrollable
356         // views.
357         final int radius = mState.mMaxRadius;
358         final Rect drawableBounds = getBounds();
359         final Rect hotspotBounds = mHotspotBounds;
360         if (radius != RADIUS_AUTO
361                 && radius <= hotspotBounds.width() / 2
362                 && radius <= hotspotBounds.height() / 2
363                 && (drawableBounds.equals(hotspotBounds)
364                         || drawableBounds.contains(hotspotBounds))) {
365             return false;
366         }
367 
368         return true;
369     }
370 
isBounded()371     private boolean isBounded() {
372         return getNumberOfLayers() > 0;
373     }
374 
375     @Override
isStateful()376     public boolean isStateful() {
377         return true;
378     }
379 
380     /**
381      * Sets the ripple color.
382      *
383      * @param color Ripple color as a color state list.
384      *
385      * @attr ref android.R.styleable#RippleDrawable_color
386      */
setColor(ColorStateList color)387     public void setColor(ColorStateList color) {
388         mState.mColor = color;
389         invalidateSelf(false);
390     }
391 
392     /**
393      * Sets the radius in pixels of the fully expanded ripple.
394      *
395      * @param radius ripple radius in pixels, or {@link #RADIUS_AUTO} to
396      *               compute the radius based on the container size
397      * @attr ref android.R.styleable#RippleDrawable_radius
398      */
setRadius(int radius)399     public void setRadius(int radius) {
400         mState.mMaxRadius = radius;
401         invalidateSelf(false);
402     }
403 
404     /**
405      * @return the radius in pixels of the fully expanded ripple if an explicit
406      *         radius has been set, or {@link #RADIUS_AUTO} if the radius is
407      *         computed based on the container size
408      * @attr ref android.R.styleable#RippleDrawable_radius
409      */
getRadius()410     public int getRadius() {
411         return mState.mMaxRadius;
412     }
413 
414     @Override
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)415     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
416             @NonNull AttributeSet attrs, @Nullable Theme theme)
417             throws XmlPullParserException, IOException {
418         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
419 
420         // Force padding default to STACK before inflating.
421         setPaddingMode(PADDING_MODE_STACK);
422 
423         // Inflation will advance the XmlPullParser and AttributeSet.
424         super.inflate(r, parser, attrs, theme);
425 
426         updateStateFromTypedArray(a);
427         verifyRequiredAttributes(a);
428         a.recycle();
429 
430         updateLocalState();
431     }
432 
433     @Override
setDrawableByLayerId(int id, Drawable drawable)434     public boolean setDrawableByLayerId(int id, Drawable drawable) {
435         if (super.setDrawableByLayerId(id, drawable)) {
436             if (id == R.id.mask) {
437                 mMask = drawable;
438                 mHasValidMask = false;
439             }
440 
441             return true;
442         }
443 
444         return false;
445     }
446 
447     /**
448      * Specifies how layer padding should affect the bounds of subsequent
449      * layers. The default and recommended value for RippleDrawable is
450      * {@link #PADDING_MODE_STACK}.
451      *
452      * @param mode padding mode, one of:
453      *            <ul>
454      *            <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
455      *            padding of the previous layer
456      *            <li>{@link #PADDING_MODE_STACK} to stack each layer directly
457      *            atop the previous layer
458      *            </ul>
459      * @see #getPaddingMode()
460      */
461     @Override
setPaddingMode(int mode)462     public void setPaddingMode(int mode) {
463         super.setPaddingMode(mode);
464     }
465 
466     /**
467      * Initializes the constant state from the values in the typed array.
468      */
updateStateFromTypedArray(@onNull TypedArray a)469     private void updateStateFromTypedArray(@NonNull TypedArray a) throws XmlPullParserException {
470         final RippleState state = mState;
471 
472         // Account for any configuration changes.
473         state.mChangingConfigurations |= a.getChangingConfigurations();
474 
475         // Extract the theme attributes, if any.
476         state.mTouchThemeAttrs = a.extractThemeAttrs();
477 
478         final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
479         if (color != null) {
480             mState.mColor = color;
481         }
482 
483         mState.mMaxRadius = a.getDimensionPixelSize(
484                 R.styleable.RippleDrawable_radius, mState.mMaxRadius);
485     }
486 
verifyRequiredAttributes(@onNull TypedArray a)487     private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
488         if (mState.mColor == null && (mState.mTouchThemeAttrs == null
489                 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
490             throw new XmlPullParserException(a.getPositionDescription() +
491                     ": <ripple> requires a valid color attribute");
492         }
493     }
494 
495     @Override
applyTheme(@onNull Theme t)496     public void applyTheme(@NonNull Theme t) {
497         super.applyTheme(t);
498 
499         final RippleState state = mState;
500         if (state == null) {
501             return;
502         }
503 
504         if (state.mTouchThemeAttrs != null) {
505             final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
506                     R.styleable.RippleDrawable);
507             try {
508                 updateStateFromTypedArray(a);
509                 verifyRequiredAttributes(a);
510             } catch (XmlPullParserException e) {
511                 rethrowAsRuntimeException(e);
512             } finally {
513                 a.recycle();
514             }
515         }
516 
517         if (state.mColor != null && state.mColor.canApplyTheme()) {
518             state.mColor = state.mColor.obtainForTheme(t);
519         }
520 
521         updateLocalState();
522     }
523 
524     @Override
canApplyTheme()525     public boolean canApplyTheme() {
526         return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
527     }
528 
529     @Override
setHotspot(float x, float y)530     public void setHotspot(float x, float y) {
531         if (mRipple == null || mBackground == null) {
532             mPendingX = x;
533             mPendingY = y;
534             mHasPending = true;
535         }
536 
537         if (mRipple != null) {
538             mRipple.move(x, y);
539         }
540     }
541 
542     /**
543      * Creates an active hotspot at the specified location.
544      */
tryBackgroundEnter(boolean focused)545     private void tryBackgroundEnter(boolean focused) {
546         if (mBackground == null) {
547             final boolean isBounded = isBounded();
548             mBackground = new RippleBackground(this, mHotspotBounds, isBounded, mForceSoftware);
549         }
550 
551         mBackground.setup(mState.mMaxRadius, mDensity);
552         mBackground.enter(focused);
553     }
554 
tryBackgroundExit()555     private void tryBackgroundExit() {
556         if (mBackground != null) {
557             // Don't null out the background, we need it to draw!
558             mBackground.exit();
559         }
560     }
561 
562     /**
563      * Attempts to start an enter animation for the active hotspot. Fails if
564      * there are too many animating ripples.
565      */
tryRippleEnter()566     private void tryRippleEnter() {
567         if (mExitingRipplesCount >= MAX_RIPPLES) {
568             // This should never happen unless the user is tapping like a maniac
569             // or there is a bug that's preventing ripples from being removed.
570             return;
571         }
572 
573         if (mRipple == null) {
574             final float x;
575             final float y;
576             if (mHasPending) {
577                 mHasPending = false;
578                 x = mPendingX;
579                 y = mPendingY;
580             } else {
581                 x = mHotspotBounds.exactCenterX();
582                 y = mHotspotBounds.exactCenterY();
583             }
584 
585             final boolean isBounded = isBounded();
586             mRipple = new RippleForeground(this, mHotspotBounds, x, y, isBounded, mForceSoftware);
587         }
588 
589         mRipple.setup(mState.mMaxRadius, mDensity);
590         mRipple.enter(false);
591     }
592 
593     /**
594      * Attempts to start an exit animation for the active hotspot. Fails if
595      * there is no active hotspot.
596      */
tryRippleExit()597     private void tryRippleExit() {
598         if (mRipple != null) {
599             if (mExitingRipples == null) {
600                 mExitingRipples = new RippleForeground[MAX_RIPPLES];
601             }
602             mExitingRipples[mExitingRipplesCount++] = mRipple;
603             mRipple.exit();
604             mRipple = null;
605         }
606     }
607 
608     /**
609      * Cancels and removes the active ripple, all exiting ripples, and the
610      * background. Nothing will be drawn after this method is called.
611      */
clearHotspots()612     private void clearHotspots() {
613         if (mRipple != null) {
614             mRipple.end();
615             mRipple = null;
616             mRippleActive = false;
617         }
618 
619         if (mBackground != null) {
620             mBackground.end();
621             mBackground = null;
622             mBackgroundActive = false;
623         }
624 
625         cancelExitingRipples();
626     }
627 
628     @Override
setHotspotBounds(int left, int top, int right, int bottom)629     public void setHotspotBounds(int left, int top, int right, int bottom) {
630         mOverrideBounds = true;
631         mHotspotBounds.set(left, top, right, bottom);
632 
633         onHotspotBoundsChanged();
634     }
635 
636     @Override
getHotspotBounds(Rect outRect)637     public void getHotspotBounds(Rect outRect) {
638         outRect.set(mHotspotBounds);
639     }
640 
641     /**
642      * Notifies all the animating ripples that the hotspot bounds have changed.
643      */
onHotspotBoundsChanged()644     private void onHotspotBoundsChanged() {
645         final int count = mExitingRipplesCount;
646         final RippleForeground[] ripples = mExitingRipples;
647         for (int i = 0; i < count; i++) {
648             ripples[i].onHotspotBoundsChanged();
649         }
650 
651         if (mRipple != null) {
652             mRipple.onHotspotBoundsChanged();
653         }
654 
655         if (mBackground != null) {
656             mBackground.onHotspotBoundsChanged();
657         }
658     }
659 
660     /**
661      * Populates <code>outline</code> with the first available layer outline,
662      * excluding the mask layer.
663      *
664      * @param outline Outline in which to place the first available layer outline
665      */
666     @Override
getOutline(@onNull Outline outline)667     public void getOutline(@NonNull Outline outline) {
668         final LayerState state = mLayerState;
669         final ChildDrawable[] children = state.mChildren;
670         final int N = state.mNum;
671         for (int i = 0; i < N; i++) {
672             if (children[i].mId != R.id.mask) {
673                 children[i].mDrawable.getOutline(outline);
674                 if (!outline.isEmpty()) return;
675             }
676         }
677     }
678 
679     /**
680      * Optimized for drawing ripples with a mask layer and optional content.
681      */
682     @Override
draw(@onNull Canvas canvas)683     public void draw(@NonNull Canvas canvas) {
684         pruneRipples();
685 
686         // Clip to the dirty bounds, which will be the drawable bounds if we
687         // have a mask or content and the ripple bounds if we're projecting.
688         final Rect bounds = getDirtyBounds();
689         final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
690         canvas.clipRect(bounds);
691 
692         drawContent(canvas);
693         drawBackgroundAndRipples(canvas);
694 
695         canvas.restoreToCount(saveCount);
696     }
697 
698     @Override
invalidateSelf()699     public void invalidateSelf() {
700         invalidateSelf(true);
701     }
702 
invalidateSelf(boolean invalidateMask)703     void invalidateSelf(boolean invalidateMask) {
704         super.invalidateSelf();
705 
706         if (invalidateMask) {
707             // Force the mask to update on the next draw().
708             mHasValidMask = false;
709         }
710 
711     }
712 
pruneRipples()713     private void pruneRipples() {
714         int remaining = 0;
715 
716         // Move remaining entries into pruned spaces.
717         final RippleForeground[] ripples = mExitingRipples;
718         final int count = mExitingRipplesCount;
719         for (int i = 0; i < count; i++) {
720             if (!ripples[i].hasFinishedExit()) {
721                 ripples[remaining++] = ripples[i];
722             }
723         }
724 
725         // Null out the remaining entries.
726         for (int i = remaining; i < count; i++) {
727             ripples[i] = null;
728         }
729 
730         mExitingRipplesCount = remaining;
731     }
732 
733     /**
734      * @return whether we need to use a mask
735      */
updateMaskShaderIfNeeded()736     private void updateMaskShaderIfNeeded() {
737         if (mHasValidMask) {
738             return;
739         }
740 
741         final int maskType = getMaskType();
742         if (maskType == MASK_UNKNOWN) {
743             return;
744         }
745 
746         mHasValidMask = true;
747 
748         final Rect bounds = getBounds();
749         if (maskType == MASK_NONE || bounds.isEmpty()) {
750             if (mMaskBuffer != null) {
751                 mMaskBuffer.recycle();
752                 mMaskBuffer = null;
753                 mMaskShader = null;
754                 mMaskCanvas = null;
755             }
756             mMaskMatrix = null;
757             mMaskColorFilter = null;
758             return;
759         }
760 
761         // Ensure we have a correctly-sized buffer.
762         if (mMaskBuffer == null
763                 || mMaskBuffer.getWidth() != bounds.width()
764                 || mMaskBuffer.getHeight() != bounds.height()) {
765             if (mMaskBuffer != null) {
766                 mMaskBuffer.recycle();
767             }
768 
769             mMaskBuffer = Bitmap.createBitmap(
770                     bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
771             mMaskShader = new BitmapShader(mMaskBuffer,
772                     Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
773             mMaskCanvas = new Canvas(mMaskBuffer);
774         } else {
775             mMaskBuffer.eraseColor(Color.TRANSPARENT);
776         }
777 
778         if (mMaskMatrix == null) {
779             mMaskMatrix = new Matrix();
780         } else {
781             mMaskMatrix.reset();
782         }
783 
784         if (mMaskColorFilter == null) {
785             mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
786         }
787 
788         // Draw the appropriate mask anchored to (0,0).
789         final int left = bounds.left;
790         final int top = bounds.top;
791         mMaskCanvas.translate(-left, -top);
792         if (maskType == MASK_EXPLICIT) {
793             drawMask(mMaskCanvas);
794         } else if (maskType == MASK_CONTENT) {
795             drawContent(mMaskCanvas);
796         }
797         mMaskCanvas.translate(left, top);
798     }
799 
getMaskType()800     private int getMaskType() {
801         if (mRipple == null && mExitingRipplesCount <= 0
802                 && (mBackground == null || !mBackground.isVisible())) {
803             // We might need a mask later.
804             return MASK_UNKNOWN;
805         }
806 
807         if (mMask != null) {
808             if (mMask.getOpacity() == PixelFormat.OPAQUE) {
809                 // Clipping handles opaque explicit masks.
810                 return MASK_NONE;
811             } else {
812                 return MASK_EXPLICIT;
813             }
814         }
815 
816         // Check for non-opaque, non-mask content.
817         final ChildDrawable[] array = mLayerState.mChildren;
818         final int count = mLayerState.mNum;
819         for (int i = 0; i < count; i++) {
820             if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
821                 return MASK_CONTENT;
822             }
823         }
824 
825         // Clipping handles opaque content.
826         return MASK_NONE;
827     }
828 
drawContent(Canvas canvas)829     private void drawContent(Canvas canvas) {
830         // Draw everything except the mask.
831         final ChildDrawable[] array = mLayerState.mChildren;
832         final int count = mLayerState.mNum;
833         for (int i = 0; i < count; i++) {
834             if (array[i].mId != R.id.mask) {
835                 array[i].mDrawable.draw(canvas);
836             }
837         }
838     }
839 
drawBackgroundAndRipples(Canvas canvas)840     private void drawBackgroundAndRipples(Canvas canvas) {
841         final RippleForeground active = mRipple;
842         final RippleBackground background = mBackground;
843         final int count = mExitingRipplesCount;
844         if (active == null && count <= 0 && (background == null || !background.isVisible())) {
845             // Move along, nothing to draw here.
846             return;
847         }
848 
849         final float x = mHotspotBounds.exactCenterX();
850         final float y = mHotspotBounds.exactCenterY();
851         canvas.translate(x, y);
852 
853         updateMaskShaderIfNeeded();
854 
855         // Position the shader to account for canvas translation.
856         if (mMaskShader != null) {
857             final Rect bounds = getBounds();
858             mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
859             mMaskShader.setLocalMatrix(mMaskMatrix);
860         }
861 
862         // Grab the color for the current state and cut the alpha channel in
863         // half so that the ripple and background together yield full alpha.
864         final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
865         final int halfAlpha = (Color.alpha(color) / 2) << 24;
866         final Paint p = getRipplePaint();
867 
868         if (mMaskColorFilter != null) {
869             // The ripple timing depends on the paint's alpha value, so we need
870             // to push just the alpha channel into the paint and let the filter
871             // handle the full-alpha color.
872             final int fullAlphaColor = color | (0xFF << 24);
873             mMaskColorFilter.setColor(fullAlphaColor);
874 
875             p.setColor(halfAlpha);
876             p.setColorFilter(mMaskColorFilter);
877             p.setShader(mMaskShader);
878         } else {
879             final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
880             p.setColor(halfAlphaColor);
881             p.setColorFilter(null);
882             p.setShader(null);
883         }
884 
885         if (background != null && background.isVisible()) {
886             background.draw(canvas, p);
887         }
888 
889         if (count > 0) {
890             final RippleForeground[] ripples = mExitingRipples;
891             for (int i = 0; i < count; i++) {
892                 ripples[i].draw(canvas, p);
893             }
894         }
895 
896         if (active != null) {
897             active.draw(canvas, p);
898         }
899 
900         canvas.translate(-x, -y);
901     }
902 
drawMask(Canvas canvas)903     private void drawMask(Canvas canvas) {
904         mMask.draw(canvas);
905     }
906 
getRipplePaint()907     private Paint getRipplePaint() {
908         if (mRipplePaint == null) {
909             mRipplePaint = new Paint();
910             mRipplePaint.setAntiAlias(true);
911             mRipplePaint.setStyle(Paint.Style.FILL);
912         }
913         return mRipplePaint;
914     }
915 
916     @Override
getDirtyBounds()917     public Rect getDirtyBounds() {
918         if (!isBounded()) {
919             final Rect drawingBounds = mDrawingBounds;
920             final Rect dirtyBounds = mDirtyBounds;
921             dirtyBounds.set(drawingBounds);
922             drawingBounds.setEmpty();
923 
924             final int cX = (int) mHotspotBounds.exactCenterX();
925             final int cY = (int) mHotspotBounds.exactCenterY();
926             final Rect rippleBounds = mTempRect;
927 
928             final RippleForeground[] activeRipples = mExitingRipples;
929             final int N = mExitingRipplesCount;
930             for (int i = 0; i < N; i++) {
931                 activeRipples[i].getBounds(rippleBounds);
932                 rippleBounds.offset(cX, cY);
933                 drawingBounds.union(rippleBounds);
934             }
935 
936             final RippleBackground background = mBackground;
937             if (background != null) {
938                 background.getBounds(rippleBounds);
939                 rippleBounds.offset(cX, cY);
940                 drawingBounds.union(rippleBounds);
941             }
942 
943             dirtyBounds.union(drawingBounds);
944             dirtyBounds.union(super.getDirtyBounds());
945             return dirtyBounds;
946         } else {
947             return getBounds();
948         }
949     }
950 
951     /**
952      * Sets whether to disable RenderThread animations for this ripple.
953      *
954      * @param forceSoftware true if RenderThread animations should be disabled, false otherwise
955      * @hide
956      */
setForceSoftware(boolean forceSoftware)957     public void setForceSoftware(boolean forceSoftware) {
958         mForceSoftware = forceSoftware;
959     }
960 
961     @Override
getConstantState()962     public ConstantState getConstantState() {
963         return mState;
964     }
965 
966     @Override
mutate()967     public Drawable mutate() {
968         super.mutate();
969 
970         // LayerDrawable creates a new state using createConstantState, so
971         // this should always be a safe cast.
972         mState = (RippleState) mLayerState;
973 
974         // The locally cached drawable may have changed.
975         mMask = findDrawableByLayerId(R.id.mask);
976 
977         return this;
978     }
979 
980     @Override
createConstantState(LayerState state, Resources res)981     RippleState createConstantState(LayerState state, Resources res) {
982         return new RippleState(state, this, res);
983     }
984 
985     static class RippleState extends LayerState {
986         int[] mTouchThemeAttrs;
987         ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
988         int mMaxRadius = RADIUS_AUTO;
989 
RippleState(LayerState orig, RippleDrawable owner, Resources res)990         public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
991             super(orig, owner, res);
992 
993             if (orig != null && orig instanceof RippleState) {
994                 final RippleState origs = (RippleState) orig;
995                 mTouchThemeAttrs = origs.mTouchThemeAttrs;
996                 mColor = origs.mColor;
997                 mMaxRadius = origs.mMaxRadius;
998 
999                 if (origs.mDensity != mDensity) {
1000                     applyDensityScaling(orig.mDensity, mDensity);
1001                 }
1002             }
1003         }
1004 
1005         @Override
onDensityChanged(int sourceDensity, int targetDensity)1006         protected void onDensityChanged(int sourceDensity, int targetDensity) {
1007             super.onDensityChanged(sourceDensity, targetDensity);
1008 
1009             applyDensityScaling(sourceDensity, targetDensity);
1010         }
1011 
applyDensityScaling(int sourceDensity, int targetDensity)1012         private void applyDensityScaling(int sourceDensity, int targetDensity) {
1013             if (mMaxRadius != RADIUS_AUTO) {
1014                 mMaxRadius = Drawable.scaleFromDensity(
1015                         mMaxRadius, sourceDensity, targetDensity, true);
1016             }
1017         }
1018 
1019         @Override
canApplyTheme()1020         public boolean canApplyTheme() {
1021             return mTouchThemeAttrs != null
1022                     || (mColor != null && mColor.canApplyTheme())
1023                     || super.canApplyTheme();
1024         }
1025 
1026         @Override
newDrawable()1027         public Drawable newDrawable() {
1028             return new RippleDrawable(this, null);
1029         }
1030 
1031         @Override
newDrawable(Resources res)1032         public Drawable newDrawable(Resources res) {
1033             return new RippleDrawable(this, res);
1034         }
1035 
1036         @Override
getChangingConfigurations()1037         public @Config int getChangingConfigurations() {
1038             return super.getChangingConfigurations()
1039                     | (mColor != null ? mColor.getChangingConfigurations() : 0);
1040         }
1041     }
1042 
RippleDrawable(RippleState state, Resources res)1043     private RippleDrawable(RippleState state, Resources res) {
1044         mState = new RippleState(state, this, res);
1045         mLayerState = mState;
1046         mDensity = Drawable.resolveDensity(res, mState.mDensity);
1047 
1048         if (mState.mNum > 0) {
1049             ensurePadding();
1050             refreshPadding();
1051         }
1052 
1053         updateLocalState();
1054     }
1055 
updateLocalState()1056     private void updateLocalState() {
1057         // Initialize from constant state.
1058         mMask = findDrawableByLayerId(R.id.mask);
1059     }
1060 }
1061