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