RippleDrawable.java revision cc3c573334a9cd2124a8a0ccf2f37884e36f83fa
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
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.Canvas;
31import android.graphics.Color;
32import android.graphics.ColorFilter;
33import android.graphics.Outline;
34import android.graphics.Paint;
35import android.graphics.PixelFormat;
36import android.graphics.PorterDuff.Mode;
37import android.graphics.PorterDuffXfermode;
38import android.graphics.Rect;
39import android.util.AttributeSet;
40import android.util.DisplayMetrics;
41
42import java.io.IOException;
43import java.util.Arrays;
44
45/**
46 * Drawable that shows a ripple effect in response to state changes. The
47 * anchoring position of the ripple for a given state may be specified by
48 * calling {@link #setHotspot(float, float)} with the corresponding state
49 * attribute identifier.
50 * <p>
51 * A touch feedback drawable may contain multiple child layers, including a
52 * special mask layer that is not drawn to the screen. A single layer may be set
53 * as the mask by specifying its android:id value as {@link android.R.id#mask}.
54 * <pre>
55 * <code>&lt!-- A red ripple masked against an opaque rectangle. --/>
56 * &ltripple android:color="#ffff0000">
57 *   &ltitem android:id="@android:id/mask"
58 *         android:drawable="@android:color/white" />
59 * &ltripple /></code>
60 * </pre>
61 * <p>
62 * If a mask layer is set, the ripple effect will be masked against that layer
63 * before it is drawn over the composite of the remaining child layers.
64 * <p>
65 * If no mask layer is set, the ripple effect is masked against the composite
66 * of the child layers.
67 * <pre>
68 * <code>&lt!-- A blue ripple drawn atop a black rectangle. --/>
69 * &ltripple android:color="#ff00ff00">
70 *   &ltitem android:drawable="@android:color/black" />
71 * &ltripple />
72 *
73 * &lt!-- A red ripple drawn atop a drawable resource. --/>
74 * &ltripple android:color="#ff00ff00">
75 *   &ltitem android:drawable="@drawable/my_drawable" />
76 * &ltripple /></code>
77 * </pre>
78 * <p>
79 * If no child layers or mask is specified and the ripple is set as a View
80 * background, the ripple will be drawn atop the first available parent
81 * background within the View's hierarchy. In this case, the drawing region
82 * may extend outside of the Drawable bounds.
83 * <pre>
84 * <code>&lt!-- An unbounded green ripple. --/>
85 * &ltripple android:color="#ff0000ff" /></code>
86 * </pre>
87 *
88 * @attr ref android.R.styleable#RippleDrawable_color
89 */
90public class RippleDrawable extends LayerDrawable {
91    private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN);
92    private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP);
93    private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(Mode.SRC_OVER);
94
95    /**
96     * Constant for automatically determining the maximum ripple radius.
97     *
98     * @see #setMaxRadius(int)
99     * @hide
100     */
101    public static final int RADIUS_AUTO = -1;
102
103    /** The maximum number of ripples supported. */
104    private static final int MAX_RIPPLES = 10;
105
106    private final Rect mTempRect = new Rect();
107
108    /** Current ripple effect bounds, used to constrain ripple effects. */
109    private final Rect mHotspotBounds = new Rect();
110
111    /** Current drawing bounds, used to compute dirty region. */
112    private final Rect mDrawingBounds = new Rect();
113
114    /** Current dirty bounds, union of current and previous drawing bounds. */
115    private final Rect mDirtyBounds = new Rect();
116
117    /** Mirrors mLayerState with some extra information. */
118    private RippleState mState;
119
120    /** The masking layer, e.g. the layer with id R.id.mask. */
121    private Drawable mMask;
122
123    /** The current background. May be actively animating or pending entry. */
124    private RippleBackground mBackground;
125
126    /** Whether we expect to draw a background when visible. */
127    private boolean mBackgroundActive;
128
129    /** The current ripple. May be actively animating or pending entry. */
130    private Ripple mRipple;
131
132    /** Whether we expect to draw a ripple when visible. */
133    private boolean mRippleActive;
134
135    // Hotspot coordinates that are awaiting activation.
136    private float mPendingX;
137    private float mPendingY;
138    private boolean mHasPending;
139
140    /**
141     * Lazily-created array of actively animating ripples. Inactive ripples are
142     * pruned during draw(). The locations of these will not change.
143     */
144    private Ripple[] mExitingRipples;
145    private int mExitingRipplesCount = 0;
146
147    /** Paint used to control appearance of ripples. */
148    private Paint mRipplePaint;
149
150    /** Paint used to control reveal layer masking. */
151    private Paint mMaskingPaint;
152
153    /** Target density of the display into which ripples are drawn. */
154    private float mDensity = 1.0f;
155
156    /** Whether bounds are being overridden. */
157    private boolean mOverrideBounds;
158
159    /**
160     * Constructor used for drawable inflation.
161     */
162    RippleDrawable() {
163        this(new RippleState(null, null, null), null);
164    }
165
166    /**
167     * Creates a new ripple drawable with the specified ripple color and
168     * optional content and mask drawables.
169     *
170     * @param color The ripple color
171     * @param content The content drawable, may be {@code null}
172     * @param mask The mask drawable, may be {@code null}
173     */
174    public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content,
175            @Nullable Drawable mask) {
176        this(new RippleState(null, null, null), null);
177
178        if (color == null) {
179            throw new IllegalArgumentException("RippleDrawable requires a non-null color");
180        }
181
182        if (content != null) {
183            addLayer(content, null, 0, 0, 0, 0, 0);
184        }
185
186        if (mask != null) {
187            addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0);
188        }
189
190        setColor(color);
191        ensurePadding();
192        initializeFromState();
193    }
194
195    @Override
196    public void jumpToCurrentState() {
197        super.jumpToCurrentState();
198
199        if (mRipple != null) {
200            mRipple.jump();
201        }
202
203        if (mBackground != null) {
204            mBackground.jump();
205        }
206
207        cancelExitingRipples();
208        invalidateSelf();
209    }
210
211    private boolean cancelExitingRipples() {
212        boolean needsDraw = false;
213
214        final int count = mExitingRipplesCount;
215        final Ripple[] ripples = mExitingRipples;
216        for (int i = 0; i < count; i++) {
217            needsDraw |= ripples[i].isHardwareAnimating();
218            ripples[i].cancel();
219        }
220
221        if (ripples != null) {
222            Arrays.fill(ripples, 0, count, null);
223        }
224        mExitingRipplesCount = 0;
225
226        return needsDraw;
227    }
228
229    @Override
230    public void setAlpha(int alpha) {
231        super.setAlpha(alpha);
232
233        // TODO: Should we support this?
234    }
235
236    @Override
237    public void setColorFilter(ColorFilter cf) {
238        super.setColorFilter(cf);
239
240        // TODO: Should we support this?
241    }
242
243    @Override
244    public int getOpacity() {
245        // Worst-case scenario.
246        return PixelFormat.TRANSLUCENT;
247    }
248
249    @Override
250    protected boolean onStateChange(int[] stateSet) {
251        final boolean changed = super.onStateChange(stateSet);
252
253        boolean enabled = false;
254        boolean pressed = false;
255        boolean focused = false;
256
257        for (int state : stateSet) {
258            if (state == R.attr.state_enabled) {
259                enabled = true;
260            }
261            if (state == R.attr.state_focused) {
262                focused = true;
263            }
264            if (state == R.attr.state_pressed) {
265                pressed = true;
266            }
267        }
268
269        setRippleActive(enabled && pressed);
270        setBackgroundActive(focused || (enabled && pressed), focused);
271
272        return changed;
273    }
274
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
286    private void setBackgroundActive(boolean active, boolean focused) {
287        if (mBackgroundActive != active) {
288            mBackgroundActive = active;
289            if (active) {
290                tryBackgroundEnter(focused);
291            } else {
292                tryBackgroundExit();
293            }
294        }
295    }
296
297    @Override
298    protected void onBoundsChange(Rect bounds) {
299        super.onBoundsChange(bounds);
300
301        if (!mOverrideBounds) {
302            mHotspotBounds.set(bounds);
303            onHotspotBoundsChanged();
304        }
305
306        invalidateSelf();
307    }
308
309    @Override
310    public boolean setVisible(boolean visible, boolean restart) {
311        final boolean changed = super.setVisible(visible, restart);
312
313        if (!visible) {
314            clearHotspots();
315        } else if (changed) {
316            // If we just became visible, ensure the background and ripple
317            // visibilities are consistent with their internal states.
318            if (mRippleActive) {
319                tryRippleEnter();
320            }
321
322            if (mBackgroundActive) {
323                tryBackgroundEnter(false);
324            }
325
326            // Skip animations, just show the correct final states.
327            jumpToCurrentState();
328        }
329
330        return changed;
331    }
332
333    /**
334     * @hide
335     */
336    @Override
337    public boolean isProjected() {
338        return getNumberOfLayers() == 0;
339    }
340
341    @Override
342    public boolean isStateful() {
343        return true;
344    }
345
346    public void setColor(ColorStateList color) {
347        mState.mColor = color;
348        invalidateSelf();
349    }
350
351    @Override
352    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
353            throws XmlPullParserException, IOException {
354        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
355        updateStateFromTypedArray(a);
356        a.recycle();
357
358        // Force padding default to STACK before inflating.
359        setPaddingMode(PADDING_MODE_STACK);
360
361        super.inflate(r, parser, attrs, theme);
362
363        setTargetDensity(r.getDisplayMetrics());
364        initializeFromState();
365    }
366
367    @Override
368    public boolean setDrawableByLayerId(int id, Drawable drawable) {
369        if (super.setDrawableByLayerId(id, drawable)) {
370            if (id == R.id.mask) {
371                mMask = drawable;
372            }
373
374            return true;
375        }
376
377        return false;
378    }
379
380    /**
381     * Specifies how layer padding should affect the bounds of subsequent
382     * layers. The default and recommended value for RippleDrawable is
383     * {@link #PADDING_MODE_STACK}.
384     *
385     * @param mode padding mode, one of:
386     *            <ul>
387     *            <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
388     *            padding of the previous layer
389     *            <li>{@link #PADDING_MODE_STACK} to stack each layer directly
390     *            atop the previous layer
391     *            </ul>
392     * @see #getPaddingMode()
393     */
394    @Override
395    public void setPaddingMode(int mode) {
396        super.setPaddingMode(mode);
397    }
398
399    /**
400     * Initializes the constant state from the values in the typed array.
401     */
402    private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
403        final RippleState state = mState;
404
405        // Account for any configuration changes.
406        state.mChangingConfigurations |= a.getChangingConfigurations();
407
408        // Extract the theme attributes, if any.
409        state.mTouchThemeAttrs = a.extractThemeAttrs();
410
411        final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
412        if (color != null) {
413            mState.mColor = color;
414        }
415
416        verifyRequiredAttributes(a);
417    }
418
419    private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
420        if (mState.mColor == null && (mState.mTouchThemeAttrs == null
421                || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
422            throw new XmlPullParserException(a.getPositionDescription() +
423                    ": <ripple> requires a valid color attribute");
424        }
425    }
426
427    /**
428     * Set the density at which this drawable will be rendered.
429     *
430     * @param metrics The display metrics for this drawable.
431     */
432    private void setTargetDensity(DisplayMetrics metrics) {
433        if (mDensity != metrics.density) {
434            mDensity = metrics.density;
435            invalidateSelf();
436        }
437    }
438
439    @Override
440    public void applyTheme(Theme t) {
441        super.applyTheme(t);
442
443        final RippleState state = mState;
444        if (state == null || state.mTouchThemeAttrs == null) {
445            return;
446        }
447
448        final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
449                R.styleable.RippleDrawable);
450        try {
451            updateStateFromTypedArray(a);
452        } catch (XmlPullParserException e) {
453            throw new RuntimeException(e);
454        } finally {
455            a.recycle();
456        }
457
458        initializeFromState();
459    }
460
461    @Override
462    public boolean canApplyTheme() {
463        return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
464    }
465
466    @Override
467    public void setHotspot(float x, float y) {
468        if (mRipple == null || mBackground == null) {
469            mPendingX = x;
470            mPendingY = y;
471            mHasPending = true;
472        }
473
474        if (mRipple != null) {
475            mRipple.move(x, y);
476        }
477    }
478
479    /**
480     * Creates an active hotspot at the specified location.
481     */
482    private void tryBackgroundEnter(boolean focused) {
483        if (mBackground == null) {
484            mBackground = new RippleBackground(this, mHotspotBounds);
485        }
486
487        mBackground.setup(mState.mMaxRadius, mDensity);
488        mBackground.enter(focused);
489    }
490
491    private void tryBackgroundExit() {
492        if (mBackground != null) {
493            // Don't null out the background, we need it to draw!
494            mBackground.exit();
495        }
496    }
497
498    /**
499     * Attempts to start an enter animation for the active hotspot. Fails if
500     * there are too many animating ripples.
501     */
502    private void tryRippleEnter() {
503        if (mExitingRipplesCount >= MAX_RIPPLES) {
504            // This should never happen unless the user is tapping like a maniac
505            // or there is a bug that's preventing ripples from being removed.
506            return;
507        }
508
509        if (mRipple == null) {
510            final float x;
511            final float y;
512            if (mHasPending) {
513                mHasPending = false;
514                x = mPendingX;
515                y = mPendingY;
516            } else {
517                x = mHotspotBounds.exactCenterX();
518                y = mHotspotBounds.exactCenterY();
519            }
520            mRipple = new Ripple(this, mHotspotBounds, x, y);
521        }
522
523        mRipple.setup(mState.mMaxRadius, mDensity);
524        mRipple.enter();
525    }
526
527    /**
528     * Attempts to start an exit animation for the active hotspot. Fails if
529     * there is no active hotspot.
530     */
531    private void tryRippleExit() {
532        if (mRipple != null) {
533            if (mExitingRipples == null) {
534                mExitingRipples = new Ripple[MAX_RIPPLES];
535            }
536            mExitingRipples[mExitingRipplesCount++] = mRipple;
537            mRipple.exit();
538            mRipple = null;
539        }
540    }
541
542    /**
543     * Cancels and removes the active ripple, all exiting ripples, and the
544     * background. Nothing will be drawn after this method is called.
545     */
546    private void clearHotspots() {
547        if (mRipple != null) {
548            mRipple.cancel();
549            mRipple = null;
550        }
551
552        if (mBackground != null) {
553            mBackground.cancel();
554            mBackground = null;
555        }
556
557        cancelExitingRipples();
558        invalidateSelf();
559    }
560
561    @Override
562    public void setHotspotBounds(int left, int top, int right, int bottom) {
563        mOverrideBounds = true;
564        mHotspotBounds.set(left, top, right, bottom);
565
566        onHotspotBoundsChanged();
567    }
568
569    /** @hide */
570    @Override
571    public void getHotspotBounds(Rect outRect) {
572        outRect.set(mHotspotBounds);
573    }
574
575    /**
576     * Notifies all the animating ripples that the hotspot bounds have changed.
577     */
578    private void onHotspotBoundsChanged() {
579        final int count = mExitingRipplesCount;
580        final Ripple[] ripples = mExitingRipples;
581        for (int i = 0; i < count; i++) {
582            ripples[i].onHotspotBoundsChanged();
583        }
584
585        if (mRipple != null) {
586            mRipple.onHotspotBoundsChanged();
587        }
588
589        if (mBackground != null) {
590            mBackground.onHotspotBoundsChanged();
591        }
592    }
593
594    /**
595     * Populates <code>outline</code> with the first available layer outline,
596     * excluding the mask layer.
597     *
598     * @param outline Outline in which to place the first available layer outline
599     */
600    @Override
601    public void getOutline(@NonNull Outline outline) {
602        final LayerState state = mLayerState;
603        final ChildDrawable[] children = state.mChildren;
604        final int N = state.mNum;
605        for (int i = 0; i < N; i++) {
606            if (children[i].mId != R.id.mask) {
607                children[i].mDrawable.getOutline(outline);
608                if (!outline.isEmpty()) return;
609            }
610        }
611    }
612
613    /**
614     * Optimized for drawing ripples with a mask layer and optional content.
615     */
616    @Override
617    public void draw(@NonNull Canvas canvas) {
618        final boolean hasMask = mMask != null;
619        final boolean hasRipples = mRipple != null || mExitingRipplesCount > 0
620                || (mBackground != null && mBackground.shouldDraw());
621
622        // Clip to the dirty bounds, which will be the drawable bounds if we
623        // have a mask or content and the ripple bounds if we're projecting.
624        final Rect bounds = getDirtyBounds();
625        final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
626        canvas.clipRect(bounds);
627
628        // If we have content, draw it first. If we have ripples and no mask,
629        // we'll draw it into a SRC_OVER layer so that we can mask ripples
630        // against it using SRC_IN.
631        final boolean hasContentLayer = drawContent(canvas, bounds, hasRipples, hasMask);
632
633        // Next, try to draw the ripples. If we have a non-opaque mask, we'll
634        // draw the ripples into a SRC_OVER layer, draw the mask into a DST_IN
635        // layer, and blend.
636        if (hasRipples) {
637            final boolean hasNonOpaqueMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE;
638            final boolean hasRippleLayer = drawBackgroundAndRipples(canvas, bounds,
639                    hasNonOpaqueMask, hasContentLayer);
640
641            // If drawing ripples created a layer, we have a non-opaque mask
642            // that needs to be blended on top of the ripples with DST_IN.
643            if (hasRippleLayer) {
644                drawMaskingLayer(canvas, bounds, DST_IN);
645            }
646        }
647
648        canvas.restoreToCount(saveCount);
649    }
650
651    /**
652     * Removes a ripple from the exiting ripple list.
653     *
654     * @param ripple the ripple to remove
655     */
656    void removeRipple(Ripple ripple) {
657        // Ripple ripple ripple ripple. Ripple ripple.
658        final Ripple[] ripples = mExitingRipples;
659        final int count = mExitingRipplesCount;
660        final int index = getRippleIndex(ripple);
661        if (index >= 0) {
662            System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
663            ripples[count - 1] = null;
664            mExitingRipplesCount--;
665
666            invalidateSelf();
667        }
668    }
669
670    private int getRippleIndex(Ripple ripple) {
671        final Ripple[] ripples = mExitingRipples;
672        final int count = mExitingRipplesCount;
673        for (int i = 0; i < count; i++) {
674            if (ripples[i] == ripple) {
675                return i;
676            }
677        }
678        return -1;
679    }
680
681    private boolean drawContent(Canvas canvas, Rect bounds, boolean hasRipples, boolean hasMask) {
682        final ChildDrawable[] array = mLayerState.mChildren;
683        final int count = mLayerState.mNum;
684
685        boolean needsLayer = false;
686
687        if (hasRipples && !hasMask) {
688            // If we only have opaque content, we don't really need a layer
689            // because the ripples will be clipped to the drawable bounds.
690            for (int i = 0; i < count; i++) {
691                if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
692                    needsLayer = true;
693                    break;
694                }
695            }
696        }
697
698        if (needsLayer) {
699            canvas.saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom,
700                    getMaskingPaint(SRC_OVER));
701        }
702
703        // Draw everything except the mask.
704        for (int i = 0; i < count; i++) {
705            if (array[i].mId != R.id.mask) {
706                array[i].mDrawable.draw(canvas);
707            }
708        }
709
710        return needsLayer;
711    }
712
713    private boolean drawBackgroundAndRipples(
714            Canvas canvas, Rect bounds, boolean hasNonOpaqueMask, boolean hasContentLayer) {
715        if (hasNonOpaqueMask) {
716            final Paint p = getMaskingPaint(SRC_OVER);
717            canvas.saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, p);
718        }
719
720        final PorterDuffXfermode mode = hasContentLayer ? SRC_ATOP : SRC_OVER;
721        final float x = mHotspotBounds.exactCenterX();
722        final float y = mHotspotBounds.exactCenterY();
723        canvas.translate(x, y);
724
725        final Paint p = getRipplePaint();
726        p.setXfermode(mode);
727
728        // Grab the color for the current state and cut the alpha channel in
729        // half so that the ripple and background together yield full alpha.
730        final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
731        final int alpha = (Color.alpha(color) / 2) << 24;
732        p.setColor(color & 0xFFFFFF | alpha);
733
734        final RippleBackground background = mBackground;
735        if (background != null && background.shouldDraw()) {
736            background.draw(canvas, p);
737        }
738
739        final int count = mExitingRipplesCount;
740        if (count > 0) {
741            final Ripple[] ripples = mExitingRipples;
742            for (int i = 0; i < count; i++) {
743                ripples[i].draw(canvas, p);
744            }
745        }
746
747        final Ripple active = mRipple;
748        if (active != null) {
749            active.draw(canvas, p);
750        }
751
752        canvas.translate(-x, -y);
753
754        // Returns true if a layer was created.
755        return hasNonOpaqueMask;
756    }
757
758    private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
759        final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
760                bounds.right, bounds.bottom, getMaskingPaint(mode));
761
762        // Ensure that DST_IN blends using the entire layer.
763        canvas.drawColor(Color.TRANSPARENT);
764
765        mMask.draw(canvas);
766
767        return restoreToCount;
768    }
769
770    private Paint getRipplePaint() {
771        if (mRipplePaint == null) {
772            mRipplePaint = new Paint();
773            mRipplePaint.setAntiAlias(true);
774            mRipplePaint.setStyle(Paint.Style.FILL);
775        }
776        return mRipplePaint;
777    }
778
779    private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
780        if (mMaskingPaint == null) {
781            mMaskingPaint = new Paint();
782        }
783        mMaskingPaint.setXfermode(xfermode);
784        mMaskingPaint.setAlpha(0xFF);
785        return mMaskingPaint;
786    }
787
788    @Override
789    public Rect getDirtyBounds() {
790        if (isProjected()) {
791            final Rect drawingBounds = mDrawingBounds;
792            final Rect dirtyBounds = mDirtyBounds;
793            dirtyBounds.set(drawingBounds);
794            drawingBounds.setEmpty();
795
796            final int cX = (int) mHotspotBounds.exactCenterX();
797            final int cY = (int) mHotspotBounds.exactCenterY();
798            final Rect rippleBounds = mTempRect;
799
800            final Ripple[] activeRipples = mExitingRipples;
801            final int N = mExitingRipplesCount;
802            for (int i = 0; i < N; i++) {
803                activeRipples[i].getBounds(rippleBounds);
804                rippleBounds.offset(cX, cY);
805                drawingBounds.union(rippleBounds);
806            }
807
808            final RippleBackground background = mBackground;
809            if (background != null) {
810                background.getBounds(rippleBounds);
811                rippleBounds.offset(cX, cY);
812                drawingBounds.union(rippleBounds);
813            }
814
815            dirtyBounds.union(drawingBounds);
816            dirtyBounds.union(super.getDirtyBounds());
817            return dirtyBounds;
818        } else {
819            return getBounds();
820        }
821    }
822
823    @Override
824    public ConstantState getConstantState() {
825        return mState;
826    }
827
828    @Override
829    public Drawable mutate() {
830        super.mutate();
831
832        // LayerDrawable creates a new state using createConstantState, so
833        // this should always be a safe cast.
834        mState = (RippleState) mLayerState;
835        return this;
836    }
837
838    @Override
839    RippleState createConstantState(LayerState state, Resources res) {
840        return new RippleState(state, this, res);
841    }
842
843    static class RippleState extends LayerState {
844        int[] mTouchThemeAttrs;
845        ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
846        int mMaxRadius = RADIUS_AUTO;
847
848        public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
849            super(orig, owner, res);
850
851            if (orig != null && orig instanceof RippleState) {
852                final RippleState origs = (RippleState) orig;
853                mTouchThemeAttrs = origs.mTouchThemeAttrs;
854                mColor = origs.mColor;
855                mMaxRadius = origs.mMaxRadius;
856            }
857        }
858
859        @Override
860        public boolean canApplyTheme() {
861            return mTouchThemeAttrs != null || super.canApplyTheme();
862        }
863
864        @Override
865        public Drawable newDrawable() {
866            return new RippleDrawable(this, null);
867        }
868
869        @Override
870        public Drawable newDrawable(Resources res) {
871            return new RippleDrawable(this, res);
872        }
873    }
874
875    /**
876     * Sets the maximum ripple radius in pixels. The default value of
877     * {@link #RADIUS_AUTO} defines the radius as the distance from the center
878     * of the drawable bounds (or hotspot bounds, if specified) to a corner.
879     *
880     * @param maxRadius the maximum ripple radius in pixels or
881     *            {@link #RADIUS_AUTO} to automatically determine the maximum
882     *            radius based on the bounds
883     * @see #getMaxRadius()
884     * @see #setHotspotBounds(int, int, int, int)
885     * @hide
886     */
887    public void setMaxRadius(int maxRadius) {
888        if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
889            throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
890        }
891
892        mState.mMaxRadius = maxRadius;
893    }
894
895    /**
896     * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
897     *         the radius is determined automatically
898     * @see #setMaxRadius(int)
899     * @hide
900     */
901    public int getMaxRadius() {
902        return mState.mMaxRadius;
903    }
904
905    private RippleDrawable(RippleState state, Resources res) {
906        mState = new RippleState(state, this, res);
907        mLayerState = mState;
908
909        if (mState.mNum > 0) {
910            ensurePadding();
911        }
912
913        if (res != null) {
914            mDensity = res.getDisplayMetrics().density;
915        }
916
917        initializeFromState();
918    }
919
920    private void initializeFromState() {
921        // Initialize from constant state.
922        mMask = findDrawableByLayerId(R.id.mask);
923    }
924}
925