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