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