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