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