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