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