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