RippleDrawable.java revision 45c4bbbbce6bbad50a033efcba7948a23f1f117a
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.graphics.drawable;
18
19import com.android.internal.R;
20
21import org.xmlpull.v1.XmlPullParser;
22import org.xmlpull.v1.XmlPullParserException;
23
24import android.annotation.NonNull;
25import android.annotation.Nullable;
26import android.content.res.ColorStateList;
27import android.content.res.Resources;
28import android.content.res.Resources.Theme;
29import android.content.res.TypedArray;
30import android.graphics.Bitmap;
31import android.graphics.BitmapShader;
32import android.graphics.Canvas;
33import android.graphics.Color;
34import android.graphics.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        updateLocalState();
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    /**
356     * Sets the ripple color.
357     *
358     * @param color Ripple color as a color state list.
359     */
360    public void setColor(ColorStateList color) {
361        mState.mColor = color;
362        invalidateSelf();
363    }
364
365    @Override
366    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
367            throws XmlPullParserException, IOException {
368        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
369        updateStateFromTypedArray(a);
370        a.recycle();
371
372        // Force padding default to STACK before inflating.
373        setPaddingMode(PADDING_MODE_STACK);
374
375        super.inflate(r, parser, attrs, theme);
376
377        setTargetDensity(r.getDisplayMetrics());
378
379        updateLocalState();
380    }
381
382    @Override
383    public boolean setDrawableByLayerId(int id, Drawable drawable) {
384        if (super.setDrawableByLayerId(id, drawable)) {
385            if (id == R.id.mask) {
386                mMask = drawable;
387            }
388
389            return true;
390        }
391
392        return false;
393    }
394
395    /**
396     * Specifies how layer padding should affect the bounds of subsequent
397     * layers. The default and recommended value for RippleDrawable is
398     * {@link #PADDING_MODE_STACK}.
399     *
400     * @param mode padding mode, one of:
401     *            <ul>
402     *            <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
403     *            padding of the previous layer
404     *            <li>{@link #PADDING_MODE_STACK} to stack each layer directly
405     *            atop the previous layer
406     *            </ul>
407     * @see #getPaddingMode()
408     */
409    @Override
410    public void setPaddingMode(int mode) {
411        super.setPaddingMode(mode);
412    }
413
414    /**
415     * Initializes the constant state from the values in the typed array.
416     */
417    private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
418        final RippleState state = mState;
419
420        // Account for any configuration changes.
421        state.mChangingConfigurations |= a.getChangingConfigurations();
422
423        // Extract the theme attributes, if any.
424        state.mTouchThemeAttrs = a.extractThemeAttrs();
425
426        final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
427        if (color != null) {
428            mState.mColor = color;
429        }
430
431        verifyRequiredAttributes(a);
432    }
433
434    private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
435        if (mState.mColor == null && (mState.mTouchThemeAttrs == null
436                || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
437            throw new XmlPullParserException(a.getPositionDescription() +
438                    ": <ripple> requires a valid color attribute");
439        }
440    }
441
442    /**
443     * Set the density at which this drawable will be rendered.
444     *
445     * @param metrics The display metrics for this drawable.
446     */
447    private void setTargetDensity(DisplayMetrics metrics) {
448        if (mDensity != metrics.density) {
449            mDensity = metrics.density;
450            invalidateSelf();
451        }
452    }
453
454    @Override
455    public void applyTheme(Theme t) {
456        super.applyTheme(t);
457
458        final RippleState state = mState;
459        if (state == null) {
460            return;
461        }
462
463        if (state.mTouchThemeAttrs != null) {
464            final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
465                    R.styleable.RippleDrawable);
466            try {
467                updateStateFromTypedArray(a);
468            } catch (XmlPullParserException e) {
469                throw new RuntimeException(e);
470            } finally {
471                a.recycle();
472            }
473        }
474
475        if (state.mColor != null && state.mColor.canApplyTheme()) {
476            state.mColor.applyTheme(t);
477        }
478
479        updateLocalState();
480    }
481
482    @Override
483    public boolean canApplyTheme() {
484        return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
485    }
486
487    @Override
488    public void setHotspot(float x, float y) {
489        if (mRipple == null || mBackground == null) {
490            mPendingX = x;
491            mPendingY = y;
492            mHasPending = true;
493        }
494
495        if (mRipple != null) {
496            mRipple.move(x, y);
497        }
498    }
499
500    /**
501     * Creates an active hotspot at the specified location.
502     */
503    private void tryBackgroundEnter(boolean focused) {
504        if (mBackground == null) {
505            mBackground = new RippleBackground(this, mHotspotBounds);
506        }
507
508        mBackground.setup(mState.mMaxRadius, mDensity);
509        mBackground.enter(focused);
510    }
511
512    private void tryBackgroundExit() {
513        if (mBackground != null) {
514            // Don't null out the background, we need it to draw!
515            mBackground.exit();
516        }
517    }
518
519    /**
520     * Attempts to start an enter animation for the active hotspot. Fails if
521     * there are too many animating ripples.
522     */
523    private void tryRippleEnter() {
524        if (mExitingRipplesCount >= MAX_RIPPLES) {
525            // This should never happen unless the user is tapping like a maniac
526            // or there is a bug that's preventing ripples from being removed.
527            return;
528        }
529
530        if (mRipple == null) {
531            final float x;
532            final float y;
533            if (mHasPending) {
534                mHasPending = false;
535                x = mPendingX;
536                y = mPendingY;
537            } else {
538                x = mHotspotBounds.exactCenterX();
539                y = mHotspotBounds.exactCenterY();
540            }
541            mRipple = new Ripple(this, mHotspotBounds, x, y);
542        }
543
544        mRipple.setup(mState.mMaxRadius, mDensity);
545        mRipple.enter();
546    }
547
548    /**
549     * Attempts to start an exit animation for the active hotspot. Fails if
550     * there is no active hotspot.
551     */
552    private void tryRippleExit() {
553        if (mRipple != null) {
554            if (mExitingRipples == null) {
555                mExitingRipples = new Ripple[MAX_RIPPLES];
556            }
557            mExitingRipples[mExitingRipplesCount++] = mRipple;
558            mRipple.exit();
559            mRipple = null;
560        }
561    }
562
563    /**
564     * Cancels and removes the active ripple, all exiting ripples, and the
565     * background. Nothing will be drawn after this method is called.
566     */
567    private void clearHotspots() {
568        if (mRipple != null) {
569            mRipple.cancel();
570            mRipple = null;
571            mRippleActive = false;
572        }
573
574        if (mBackground != null) {
575            mBackground.cancel();
576            mBackground = null;
577            mBackgroundActive = false;
578        }
579
580        cancelExitingRipples();
581        invalidateSelf();
582    }
583
584    @Override
585    public void setHotspotBounds(int left, int top, int right, int bottom) {
586        mOverrideBounds = true;
587        mHotspotBounds.set(left, top, right, bottom);
588
589        onHotspotBoundsChanged();
590    }
591
592    /** @hide */
593    @Override
594    public void getHotspotBounds(Rect outRect) {
595        outRect.set(mHotspotBounds);
596    }
597
598    /**
599     * Notifies all the animating ripples that the hotspot bounds have changed.
600     */
601    private void onHotspotBoundsChanged() {
602        final int count = mExitingRipplesCount;
603        final Ripple[] ripples = mExitingRipples;
604        for (int i = 0; i < count; i++) {
605            ripples[i].onHotspotBoundsChanged();
606        }
607
608        if (mRipple != null) {
609            mRipple.onHotspotBoundsChanged();
610        }
611
612        if (mBackground != null) {
613            mBackground.onHotspotBoundsChanged();
614        }
615    }
616
617    /**
618     * Populates <code>outline</code> with the first available layer outline,
619     * excluding the mask layer.
620     *
621     * @param outline Outline in which to place the first available layer outline
622     */
623    @Override
624    public void getOutline(@NonNull Outline outline) {
625        final LayerState state = mLayerState;
626        final ChildDrawable[] children = state.mChildren;
627        final int N = state.mNum;
628        for (int i = 0; i < N; i++) {
629            if (children[i].mId != R.id.mask) {
630                children[i].mDrawable.getOutline(outline);
631                if (!outline.isEmpty()) return;
632            }
633        }
634    }
635
636    /**
637     * Optimized for drawing ripples with a mask layer and optional content.
638     */
639    @Override
640    public void draw(@NonNull Canvas canvas) {
641        // Clip to the dirty bounds, which will be the drawable bounds if we
642        // have a mask or content and the ripple bounds if we're projecting.
643        final Rect bounds = getDirtyBounds();
644        final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
645        canvas.clipRect(bounds);
646
647        drawContent(canvas);
648        drawBackgroundAndRipples(canvas);
649
650        canvas.restoreToCount(saveCount);
651    }
652
653    @Override
654    public void invalidateSelf() {
655        super.invalidateSelf();
656
657        // Force the mask to update on the next draw().
658        mHasValidMask = false;
659    }
660
661    /**
662     * @return whether we need to use a mask
663     */
664    private void updateMaskShaderIfNeeded() {
665        if (mHasValidMask) {
666            return;
667        }
668
669        final int maskType = getMaskType();
670        if (maskType == MASK_UNKNOWN) {
671            return;
672        }
673
674        mHasValidMask = true;
675
676        if (maskType == MASK_NONE) {
677            if (mMaskBuffer != null) {
678                mMaskBuffer.recycle();
679                mMaskBuffer = null;
680                mMaskShader = null;
681                mMaskCanvas = null;
682            }
683            mMaskMatrix = null;
684            mMaskColorFilter = null;
685            return;
686        }
687
688        // Ensure we have a correctly-sized buffer.
689        final Rect bounds = getBounds();
690        if (mMaskBuffer == null
691                || mMaskBuffer.getWidth() != bounds.width()
692                || mMaskBuffer.getHeight() != bounds.height()) {
693            if (mMaskBuffer != null) {
694                mMaskBuffer.recycle();
695            }
696
697            mMaskBuffer = Bitmap.createBitmap(
698                    bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
699            mMaskShader = new BitmapShader(mMaskBuffer,
700                    Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
701            mMaskCanvas = new Canvas(mMaskBuffer);
702        } else {
703            mMaskBuffer.eraseColor(Color.TRANSPARENT);
704        }
705
706        if (mMaskMatrix == null) {
707            mMaskMatrix = new Matrix();
708        } else {
709            mMaskMatrix.reset();
710        }
711
712        if (mMaskColorFilter == null) {
713            mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
714        }
715
716        // Draw the appropriate mask.
717        if (maskType == MASK_EXPLICIT) {
718            drawMask(mMaskCanvas);
719        } else if (maskType == MASK_CONTENT) {
720            drawContent(mMaskCanvas);
721        }
722    }
723
724    private int getMaskType() {
725        if (mRipple == null && mExitingRipplesCount <= 0
726                && (mBackground == null || !mBackground.shouldDraw())) {
727            // We might need a mask later.
728            return MASK_UNKNOWN;
729        }
730
731        if (mMask != null) {
732            if (mMask.getOpacity() == PixelFormat.OPAQUE) {
733                // Clipping handles opaque explicit masks.
734                return MASK_NONE;
735            } else {
736                return MASK_EXPLICIT;
737            }
738        }
739
740        // Check for non-opaque, non-mask content.
741        final ChildDrawable[] array = mLayerState.mChildren;
742        final int count = mLayerState.mNum;
743        for (int i = 0; i < count; i++) {
744            if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
745                return MASK_CONTENT;
746            }
747        }
748
749        // Clipping handles opaque content.
750        return MASK_NONE;
751    }
752
753    /**
754     * Removes a ripple from the exiting ripple list.
755     *
756     * @param ripple the ripple to remove
757     */
758    void removeRipple(Ripple ripple) {
759        // Ripple ripple ripple ripple. Ripple ripple.
760        final Ripple[] ripples = mExitingRipples;
761        final int count = mExitingRipplesCount;
762        final int index = getRippleIndex(ripple);
763        if (index >= 0) {
764            System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
765            ripples[count - 1] = null;
766            mExitingRipplesCount--;
767
768            invalidateSelf();
769        }
770    }
771
772    private int getRippleIndex(Ripple ripple) {
773        final Ripple[] ripples = mExitingRipples;
774        final int count = mExitingRipplesCount;
775        for (int i = 0; i < count; i++) {
776            if (ripples[i] == ripple) {
777                return i;
778            }
779        }
780        return -1;
781    }
782
783    private void drawContent(Canvas canvas) {
784        // Draw everything except the mask.
785        final ChildDrawable[] array = mLayerState.mChildren;
786        final int count = mLayerState.mNum;
787        for (int i = 0; i < count; i++) {
788            if (array[i].mId != R.id.mask) {
789                array[i].mDrawable.draw(canvas);
790            }
791        }
792    }
793
794    private void drawBackgroundAndRipples(Canvas canvas) {
795        final Ripple active = mRipple;
796        final RippleBackground background = mBackground;
797        final int count = mExitingRipplesCount;
798        if (active == null && count <= 0 && (background == null || !background.shouldDraw())) {
799            // Move along, nothing to draw here.
800            return;
801        }
802
803        final float x = mHotspotBounds.exactCenterX();
804        final float y = mHotspotBounds.exactCenterY();
805        canvas.translate(x, y);
806
807        updateMaskShaderIfNeeded();
808
809        // Position the shader to account for canvas translation.
810        if (mMaskShader != null) {
811            mMaskMatrix.setTranslate(-x, -y);
812            mMaskShader.setLocalMatrix(mMaskMatrix);
813        }
814
815        // Grab the color for the current state and cut the alpha channel in
816        // half so that the ripple and background together yield full alpha.
817        final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
818        final int halfAlpha = (Color.alpha(color) / 2) << 24;
819        final Paint p = getRipplePaint();
820
821        if (mMaskColorFilter != null) {
822            // The ripple timing depends on the paint's alpha value, so we need
823            // to push just the alpha channel into the paint and let the filter
824            // handle the full-alpha color.
825            final int fullAlphaColor = color | (0xFF << 24);
826            mMaskColorFilter.setColor(fullAlphaColor);
827
828            p.setColor(halfAlpha);
829            p.setColorFilter(mMaskColorFilter);
830            p.setShader(mMaskShader);
831        } else {
832            final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
833            p.setColor(halfAlphaColor);
834            p.setColorFilter(null);
835            p.setShader(null);
836        }
837
838        if (background != null && background.shouldDraw()) {
839            background.draw(canvas, p);
840        }
841
842        if (count > 0) {
843            final Ripple[] ripples = mExitingRipples;
844            for (int i = 0; i < count; i++) {
845                ripples[i].draw(canvas, p);
846            }
847        }
848
849        if (active != null) {
850            active.draw(canvas, p);
851        }
852
853        canvas.translate(-x, -y);
854    }
855
856    private void drawMask(Canvas canvas) {
857        mMask.draw(canvas);
858    }
859
860    private Paint getRipplePaint() {
861        if (mRipplePaint == null) {
862            mRipplePaint = new Paint();
863            mRipplePaint.setAntiAlias(true);
864            mRipplePaint.setStyle(Paint.Style.FILL);
865        }
866        return mRipplePaint;
867    }
868
869    @Override
870    public Rect getDirtyBounds() {
871        if (isProjected()) {
872            final Rect drawingBounds = mDrawingBounds;
873            final Rect dirtyBounds = mDirtyBounds;
874            dirtyBounds.set(drawingBounds);
875            drawingBounds.setEmpty();
876
877            final int cX = (int) mHotspotBounds.exactCenterX();
878            final int cY = (int) mHotspotBounds.exactCenterY();
879            final Rect rippleBounds = mTempRect;
880
881            final Ripple[] activeRipples = mExitingRipples;
882            final int N = mExitingRipplesCount;
883            for (int i = 0; i < N; i++) {
884                activeRipples[i].getBounds(rippleBounds);
885                rippleBounds.offset(cX, cY);
886                drawingBounds.union(rippleBounds);
887            }
888
889            final RippleBackground background = mBackground;
890            if (background != null) {
891                background.getBounds(rippleBounds);
892                rippleBounds.offset(cX, cY);
893                drawingBounds.union(rippleBounds);
894            }
895
896            dirtyBounds.union(drawingBounds);
897            dirtyBounds.union(super.getDirtyBounds());
898            return dirtyBounds;
899        } else {
900            return getBounds();
901        }
902    }
903
904    @Override
905    public ConstantState getConstantState() {
906        return mState;
907    }
908
909    @Override
910    public Drawable mutate() {
911        super.mutate();
912
913        // LayerDrawable creates a new state using createConstantState, so
914        // this should always be a safe cast.
915        mState = (RippleState) mLayerState;
916
917        // The locally cached drawable may have changed.
918        mMask = findDrawableByLayerId(R.id.mask);
919
920        return this;
921    }
922
923    @Override
924    RippleState createConstantState(LayerState state, Resources res) {
925        return new RippleState(state, this, res);
926    }
927
928    static class RippleState extends LayerState {
929        int[] mTouchThemeAttrs;
930        ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
931        int mMaxRadius = RADIUS_AUTO;
932
933        public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
934            super(orig, owner, res);
935
936            if (orig != null && orig instanceof RippleState) {
937                final RippleState origs = (RippleState) orig;
938                mTouchThemeAttrs = origs.mTouchThemeAttrs;
939                mColor = origs.mColor;
940                mMaxRadius = origs.mMaxRadius;
941            }
942        }
943
944        @Override
945        public boolean canApplyTheme() {
946            return mTouchThemeAttrs != null
947                    || (mColor != null && mColor.canApplyTheme())
948                    || super.canApplyTheme();
949        }
950
951        @Override
952        public Drawable newDrawable() {
953            return new RippleDrawable(this, null);
954        }
955
956        @Override
957        public Drawable newDrawable(Resources res) {
958            return new RippleDrawable(this, res);
959        }
960    }
961
962    /**
963     * Sets the maximum ripple radius in pixels. The default value of
964     * {@link #RADIUS_AUTO} defines the radius as the distance from the center
965     * of the drawable bounds (or hotspot bounds, if specified) to a corner.
966     *
967     * @param maxRadius the maximum ripple radius in pixels or
968     *            {@link #RADIUS_AUTO} to automatically determine the maximum
969     *            radius based on the bounds
970     * @see #getMaxRadius()
971     * @see #setHotspotBounds(int, int, int, int)
972     * @hide
973     */
974    public void setMaxRadius(int maxRadius) {
975        if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
976            throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
977        }
978
979        mState.mMaxRadius = maxRadius;
980    }
981
982    /**
983     * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
984     *         the radius is determined automatically
985     * @see #setMaxRadius(int)
986     * @hide
987     */
988    public int getMaxRadius() {
989        return mState.mMaxRadius;
990    }
991
992    private RippleDrawable(RippleState state, Resources res) {
993        mState = new RippleState(state, this, res);
994        mLayerState = mState;
995
996        if (mState.mNum > 0) {
997            ensurePadding();
998        }
999
1000        if (res != null) {
1001            mDensity = res.getDisplayMetrics().density;
1002        }
1003
1004        updateLocalState();
1005    }
1006
1007    private void updateLocalState() {
1008        // Initialize from constant state.
1009        mMask = findDrawableByLayerId(R.id.mask);
1010    }
1011}
1012