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