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  * &lt;ripple android:color="#ffff0000">
63  *   &lt;item 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  * &lt;ripple android:color="#ff00ff00">
76  *   &lt;item android:drawable="@android:color/black" />
77  * &lt;/ripple>
78  *
79  * &lt;!-- A blue ripple drawn atop a drawable resource. --/>
80  * &lt;ripple android:color="#ff0000ff">
81  *   &lt;item 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  * &lt;ripple 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     /** @hide */
381     @Override
hasFocusStateSpecified()382     public boolean hasFocusStateSpecified() {
383         return true;
384     }
385 
386     /**
387      * Sets the ripple color.
388      *
389      * @param color Ripple color as a color state list.
390      *
391      * @attr ref android.R.styleable#RippleDrawable_color
392      */
setColor(ColorStateList color)393     public void setColor(ColorStateList color) {
394         mState.mColor = color;
395         invalidateSelf(false);
396     }
397 
398     /**
399      * Sets the radius in pixels of the fully expanded ripple.
400      *
401      * @param radius ripple radius in pixels, or {@link #RADIUS_AUTO} to
402      *               compute the radius based on the container size
403      * @attr ref android.R.styleable#RippleDrawable_radius
404      */
setRadius(int radius)405     public void setRadius(int radius) {
406         mState.mMaxRadius = radius;
407         invalidateSelf(false);
408     }
409 
410     /**
411      * @return the radius in pixels of the fully expanded ripple if an explicit
412      *         radius has been set, or {@link #RADIUS_AUTO} if the radius is
413      *         computed based on the container size
414      * @attr ref android.R.styleable#RippleDrawable_radius
415      */
getRadius()416     public int getRadius() {
417         return mState.mMaxRadius;
418     }
419 
420     @Override
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)421     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
422             @NonNull AttributeSet attrs, @Nullable Theme theme)
423             throws XmlPullParserException, IOException {
424         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
425 
426         // Force padding default to STACK before inflating.
427         setPaddingMode(PADDING_MODE_STACK);
428 
429         // Inflation will advance the XmlPullParser and AttributeSet.
430         super.inflate(r, parser, attrs, theme);
431 
432         updateStateFromTypedArray(a);
433         verifyRequiredAttributes(a);
434         a.recycle();
435 
436         updateLocalState();
437     }
438 
439     @Override
setDrawableByLayerId(int id, Drawable drawable)440     public boolean setDrawableByLayerId(int id, Drawable drawable) {
441         if (super.setDrawableByLayerId(id, drawable)) {
442             if (id == R.id.mask) {
443                 mMask = drawable;
444                 mHasValidMask = false;
445             }
446 
447             return true;
448         }
449 
450         return false;
451     }
452 
453     /**
454      * Specifies how layer padding should affect the bounds of subsequent
455      * layers. The default and recommended value for RippleDrawable is
456      * {@link #PADDING_MODE_STACK}.
457      *
458      * @param mode padding mode, one of:
459      *            <ul>
460      *            <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
461      *            padding of the previous layer
462      *            <li>{@link #PADDING_MODE_STACK} to stack each layer directly
463      *            atop the previous layer
464      *            </ul>
465      * @see #getPaddingMode()
466      */
467     @Override
setPaddingMode(int mode)468     public void setPaddingMode(int mode) {
469         super.setPaddingMode(mode);
470     }
471 
472     /**
473      * Initializes the constant state from the values in the typed array.
474      */
updateStateFromTypedArray(@onNull TypedArray a)475     private void updateStateFromTypedArray(@NonNull TypedArray a) throws XmlPullParserException {
476         final RippleState state = mState;
477 
478         // Account for any configuration changes.
479         state.mChangingConfigurations |= a.getChangingConfigurations();
480 
481         // Extract the theme attributes, if any.
482         state.mTouchThemeAttrs = a.extractThemeAttrs();
483 
484         final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
485         if (color != null) {
486             mState.mColor = color;
487         }
488 
489         mState.mMaxRadius = a.getDimensionPixelSize(
490                 R.styleable.RippleDrawable_radius, mState.mMaxRadius);
491     }
492 
verifyRequiredAttributes(@onNull TypedArray a)493     private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
494         if (mState.mColor == null && (mState.mTouchThemeAttrs == null
495                 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
496             throw new XmlPullParserException(a.getPositionDescription() +
497                     ": <ripple> requires a valid color attribute");
498         }
499     }
500 
501     @Override
applyTheme(@onNull Theme t)502     public void applyTheme(@NonNull Theme t) {
503         super.applyTheme(t);
504 
505         final RippleState state = mState;
506         if (state == null) {
507             return;
508         }
509 
510         if (state.mTouchThemeAttrs != null) {
511             final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
512                     R.styleable.RippleDrawable);
513             try {
514                 updateStateFromTypedArray(a);
515                 verifyRequiredAttributes(a);
516             } catch (XmlPullParserException e) {
517                 rethrowAsRuntimeException(e);
518             } finally {
519                 a.recycle();
520             }
521         }
522 
523         if (state.mColor != null && state.mColor.canApplyTheme()) {
524             state.mColor = state.mColor.obtainForTheme(t);
525         }
526 
527         updateLocalState();
528     }
529 
530     @Override
canApplyTheme()531     public boolean canApplyTheme() {
532         return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
533     }
534 
535     @Override
setHotspot(float x, float y)536     public void setHotspot(float x, float y) {
537         if (mRipple == null || mBackground == null) {
538             mPendingX = x;
539             mPendingY = y;
540             mHasPending = true;
541         }
542 
543         if (mRipple != null) {
544             mRipple.move(x, y);
545         }
546     }
547 
548     /**
549      * Creates an active hotspot at the specified location.
550      */
tryBackgroundEnter(boolean focused)551     private void tryBackgroundEnter(boolean focused) {
552         if (mBackground == null) {
553             final boolean isBounded = isBounded();
554             mBackground = new RippleBackground(this, mHotspotBounds, isBounded, mForceSoftware);
555         }
556 
557         mBackground.setup(mState.mMaxRadius, mDensity);
558         mBackground.enter(focused);
559     }
560 
tryBackgroundExit()561     private void tryBackgroundExit() {
562         if (mBackground != null) {
563             // Don't null out the background, we need it to draw!
564             mBackground.exit();
565         }
566     }
567 
568     /**
569      * Attempts to start an enter animation for the active hotspot. Fails if
570      * there are too many animating ripples.
571      */
tryRippleEnter()572     private void tryRippleEnter() {
573         if (mExitingRipplesCount >= MAX_RIPPLES) {
574             // This should never happen unless the user is tapping like a maniac
575             // or there is a bug that's preventing ripples from being removed.
576             return;
577         }
578 
579         if (mRipple == null) {
580             final float x;
581             final float y;
582             if (mHasPending) {
583                 mHasPending = false;
584                 x = mPendingX;
585                 y = mPendingY;
586             } else {
587                 x = mHotspotBounds.exactCenterX();
588                 y = mHotspotBounds.exactCenterY();
589             }
590 
591             final boolean isBounded = isBounded();
592             mRipple = new RippleForeground(this, mHotspotBounds, x, y, isBounded, mForceSoftware);
593         }
594 
595         mRipple.setup(mState.mMaxRadius, mDensity);
596         mRipple.enter(false);
597     }
598 
599     /**
600      * Attempts to start an exit animation for the active hotspot. Fails if
601      * there is no active hotspot.
602      */
tryRippleExit()603     private void tryRippleExit() {
604         if (mRipple != null) {
605             if (mExitingRipples == null) {
606                 mExitingRipples = new RippleForeground[MAX_RIPPLES];
607             }
608             mExitingRipples[mExitingRipplesCount++] = mRipple;
609             mRipple.exit();
610             mRipple = null;
611         }
612     }
613 
614     /**
615      * Cancels and removes the active ripple, all exiting ripples, and the
616      * background. Nothing will be drawn after this method is called.
617      */
clearHotspots()618     private void clearHotspots() {
619         if (mRipple != null) {
620             mRipple.end();
621             mRipple = null;
622             mRippleActive = false;
623         }
624 
625         if (mBackground != null) {
626             mBackground.end();
627             mBackground = null;
628             mBackgroundActive = false;
629         }
630 
631         cancelExitingRipples();
632     }
633 
634     @Override
setHotspotBounds(int left, int top, int right, int bottom)635     public void setHotspotBounds(int left, int top, int right, int bottom) {
636         mOverrideBounds = true;
637         mHotspotBounds.set(left, top, right, bottom);
638 
639         onHotspotBoundsChanged();
640     }
641 
642     @Override
getHotspotBounds(Rect outRect)643     public void getHotspotBounds(Rect outRect) {
644         outRect.set(mHotspotBounds);
645     }
646 
647     /**
648      * Notifies all the animating ripples that the hotspot bounds have changed.
649      */
onHotspotBoundsChanged()650     private void onHotspotBoundsChanged() {
651         final int count = mExitingRipplesCount;
652         final RippleForeground[] ripples = mExitingRipples;
653         for (int i = 0; i < count; i++) {
654             ripples[i].onHotspotBoundsChanged();
655         }
656 
657         if (mRipple != null) {
658             mRipple.onHotspotBoundsChanged();
659         }
660 
661         if (mBackground != null) {
662             mBackground.onHotspotBoundsChanged();
663         }
664     }
665 
666     /**
667      * Populates <code>outline</code> with the first available layer outline,
668      * excluding the mask layer.
669      *
670      * @param outline Outline in which to place the first available layer outline
671      */
672     @Override
getOutline(@onNull Outline outline)673     public void getOutline(@NonNull Outline outline) {
674         final LayerState state = mLayerState;
675         final ChildDrawable[] children = state.mChildren;
676         final int N = state.mNumChildren;
677         for (int i = 0; i < N; i++) {
678             if (children[i].mId != R.id.mask) {
679                 children[i].mDrawable.getOutline(outline);
680                 if (!outline.isEmpty()) return;
681             }
682         }
683     }
684 
685     /**
686      * Optimized for drawing ripples with a mask layer and optional content.
687      */
688     @Override
draw(@onNull Canvas canvas)689     public void draw(@NonNull Canvas canvas) {
690         pruneRipples();
691 
692         // Clip to the dirty bounds, which will be the drawable bounds if we
693         // have a mask or content and the ripple bounds if we're projecting.
694         final Rect bounds = getDirtyBounds();
695         final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
696         canvas.clipRect(bounds);
697 
698         drawContent(canvas);
699         drawBackgroundAndRipples(canvas);
700 
701         canvas.restoreToCount(saveCount);
702     }
703 
704     @Override
invalidateSelf()705     public void invalidateSelf() {
706         invalidateSelf(true);
707     }
708 
invalidateSelf(boolean invalidateMask)709     void invalidateSelf(boolean invalidateMask) {
710         super.invalidateSelf();
711 
712         if (invalidateMask) {
713             // Force the mask to update on the next draw().
714             mHasValidMask = false;
715         }
716 
717     }
718 
pruneRipples()719     private void pruneRipples() {
720         int remaining = 0;
721 
722         // Move remaining entries into pruned spaces.
723         final RippleForeground[] ripples = mExitingRipples;
724         final int count = mExitingRipplesCount;
725         for (int i = 0; i < count; i++) {
726             if (!ripples[i].hasFinishedExit()) {
727                 ripples[remaining++] = ripples[i];
728             }
729         }
730 
731         // Null out the remaining entries.
732         for (int i = remaining; i < count; i++) {
733             ripples[i] = null;
734         }
735 
736         mExitingRipplesCount = remaining;
737     }
738 
739     /**
740      * @return whether we need to use a mask
741      */
updateMaskShaderIfNeeded()742     private void updateMaskShaderIfNeeded() {
743         if (mHasValidMask) {
744             return;
745         }
746 
747         final int maskType = getMaskType();
748         if (maskType == MASK_UNKNOWN) {
749             return;
750         }
751 
752         mHasValidMask = true;
753 
754         final Rect bounds = getBounds();
755         if (maskType == MASK_NONE || bounds.isEmpty()) {
756             if (mMaskBuffer != null) {
757                 mMaskBuffer.recycle();
758                 mMaskBuffer = null;
759                 mMaskShader = null;
760                 mMaskCanvas = null;
761             }
762             mMaskMatrix = null;
763             mMaskColorFilter = null;
764             return;
765         }
766 
767         // Ensure we have a correctly-sized buffer.
768         if (mMaskBuffer == null
769                 || mMaskBuffer.getWidth() != bounds.width()
770                 || mMaskBuffer.getHeight() != bounds.height()) {
771             if (mMaskBuffer != null) {
772                 mMaskBuffer.recycle();
773             }
774 
775             mMaskBuffer = Bitmap.createBitmap(
776                     bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
777             mMaskShader = new BitmapShader(mMaskBuffer,
778                     Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
779             mMaskCanvas = new Canvas(mMaskBuffer);
780         } else {
781             mMaskBuffer.eraseColor(Color.TRANSPARENT);
782         }
783 
784         if (mMaskMatrix == null) {
785             mMaskMatrix = new Matrix();
786         } else {
787             mMaskMatrix.reset();
788         }
789 
790         if (mMaskColorFilter == null) {
791             mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
792         }
793 
794         // Draw the appropriate mask anchored to (0,0).
795         final int left = bounds.left;
796         final int top = bounds.top;
797         mMaskCanvas.translate(-left, -top);
798         if (maskType == MASK_EXPLICIT) {
799             drawMask(mMaskCanvas);
800         } else if (maskType == MASK_CONTENT) {
801             drawContent(mMaskCanvas);
802         }
803         mMaskCanvas.translate(left, top);
804     }
805 
getMaskType()806     private int getMaskType() {
807         if (mRipple == null && mExitingRipplesCount <= 0
808                 && (mBackground == null || !mBackground.isVisible())) {
809             // We might need a mask later.
810             return MASK_UNKNOWN;
811         }
812 
813         if (mMask != null) {
814             if (mMask.getOpacity() == PixelFormat.OPAQUE) {
815                 // Clipping handles opaque explicit masks.
816                 return MASK_NONE;
817             } else {
818                 return MASK_EXPLICIT;
819             }
820         }
821 
822         // Check for non-opaque, non-mask content.
823         final ChildDrawable[] array = mLayerState.mChildren;
824         final int count = mLayerState.mNumChildren;
825         for (int i = 0; i < count; i++) {
826             if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
827                 return MASK_CONTENT;
828             }
829         }
830 
831         // Clipping handles opaque content.
832         return MASK_NONE;
833     }
834 
drawContent(Canvas canvas)835     private void drawContent(Canvas canvas) {
836         // Draw everything except the mask.
837         final ChildDrawable[] array = mLayerState.mChildren;
838         final int count = mLayerState.mNumChildren;
839         for (int i = 0; i < count; i++) {
840             if (array[i].mId != R.id.mask) {
841                 array[i].mDrawable.draw(canvas);
842             }
843         }
844     }
845 
drawBackgroundAndRipples(Canvas canvas)846     private void drawBackgroundAndRipples(Canvas canvas) {
847         final RippleForeground active = mRipple;
848         final RippleBackground background = mBackground;
849         final int count = mExitingRipplesCount;
850         if (active == null && count <= 0 && (background == null || !background.isVisible())) {
851             // Move along, nothing to draw here.
852             return;
853         }
854 
855         final float x = mHotspotBounds.exactCenterX();
856         final float y = mHotspotBounds.exactCenterY();
857         canvas.translate(x, y);
858 
859         updateMaskShaderIfNeeded();
860 
861         // Position the shader to account for canvas translation.
862         if (mMaskShader != null) {
863             final Rect bounds = getBounds();
864             mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
865             mMaskShader.setLocalMatrix(mMaskMatrix);
866         }
867 
868         // Grab the color for the current state and cut the alpha channel in
869         // half so that the ripple and background together yield full alpha.
870         final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
871         final int halfAlpha = (Color.alpha(color) / 2) << 24;
872         final Paint p = getRipplePaint();
873 
874         if (mMaskColorFilter != null) {
875             // The ripple timing depends on the paint's alpha value, so we need
876             // to push just the alpha channel into the paint and let the filter
877             // handle the full-alpha color.
878             final int fullAlphaColor = color | (0xFF << 24);
879             mMaskColorFilter.setColor(fullAlphaColor);
880 
881             p.setColor(halfAlpha);
882             p.setColorFilter(mMaskColorFilter);
883             p.setShader(mMaskShader);
884         } else {
885             final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
886             p.setColor(halfAlphaColor);
887             p.setColorFilter(null);
888             p.setShader(null);
889         }
890 
891         if (background != null && background.isVisible()) {
892             background.draw(canvas, p);
893         }
894 
895         if (count > 0) {
896             final RippleForeground[] ripples = mExitingRipples;
897             for (int i = 0; i < count; i++) {
898                 ripples[i].draw(canvas, p);
899             }
900         }
901 
902         if (active != null) {
903             active.draw(canvas, p);
904         }
905 
906         canvas.translate(-x, -y);
907     }
908 
drawMask(Canvas canvas)909     private void drawMask(Canvas canvas) {
910         mMask.draw(canvas);
911     }
912 
getRipplePaint()913     private Paint getRipplePaint() {
914         if (mRipplePaint == null) {
915             mRipplePaint = new Paint();
916             mRipplePaint.setAntiAlias(true);
917             mRipplePaint.setStyle(Paint.Style.FILL);
918         }
919         return mRipplePaint;
920     }
921 
922     @Override
getDirtyBounds()923     public Rect getDirtyBounds() {
924         if (!isBounded()) {
925             final Rect drawingBounds = mDrawingBounds;
926             final Rect dirtyBounds = mDirtyBounds;
927             dirtyBounds.set(drawingBounds);
928             drawingBounds.setEmpty();
929 
930             final int cX = (int) mHotspotBounds.exactCenterX();
931             final int cY = (int) mHotspotBounds.exactCenterY();
932             final Rect rippleBounds = mTempRect;
933 
934             final RippleForeground[] activeRipples = mExitingRipples;
935             final int N = mExitingRipplesCount;
936             for (int i = 0; i < N; i++) {
937                 activeRipples[i].getBounds(rippleBounds);
938                 rippleBounds.offset(cX, cY);
939                 drawingBounds.union(rippleBounds);
940             }
941 
942             final RippleBackground background = mBackground;
943             if (background != null) {
944                 background.getBounds(rippleBounds);
945                 rippleBounds.offset(cX, cY);
946                 drawingBounds.union(rippleBounds);
947             }
948 
949             dirtyBounds.union(drawingBounds);
950             dirtyBounds.union(super.getDirtyBounds());
951             return dirtyBounds;
952         } else {
953             return getBounds();
954         }
955     }
956 
957     /**
958      * Sets whether to disable RenderThread animations for this ripple.
959      *
960      * @param forceSoftware true if RenderThread animations should be disabled, false otherwise
961      * @hide
962      */
setForceSoftware(boolean forceSoftware)963     public void setForceSoftware(boolean forceSoftware) {
964         mForceSoftware = forceSoftware;
965     }
966 
967     @Override
getConstantState()968     public ConstantState getConstantState() {
969         return mState;
970     }
971 
972     @Override
mutate()973     public Drawable mutate() {
974         super.mutate();
975 
976         // LayerDrawable creates a new state using createConstantState, so
977         // this should always be a safe cast.
978         mState = (RippleState) mLayerState;
979 
980         // The locally cached drawable may have changed.
981         mMask = findDrawableByLayerId(R.id.mask);
982 
983         return this;
984     }
985 
986     @Override
createConstantState(LayerState state, Resources res)987     RippleState createConstantState(LayerState state, Resources res) {
988         return new RippleState(state, this, res);
989     }
990 
991     static class RippleState extends LayerState {
992         int[] mTouchThemeAttrs;
993         ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
994         int mMaxRadius = RADIUS_AUTO;
995 
RippleState(LayerState orig, RippleDrawable owner, Resources res)996         public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
997             super(orig, owner, res);
998 
999             if (orig != null && orig instanceof RippleState) {
1000                 final RippleState origs = (RippleState) orig;
1001                 mTouchThemeAttrs = origs.mTouchThemeAttrs;
1002                 mColor = origs.mColor;
1003                 mMaxRadius = origs.mMaxRadius;
1004 
1005                 if (origs.mDensity != mDensity) {
1006                     applyDensityScaling(orig.mDensity, mDensity);
1007                 }
1008             }
1009         }
1010 
1011         @Override
onDensityChanged(int sourceDensity, int targetDensity)1012         protected void onDensityChanged(int sourceDensity, int targetDensity) {
1013             super.onDensityChanged(sourceDensity, targetDensity);
1014 
1015             applyDensityScaling(sourceDensity, targetDensity);
1016         }
1017 
applyDensityScaling(int sourceDensity, int targetDensity)1018         private void applyDensityScaling(int sourceDensity, int targetDensity) {
1019             if (mMaxRadius != RADIUS_AUTO) {
1020                 mMaxRadius = Drawable.scaleFromDensity(
1021                         mMaxRadius, sourceDensity, targetDensity, true);
1022             }
1023         }
1024 
1025         @Override
canApplyTheme()1026         public boolean canApplyTheme() {
1027             return mTouchThemeAttrs != null
1028                     || (mColor != null && mColor.canApplyTheme())
1029                     || super.canApplyTheme();
1030         }
1031 
1032         @Override
newDrawable()1033         public Drawable newDrawable() {
1034             return new RippleDrawable(this, null);
1035         }
1036 
1037         @Override
newDrawable(Resources res)1038         public Drawable newDrawable(Resources res) {
1039             return new RippleDrawable(this, res);
1040         }
1041 
1042         @Override
getChangingConfigurations()1043         public @Config int getChangingConfigurations() {
1044             return super.getChangingConfigurations()
1045                     | (mColor != null ? mColor.getChangingConfigurations() : 0);
1046         }
1047     }
1048 
RippleDrawable(RippleState state, Resources res)1049     private RippleDrawable(RippleState state, Resources res) {
1050         mState = new RippleState(state, this, res);
1051         mLayerState = mState;
1052         mDensity = Drawable.resolveDensity(res, mState.mDensity);
1053 
1054         if (mState.mNumChildren > 0) {
1055             ensurePadding();
1056             refreshPadding();
1057         }
1058 
1059         updateLocalState();
1060     }
1061 
updateLocalState()1062     private void updateLocalState() {
1063         // Initialize from constant state.
1064         mMask = findDrawableByLayerId(R.id.mask);
1065     }
1066 }
1067