RippleDrawable.java revision b942b6f15c51c2ff48c59d8f620ee6156d00f67e
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            mRippleActive = false;
560        }
561
562        if (mBackground != null) {
563            mBackground.cancel();
564            mBackground = null;
565            mBackgroundActive = false;
566        }
567
568        cancelExitingRipples();
569        invalidateSelf();
570    }
571
572    @Override
573    public void setHotspotBounds(int left, int top, int right, int bottom) {
574        mOverrideBounds = true;
575        mHotspotBounds.set(left, top, right, bottom);
576
577        onHotspotBoundsChanged();
578    }
579
580    /** @hide */
581    @Override
582    public void getHotspotBounds(Rect outRect) {
583        outRect.set(mHotspotBounds);
584    }
585
586    /**
587     * Notifies all the animating ripples that the hotspot bounds have changed.
588     */
589    private void onHotspotBoundsChanged() {
590        final int count = mExitingRipplesCount;
591        final Ripple[] ripples = mExitingRipples;
592        for (int i = 0; i < count; i++) {
593            ripples[i].onHotspotBoundsChanged();
594        }
595
596        if (mRipple != null) {
597            mRipple.onHotspotBoundsChanged();
598        }
599
600        if (mBackground != null) {
601            mBackground.onHotspotBoundsChanged();
602        }
603    }
604
605    /**
606     * Populates <code>outline</code> with the first available layer outline,
607     * excluding the mask layer.
608     *
609     * @param outline Outline in which to place the first available layer outline
610     */
611    @Override
612    public void getOutline(@NonNull Outline outline) {
613        final LayerState state = mLayerState;
614        final ChildDrawable[] children = state.mChildren;
615        final int N = state.mNum;
616        for (int i = 0; i < N; i++) {
617            if (children[i].mId != R.id.mask) {
618                children[i].mDrawable.getOutline(outline);
619                if (!outline.isEmpty()) return;
620            }
621        }
622    }
623
624    /**
625     * Optimized for drawing ripples with a mask layer and optional content.
626     */
627    @Override
628    public void draw(@NonNull Canvas canvas) {
629        // Clip to the dirty bounds, which will be the drawable bounds if we
630        // have a mask or content and the ripple bounds if we're projecting.
631        final Rect bounds = getDirtyBounds();
632        final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
633        canvas.clipRect(bounds);
634
635        drawContent(canvas);
636        drawBackgroundAndRipples(canvas);
637
638        canvas.restoreToCount(saveCount);
639    }
640
641    @Override
642    public void invalidateSelf() {
643        super.invalidateSelf();
644
645        // Force the mask to update on the next draw().
646        mHasValidMask = false;
647    }
648
649    /**
650     * @return whether we need to use a mask
651     */
652    private void updateMaskShaderIfNeeded() {
653        if (mHasValidMask) {
654            return;
655        }
656
657        final int maskType = getMaskType();
658        if (maskType == MASK_UNKNOWN) {
659            return;
660        }
661
662        mHasValidMask = true;
663
664        if (maskType == MASK_NONE) {
665            if (mMaskBuffer != null) {
666                mMaskBuffer.recycle();
667                mMaskBuffer = null;
668                mMaskShader = null;
669                mMaskCanvas = null;
670            }
671            mMaskMatrix = null;
672            mMaskColorFilter = null;
673            return;
674        }
675
676        // Ensure we have a correctly-sized buffer.
677        final Rect bounds = getBounds();
678        if (mMaskBuffer == null
679                || mMaskBuffer.getWidth() != bounds.width()
680                || mMaskBuffer.getHeight() != bounds.height()) {
681            if (mMaskBuffer != null) {
682                mMaskBuffer.recycle();
683            }
684
685            mMaskBuffer = Bitmap.createBitmap(
686                    bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
687            mMaskShader = new BitmapShader(mMaskBuffer,
688                    Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
689            mMaskCanvas = new Canvas(mMaskBuffer);
690        } else {
691            mMaskBuffer.eraseColor(Color.TRANSPARENT);
692        }
693
694        if (mMaskMatrix == null) {
695            mMaskMatrix = new Matrix();
696        } else {
697            mMaskMatrix.reset();
698        }
699
700        if (mMaskColorFilter == null) {
701            mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
702        }
703
704        // Draw the appropriate mask.
705        if (maskType == MASK_EXPLICIT) {
706            drawMask(mMaskCanvas);
707        } else if (maskType == MASK_CONTENT) {
708            drawContent(mMaskCanvas);
709        }
710    }
711
712    private int getMaskType() {
713        if (mRipple == null && mExitingRipplesCount <= 0
714                && (mBackground == null || !mBackground.shouldDraw())) {
715            // We might need a mask later.
716            return MASK_UNKNOWN;
717        }
718
719        if (mMask != null) {
720            if (mMask.getOpacity() == PixelFormat.OPAQUE) {
721                // Clipping handles opaque explicit masks.
722                return MASK_NONE;
723            } else {
724                return MASK_EXPLICIT;
725            }
726        }
727
728        // Check for non-opaque, non-mask content.
729        final ChildDrawable[] array = mLayerState.mChildren;
730        final int count = mLayerState.mNum;
731        for (int i = 0; i < count; i++) {
732            if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
733                return MASK_CONTENT;
734            }
735        }
736
737        // Clipping handles opaque content.
738        return MASK_NONE;
739    }
740
741    /**
742     * Removes a ripple from the exiting ripple list.
743     *
744     * @param ripple the ripple to remove
745     */
746    void removeRipple(Ripple ripple) {
747        // Ripple ripple ripple ripple. Ripple ripple.
748        final Ripple[] ripples = mExitingRipples;
749        final int count = mExitingRipplesCount;
750        final int index = getRippleIndex(ripple);
751        if (index >= 0) {
752            System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
753            ripples[count - 1] = null;
754            mExitingRipplesCount--;
755
756            invalidateSelf();
757        }
758    }
759
760    private int getRippleIndex(Ripple ripple) {
761        final Ripple[] ripples = mExitingRipples;
762        final int count = mExitingRipplesCount;
763        for (int i = 0; i < count; i++) {
764            if (ripples[i] == ripple) {
765                return i;
766            }
767        }
768        return -1;
769    }
770
771    private void drawContent(Canvas canvas) {
772        // Draw everything except the mask.
773        final ChildDrawable[] array = mLayerState.mChildren;
774        final int count = mLayerState.mNum;
775        for (int i = 0; i < count; i++) {
776            if (array[i].mId != R.id.mask) {
777                array[i].mDrawable.draw(canvas);
778            }
779        }
780    }
781
782    private void drawBackgroundAndRipples(Canvas canvas) {
783        final Ripple active = mRipple;
784        final RippleBackground background = mBackground;
785        final int count = mExitingRipplesCount;
786        if (active == null && count <= 0 && (background == null || !background.shouldDraw())) {
787            // Move along, nothing to draw here.
788            return;
789        }
790
791        final float x = mHotspotBounds.exactCenterX();
792        final float y = mHotspotBounds.exactCenterY();
793        canvas.translate(x, y);
794
795        updateMaskShaderIfNeeded();
796
797        // Position the shader to account for canvas translation.
798        if (mMaskShader != null) {
799            mMaskMatrix.setTranslate(-x, -y);
800            mMaskShader.setLocalMatrix(mMaskMatrix);
801        }
802
803        // Grab the color for the current state and cut the alpha channel in
804        // half so that the ripple and background together yield full alpha.
805        final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
806        final int halfAlpha = (Color.alpha(color) / 2) << 24;
807        final Paint p = getRipplePaint();
808
809        if (mMaskColorFilter != null) {
810            // The ripple timing depends on the paint's alpha value, so we need
811            // to push just the alpha channel into the paint and let the filter
812            // handle the full-alpha color.
813            final int fullAlphaColor = color | (0xFF << 24);
814            mMaskColorFilter.setColor(fullAlphaColor);
815
816            p.setColor(halfAlpha);
817            p.setColorFilter(mMaskColorFilter);
818            p.setShader(mMaskShader);
819        } else {
820            final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
821            p.setColor(halfAlphaColor);
822            p.setColorFilter(null);
823            p.setShader(null);
824        }
825
826        if (background != null && background.shouldDraw()) {
827            background.draw(canvas, p);
828        }
829
830        if (count > 0) {
831            final Ripple[] ripples = mExitingRipples;
832            for (int i = 0; i < count; i++) {
833                ripples[i].draw(canvas, p);
834            }
835        }
836
837        if (active != null) {
838            active.draw(canvas, p);
839        }
840
841        canvas.translate(-x, -y);
842    }
843
844    private void drawMask(Canvas canvas) {
845        mMask.draw(canvas);
846    }
847
848    private Paint getRipplePaint() {
849        if (mRipplePaint == null) {
850            mRipplePaint = new Paint();
851            mRipplePaint.setAntiAlias(true);
852            mRipplePaint.setStyle(Paint.Style.FILL);
853        }
854        return mRipplePaint;
855    }
856
857    @Override
858    public Rect getDirtyBounds() {
859        if (isProjected()) {
860            final Rect drawingBounds = mDrawingBounds;
861            final Rect dirtyBounds = mDirtyBounds;
862            dirtyBounds.set(drawingBounds);
863            drawingBounds.setEmpty();
864
865            final int cX = (int) mHotspotBounds.exactCenterX();
866            final int cY = (int) mHotspotBounds.exactCenterY();
867            final Rect rippleBounds = mTempRect;
868
869            final Ripple[] activeRipples = mExitingRipples;
870            final int N = mExitingRipplesCount;
871            for (int i = 0; i < N; i++) {
872                activeRipples[i].getBounds(rippleBounds);
873                rippleBounds.offset(cX, cY);
874                drawingBounds.union(rippleBounds);
875            }
876
877            final RippleBackground background = mBackground;
878            if (background != null) {
879                background.getBounds(rippleBounds);
880                rippleBounds.offset(cX, cY);
881                drawingBounds.union(rippleBounds);
882            }
883
884            dirtyBounds.union(drawingBounds);
885            dirtyBounds.union(super.getDirtyBounds());
886            return dirtyBounds;
887        } else {
888            return getBounds();
889        }
890    }
891
892    @Override
893    public ConstantState getConstantState() {
894        return mState;
895    }
896
897    @Override
898    public Drawable mutate() {
899        super.mutate();
900
901        // LayerDrawable creates a new state using createConstantState, so
902        // this should always be a safe cast.
903        mState = (RippleState) mLayerState;
904
905        // The locally cached drawable may have changed.
906        mMask = findDrawableByLayerId(R.id.mask);
907
908        return this;
909    }
910
911    @Override
912    RippleState createConstantState(LayerState state, Resources res) {
913        return new RippleState(state, this, res);
914    }
915
916    static class RippleState extends LayerState {
917        int[] mTouchThemeAttrs;
918        ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
919        int mMaxRadius = RADIUS_AUTO;
920
921        public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
922            super(orig, owner, res);
923
924            if (orig != null && orig instanceof RippleState) {
925                final RippleState origs = (RippleState) orig;
926                mTouchThemeAttrs = origs.mTouchThemeAttrs;
927                mColor = origs.mColor;
928                mMaxRadius = origs.mMaxRadius;
929            }
930        }
931
932        @Override
933        public boolean canApplyTheme() {
934            return mTouchThemeAttrs != null || super.canApplyTheme();
935        }
936
937        @Override
938        public Drawable newDrawable() {
939            return new RippleDrawable(this, null);
940        }
941
942        @Override
943        public Drawable newDrawable(Resources res) {
944            return new RippleDrawable(this, res);
945        }
946    }
947
948    /**
949     * Sets the maximum ripple radius in pixels. The default value of
950     * {@link #RADIUS_AUTO} defines the radius as the distance from the center
951     * of the drawable bounds (or hotspot bounds, if specified) to a corner.
952     *
953     * @param maxRadius the maximum ripple radius in pixels or
954     *            {@link #RADIUS_AUTO} to automatically determine the maximum
955     *            radius based on the bounds
956     * @see #getMaxRadius()
957     * @see #setHotspotBounds(int, int, int, int)
958     * @hide
959     */
960    public void setMaxRadius(int maxRadius) {
961        if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
962            throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
963        }
964
965        mState.mMaxRadius = maxRadius;
966    }
967
968    /**
969     * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
970     *         the radius is determined automatically
971     * @see #setMaxRadius(int)
972     * @hide
973     */
974    public int getMaxRadius() {
975        return mState.mMaxRadius;
976    }
977
978    private RippleDrawable(RippleState state, Resources res) {
979        mState = new RippleState(state, this, res);
980        mLayerState = mState;
981
982        if (mState.mNum > 0) {
983            ensurePadding();
984        }
985
986        if (res != null) {
987            mDensity = res.getDisplayMetrics().density;
988        }
989
990        initializeFromState();
991    }
992
993    private void initializeFromState() {
994        // Initialize from constant state.
995        mMask = findDrawableByLayerId(R.id.mask);
996    }
997}
998