RippleDrawable.java revision fdbb98e56d4668c7bfa8de59c3c438c0cb69a535
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.graphics.drawable;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.res.ColorStateList;
22import android.content.res.Resources;
23import android.content.res.Resources.Theme;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.ColorFilter;
28import android.graphics.Outline;
29import android.graphics.Paint;
30import android.graphics.PixelFormat;
31import android.graphics.PorterDuff.Mode;
32import android.graphics.PorterDuffXfermode;
33import android.graphics.Rect;
34import android.util.AttributeSet;
35import android.util.DisplayMetrics;
36
37import com.android.internal.R;
38
39import org.xmlpull.v1.XmlPullParser;
40import org.xmlpull.v1.XmlPullParserException;
41
42import java.io.IOException;
43import java.util.Arrays;
44
45/**
46 * Drawable that shows a ripple effect in response to state changes. The
47 * anchoring position of the ripple for a given state may be specified by
48 * calling {@link #setHotspot(float, float)} with the corresponding state
49 * attribute identifier.
50 * <p>
51 * A touch feedback drawable may contain multiple child layers, including a
52 * special mask layer that is not drawn to the screen. A single layer may be set
53 * as the mask by specifying its android:id value as {@link android.R.id#mask}.
54 * <pre>
55 * <code>&lt!-- A red ripple masked against an opaque rectangle. --/>
56 * &ltripple android:color="#ffff0000">
57 *   &ltitem android:id="@android:id/mask"
58 *         android:drawable="@android:color/white" />
59 * &ltripple /></code>
60 * </pre>
61 * <p>
62 * If a mask layer is set, the ripple effect will be masked against that layer
63 * before it is drawn over the composite of the remaining child layers.
64 * <p>
65 * If no mask layer is set, the ripple effect is masked against the composite
66 * of the child layers.
67 * <pre>
68 * <code>&lt!-- A blue ripple drawn atop a black rectangle. --/>
69 * &ltripple android:color="#ff00ff00">
70 *   &ltitem android:drawable="@android:color/black" />
71 * &ltripple />
72 *
73 * &lt!-- A red ripple drawn atop a drawable resource. --/>
74 * &ltripple android:color="#ff00ff00">
75 *   &ltitem android:drawable="@drawable/my_drawable" />
76 * &ltripple /></code>
77 * </pre>
78 * <p>
79 * If no child layers or mask is specified and the ripple is set as a View
80 * background, the ripple will be drawn atop the first available parent
81 * background within the View's hierarchy. In this case, the drawing region
82 * may extend outside of the Drawable bounds.
83 * <pre>
84 * <code>&lt!-- An unbounded green ripple. --/>
85 * &ltripple android:color="#ff0000ff" /></code>
86 * </pre>
87 *
88 * @attr ref android.R.styleable#RippleDrawable_color
89 */
90public class RippleDrawable extends LayerDrawable {
91    private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN);
92    private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP);
93    private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(Mode.SRC_OVER);
94
95    /**
96     * Constant for automatically determining the maximum ripple radius.
97     *
98     * @see #setMaxRadius(int)
99     * @hide
100     */
101    public static final int RADIUS_AUTO = -1;
102
103    /** The maximum number of ripples supported. */
104    private static final int MAX_RIPPLES = 10;
105
106    private final Rect mTempRect = new Rect();
107
108    /** Current ripple effect bounds, used to constrain ripple effects. */
109    private final Rect mHotspotBounds = new Rect();
110
111    /** Current drawing bounds, used to compute dirty region. */
112    private final Rect mDrawingBounds = new Rect();
113
114    /** Current dirty bounds, union of current and previous drawing bounds. */
115    private final Rect mDirtyBounds = new Rect();
116
117    private final RippleState mState;
118
119    /** The masking layer, e.g. the layer with id R.id.mask. */
120    private Drawable mMask;
121
122    /** The current background. May be actively animating or pending entry. */
123    private RippleBackground mBackground;
124
125    /** Whether we expect to draw a background when visible. */
126    private boolean mBackgroundActive;
127
128    /** The current ripple. May be actively animating or pending entry. */
129    private Ripple mRipple;
130
131    /** Whether we expect to draw a ripple when visible. */
132    private boolean mRippleActive;
133
134    // Hotspot coordinates that are awaiting activation.
135    private float mPendingX;
136    private float mPendingY;
137    private boolean mHasPending;
138
139    /**
140     * Lazily-created array of actively animating ripples. Inactive ripples are
141     * pruned during draw(). The locations of these will not change.
142     */
143    private Ripple[] mExitingRipples;
144    private int mExitingRipplesCount = 0;
145
146    /** Paint used to control appearance of ripples. */
147    private Paint mRipplePaint;
148
149    /** Paint used to control reveal layer masking. */
150    private Paint mMaskingPaint;
151
152    /** Target density of the display into which ripples are drawn. */
153    private float mDensity = 1.0f;
154
155    /** Whether bounds are being overridden. */
156    private boolean mOverrideBounds;
157
158    /**
159     * Constructor used for drawable inflation.
160     */
161    RippleDrawable() {
162        this(new RippleState(null, null, null), null, null);
163    }
164
165    /**
166     * Creates a new ripple drawable with the specified ripple color and
167     * optional content and mask drawables.
168     *
169     * @param color The ripple color
170     * @param content The content drawable, may be {@code null}
171     * @param mask The mask drawable, may be {@code null}
172     */
173    public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content,
174            @Nullable Drawable mask) {
175        this(new RippleState(null, null, null), null, null);
176
177        if (color == null) {
178            throw new IllegalArgumentException("RippleDrawable requires a non-null color");
179        }
180
181        if (content != null) {
182            addLayer(content, null, 0, 0, 0, 0, 0);
183        }
184
185        if (mask != null) {
186            addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0);
187        }
188
189        setColor(color);
190        ensurePadding();
191        initializeFromState();
192    }
193
194    @Override
195    public void jumpToCurrentState() {
196        super.jumpToCurrentState();
197
198        if (mRipple != null) {
199            mRipple.jump();
200        }
201
202        if (mBackground != null) {
203            mBackground.jump();
204        }
205
206        cancelExitingRipples();
207        invalidateSelf();
208    }
209
210    private void cancelExitingRipples() {
211        final int count = mExitingRipplesCount;
212        final Ripple[] ripples = mExitingRipples;
213        for (int i = 0; i < count; i++) {
214            ripples[i].cancel();
215        }
216
217        if (ripples != null) {
218            Arrays.fill(ripples, 0, count, null);
219        }
220        mExitingRipplesCount = 0;
221    }
222
223    @Override
224    public void setAlpha(int alpha) {
225        super.setAlpha(alpha);
226
227        // TODO: Should we support this?
228    }
229
230    @Override
231    public void setColorFilter(ColorFilter cf) {
232        super.setColorFilter(cf);
233
234        // TODO: Should we support this?
235    }
236
237    @Override
238    public int getOpacity() {
239        // Worst-case scenario.
240        return PixelFormat.TRANSLUCENT;
241    }
242
243    @Override
244    protected boolean onStateChange(int[] stateSet) {
245        super.onStateChange(stateSet);
246
247        boolean enabled = false;
248        boolean pressed = false;
249        boolean focused = false;
250
251        final int N = stateSet.length;
252        for (int i = 0; i < N; i++) {
253            if (stateSet[i] == R.attr.state_enabled) {
254                enabled = true;
255            }
256            if (stateSet[i] == R.attr.state_focused) {
257                focused = true;
258            }
259            if (stateSet[i] == R.attr.state_pressed) {
260                pressed = true;
261            }
262        }
263
264        setRippleActive(enabled && pressed);
265        setBackgroundActive(focused || (enabled && pressed));
266
267        // Update the paint color. Only applicable when animated in software.
268        if (mRipplePaint != null && mState.mColor != null) {
269            final ColorStateList stateList = mState.mColor;
270            final int newColor = stateList.getColorForState(stateSet, 0);
271            final int oldColor = mRipplePaint.getColor();
272            if (oldColor != newColor) {
273                mRipplePaint.setColor(newColor);
274                invalidateSelf();
275                return true;
276            }
277        }
278
279        return false;
280    }
281
282    private void setRippleActive(boolean active) {
283        if (mRippleActive != active) {
284            mRippleActive = active;
285            if (active) {
286                tryRippleEnter();
287            } else {
288                tryRippleExit();
289            }
290        }
291    }
292
293    private void setBackgroundActive(boolean active) {
294        if (mBackgroundActive != active) {
295            mBackgroundActive = active;
296            if (active) {
297                tryBackgroundEnter();
298            } else {
299                tryBackgroundExit();
300            }
301        }
302    }
303
304    @Override
305    protected void onBoundsChange(Rect bounds) {
306        super.onBoundsChange(bounds);
307
308        if (!mOverrideBounds) {
309            mHotspotBounds.set(bounds);
310            onHotspotBoundsChanged();
311        }
312
313        invalidateSelf();
314    }
315
316    @Override
317    public boolean setVisible(boolean visible, boolean restart) {
318        final boolean changed = super.setVisible(visible, restart);
319
320        if (!visible) {
321            clearHotspots();
322        } else if (changed) {
323            // If we just became visible, ensure the background and ripple
324            // visibilities are consistent with their internal states.
325            if (mRippleActive) {
326                tryRippleEnter();
327            }
328
329            if (mBackgroundActive) {
330                tryBackgroundEnter();
331            }
332        }
333
334        return changed;
335    }
336
337    /**
338     * @hide
339     */
340    @Override
341    public boolean isProjected() {
342        return getNumberOfLayers() == 0;
343    }
344
345    @Override
346    public boolean isStateful() {
347        return true;
348    }
349
350    public void setColor(ColorStateList color) {
351        mState.mColor = color;
352        invalidateSelf();
353    }
354
355    @Override
356    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
357            throws XmlPullParserException, IOException {
358        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
359        updateStateFromTypedArray(a);
360        a.recycle();
361
362        // Force padding default to STACK before inflating.
363        setPaddingMode(PADDING_MODE_STACK);
364
365        super.inflate(r, parser, attrs, theme);
366
367        setTargetDensity(r.getDisplayMetrics());
368        initializeFromState();
369    }
370
371    @Override
372    public boolean setDrawableByLayerId(int id, Drawable drawable) {
373        if (super.setDrawableByLayerId(id, drawable)) {
374            if (id == R.id.mask) {
375                mMask = drawable;
376            }
377
378            return true;
379        }
380
381        return false;
382    }
383
384    /**
385     * Specifies how layer padding should affect the bounds of subsequent
386     * layers. The default and recommended value for RippleDrawable is
387     * {@link #PADDING_MODE_STACK}.
388     *
389     * @param mode padding mode, one of:
390     *            <ul>
391     *            <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
392     *            padding of the previous layer
393     *            <li>{@link #PADDING_MODE_STACK} to stack each layer directly
394     *            atop the previous layer
395     *            </ul>
396     * @see #getPaddingMode()
397     */
398    @Override
399    public void setPaddingMode(int mode) {
400        super.setPaddingMode(mode);
401    }
402
403    /**
404     * Initializes the constant state from the values in the typed array.
405     */
406    private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
407        final RippleState state = mState;
408
409        // Account for any configuration changes.
410        state.mChangingConfigurations |= a.getChangingConfigurations();
411
412        // Extract the theme attributes, if any.
413        state.mTouchThemeAttrs = a.extractThemeAttrs();
414
415        final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
416        if (color != null) {
417            mState.mColor = color;
418        }
419
420        verifyRequiredAttributes(a);
421    }
422
423    private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
424        if (mState.mColor == null && (mState.mTouchThemeAttrs == null
425                || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
426            throw new XmlPullParserException(a.getPositionDescription() +
427                    ": <ripple> requires a valid color attribute");
428        }
429    }
430
431    /**
432     * Set the density at which this drawable will be rendered.
433     *
434     * @param metrics The display metrics for this drawable.
435     */
436    private void setTargetDensity(DisplayMetrics metrics) {
437        if (mDensity != metrics.density) {
438            mDensity = metrics.density;
439            invalidateSelf();
440        }
441    }
442
443    @Override
444    public void applyTheme(Theme t) {
445        super.applyTheme(t);
446
447        final RippleState state = mState;
448        if (state == null || state.mTouchThemeAttrs == null) {
449            return;
450        }
451
452        final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
453                R.styleable.RippleDrawable);
454        try {
455            updateStateFromTypedArray(a);
456        } catch (XmlPullParserException e) {
457            throw new RuntimeException(e);
458        } finally {
459            a.recycle();
460        }
461
462        initializeFromState();
463    }
464
465    @Override
466    public boolean canApplyTheme() {
467        return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null;
468    }
469
470    @Override
471    public void setHotspot(float x, float y) {
472        if (mRipple == null || mBackground == null) {
473            mPendingX = x;
474            mPendingY = y;
475            mHasPending = true;
476        }
477
478        if (mRipple != null) {
479            mRipple.move(x, y);
480        }
481
482        if (mBackground != null) {
483            mBackground.move(x, y);
484        }
485    }
486
487    /**
488     * Creates an active hotspot at the specified location.
489     */
490    private void tryBackgroundEnter() {
491        if (mBackground == null) {
492            final float x;
493            final float y;
494            if (mHasPending) {
495                mHasPending = false;
496                x = mPendingX;
497                y = mPendingY;
498            } else {
499                x = mHotspotBounds.exactCenterX();
500                y = mHotspotBounds.exactCenterY();
501            }
502            mBackground = new RippleBackground(this, mHotspotBounds, x, y);
503        }
504
505        final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
506        mBackground.setup(mState.mMaxRadius, color, mDensity);
507        mBackground.enter();
508    }
509
510    private void tryBackgroundExit() {
511        if (mBackground != null) {
512            // Don't null out the background, we need it to draw!
513            mBackground.exit();
514        }
515    }
516
517    /**
518     * Attempts to start an enter animation for the active hotspot. Fails if
519     * there are too many animating ripples.
520     */
521    private void tryRippleEnter() {
522        if (mExitingRipplesCount >= MAX_RIPPLES) {
523            // This should never happen unless the user is tapping like a maniac
524            // or there is a bug that's preventing ripples from being removed.
525            return;
526        }
527
528        if (mRipple == null) {
529            final float x;
530            final float y;
531            if (mHasPending) {
532                mHasPending = false;
533                x = mPendingX;
534                y = mPendingY;
535            } else {
536                x = mHotspotBounds.exactCenterX();
537                y = mHotspotBounds.exactCenterY();
538            }
539            mRipple = new Ripple(this, mHotspotBounds, x, y);
540        }
541
542        final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
543        mRipple.setup(mState.mMaxRadius, color, mDensity);
544        mRipple.enter();
545    }
546
547    /**
548     * Attempts to start an exit animation for the active hotspot. Fails if
549     * there is no active hotspot.
550     */
551    private void tryRippleExit() {
552        if (mRipple != null) {
553            if (mExitingRipples == null) {
554                mExitingRipples = new Ripple[MAX_RIPPLES];
555            }
556            mExitingRipples[mExitingRipplesCount++] = mRipple;
557            mRipple.exit();
558            mRipple = null;
559        }
560    }
561
562    /**
563     * Cancels and removes the active ripple, all exiting ripples, and the
564     * background. Nothing will be drawn after this method is called.
565     */
566    private void clearHotspots() {
567        if (mRipple != null) {
568            mRipple.cancel();
569            mRipple = null;
570        }
571
572        if (mBackground != null) {
573            mBackground.cancel();
574            mBackground = null;
575        }
576
577        cancelExitingRipples();
578        invalidateSelf();
579    }
580
581    @Override
582    public void setHotspotBounds(int left, int top, int right, int bottom) {
583        mOverrideBounds = true;
584        mHotspotBounds.set(left, top, right, bottom);
585
586        onHotspotBoundsChanged();
587    }
588
589    /** @hide */
590    @Override
591    public void getHotspotBounds(Rect outRect) {
592        outRect.set(mHotspotBounds);
593    }
594
595    /**
596     * Notifies all the animating ripples that the hotspot bounds have changed.
597     */
598    private void onHotspotBoundsChanged() {
599        final int count = mExitingRipplesCount;
600        final Ripple[] ripples = mExitingRipples;
601        for (int i = 0; i < count; i++) {
602            ripples[i].onHotspotBoundsChanged();
603        }
604
605        if (mBackground != null) {
606            mBackground.onHotspotBoundsChanged();
607        }
608    }
609
610    /**
611     * Populates <code>outline</code> with the first available layer outline,
612     * excluding the mask layer.
613     *
614     * @param outline Outline in which to place the first available layer outline
615     */
616    @Override
617    public void getOutline(@NonNull Outline outline) {
618        final LayerState state = mLayerState;
619        final ChildDrawable[] children = state.mChildren;
620        final int N = state.mNum;
621        for (int i = 0; i < N; i++) {
622            if (children[i].mId != R.id.mask) {
623                children[i].mDrawable.getOutline(outline);
624                if (!outline.isEmpty()) return;
625            }
626        }
627    }
628
629    @Override
630    public void draw(@NonNull Canvas canvas) {
631        final boolean isProjected = isProjected();
632        final boolean hasMask = mMask != null;
633        final boolean drawNonMaskContent = mLayerState.mNum > (hasMask ? 1 : 0);
634        final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE;
635        final Rect bounds = isProjected ? getDirtyBounds() : getBounds();
636
637        // If we have content, draw it into a layer first.
638        final int contentLayer = drawNonMaskContent ?
639                drawContentLayer(canvas, bounds, SRC_OVER) : -1;
640
641        // Next, try to draw the ripples (into a layer if necessary). If we need
642        // to mask against the underlying content, set the xfermode to SRC_ATOP.
643        final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP;
644
645        // If we have a background and a non-opaque mask, draw the masking layer.
646        final int backgroundLayer = drawBackgroundLayer(canvas, bounds, xfermode);
647        if (backgroundLayer >= 0) {
648            if (drawMask) {
649                drawMaskingLayer(canvas, bounds, DST_IN);
650            }
651            canvas.restoreToCount(backgroundLayer);
652        }
653
654        // If we have ripples and a non-opaque mask, draw the masking layer.
655        final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode);
656        if (rippleLayer >= 0) {
657            if (drawMask) {
658                drawMaskingLayer(canvas, bounds, DST_IN);
659            }
660            canvas.restoreToCount(rippleLayer);
661        }
662
663        // Composite the layers if needed.
664        if (contentLayer >= 0) {
665            canvas.restoreToCount(contentLayer);
666        }
667    }
668
669    /**
670     * Removes a ripple from the exiting ripple list.
671     *
672     * @param ripple the ripple to remove
673     */
674    void removeRipple(Ripple ripple) {
675        // Ripple ripple ripple ripple. Ripple ripple.
676        final Ripple[] ripples = mExitingRipples;
677        final int count = mExitingRipplesCount;
678        final int index = getRippleIndex(ripple);
679        if (index >= 0) {
680            System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
681            ripples[count - 1] = null;
682            mExitingRipplesCount--;
683
684            invalidateSelf();
685        }
686    }
687
688    void removeBackground(RippleBackground background) {
689        if (mBackground == background) {
690            mBackground = null;
691            invalidateSelf();
692        }
693    }
694
695    private int getRippleIndex(Ripple ripple) {
696        final Ripple[] ripples = mExitingRipples;
697        final int count = mExitingRipplesCount;
698        for (int i = 0; i < count; i++) {
699            if (ripples[i] == ripple) {
700                return i;
701            }
702        }
703        return -1;
704    }
705
706    private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
707        final ChildDrawable[] array = mLayerState.mChildren;
708        final int count = mLayerState.mNum;
709
710        // We don't need a layer if we don't expect to draw any ripples, we have
711        // an explicit mask, or if the non-mask content is all opaque.
712        boolean needsLayer = false;
713        if ((mExitingRipplesCount > 0 || mBackground != null) && mMask == null) {
714            for (int i = 0; i < count; i++) {
715                if (array[i].mId != R.id.mask
716                        && array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
717                    needsLayer = true;
718                    break;
719                }
720            }
721        }
722
723        final Paint maskingPaint = getMaskingPaint(mode);
724        final int restoreToCount = needsLayer ? canvas.saveLayer(bounds.left, bounds.top,
725                bounds.right, bounds.bottom, maskingPaint) : -1;
726
727        // Draw everything except the mask.
728        for (int i = 0; i < count; i++) {
729            if (array[i].mId != R.id.mask) {
730                array[i].mDrawable.draw(canvas);
731            }
732        }
733
734        return restoreToCount;
735    }
736
737    private int drawBackgroundLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
738        // Separate the ripple color and alpha channel. The alpha will be
739        // applied when we merge the ripples down to the canvas.
740        final int rippleARGB;
741        if (mState.mColor != null) {
742            rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
743        } else {
744            rippleARGB = Color.TRANSPARENT;
745        }
746
747        if (mRipplePaint == null) {
748            mRipplePaint = new Paint();
749            mRipplePaint.setAntiAlias(true);
750        }
751
752        final int rippleAlpha = Color.alpha(rippleARGB);
753        final Paint ripplePaint = mRipplePaint;
754        ripplePaint.setColor(rippleARGB);
755        ripplePaint.setAlpha(0xFF);
756
757        boolean drewRipples = false;
758        int restoreToCount = -1;
759        int restoreTranslate = -1;
760
761        // Draw background.
762        final RippleBackground background = mBackground;
763        if (background != null) {
764            // If we're masking the ripple layer, make sure we have a layer
765            // first. This will merge SRC_OVER (directly) onto the canvas.
766            final Paint maskingPaint = getMaskingPaint(mode);
767            maskingPaint.setAlpha(rippleAlpha);
768            restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
769                    bounds.right, bounds.bottom, maskingPaint);
770
771            restoreTranslate = canvas.save();
772            // Translate the canvas to the current hotspot bounds.
773            canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
774
775            drewRipples = background.draw(canvas, ripplePaint);
776        }
777
778        // Always restore the translation.
779        if (restoreTranslate >= 0) {
780            canvas.restoreToCount(restoreTranslate);
781        }
782
783        // If we created a layer with no content, merge it immediately.
784        if (restoreToCount >= 0 && !drewRipples) {
785            canvas.restoreToCount(restoreToCount);
786            restoreToCount = -1;
787        }
788
789        return restoreToCount;
790    }
791
792    private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
793        // Separate the ripple color and alpha channel. The alpha will be
794        // applied when we merge the ripples down to the canvas.
795        final int rippleARGB;
796        if (mState.mColor != null) {
797            rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
798        } else {
799            rippleARGB = Color.TRANSPARENT;
800        }
801
802        if (mRipplePaint == null) {
803            mRipplePaint = new Paint();
804            mRipplePaint.setAntiAlias(true);
805        }
806
807        final int rippleAlpha = Color.alpha(rippleARGB);
808        final Paint ripplePaint = mRipplePaint;
809        ripplePaint.setColor(rippleARGB);
810        ripplePaint.setAlpha(0xFF);
811
812        boolean drewRipples = false;
813        int restoreToCount = -1;
814        int restoreTranslate = -1;
815
816        // Draw ripples and update the animating ripples array.
817        final int count = mExitingRipplesCount;
818        final Ripple[] ripples = mExitingRipples;
819        for (int i = 0; i <= count; i++) {
820            final Ripple ripple;
821            if (i < count) {
822                ripple = ripples[i];
823            } else if (mRipple != null) {
824                ripple = mRipple;
825            } else {
826                continue;
827            }
828
829            // If we're masking the ripple layer, make sure we have a layer
830            // first. This will merge SRC_OVER (directly) onto the canvas.
831            if (restoreToCount < 0) {
832                final Paint maskingPaint = getMaskingPaint(mode);
833                maskingPaint.setAlpha(rippleAlpha);
834                restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
835                        bounds.right, bounds.bottom, maskingPaint);
836
837                restoreTranslate = canvas.save();
838                // Translate the canvas to the current hotspot bounds.
839                canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
840            }
841
842            drewRipples |= ripple.draw(canvas, ripplePaint);
843        }
844
845        // Always restore the translation.
846        if (restoreTranslate >= 0) {
847            canvas.restoreToCount(restoreTranslate);
848        }
849
850        // If we created a layer with no content, merge it immediately.
851        if (restoreToCount >= 0 && !drewRipples) {
852            canvas.restoreToCount(restoreToCount);
853            restoreToCount = -1;
854        }
855
856        return restoreToCount;
857    }
858
859    private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
860        final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
861                bounds.right, bounds.bottom, getMaskingPaint(mode));
862
863        // Ensure that DST_IN blends using the entire layer.
864        canvas.drawColor(Color.TRANSPARENT);
865
866        mMask.draw(canvas);
867
868        return restoreToCount;
869    }
870
871    private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
872        if (mMaskingPaint == null) {
873            mMaskingPaint = new Paint();
874        }
875        mMaskingPaint.setXfermode(xfermode);
876        mMaskingPaint.setAlpha(0xFF);
877        return mMaskingPaint;
878    }
879
880    @Override
881    public Rect getDirtyBounds() {
882        if (isProjected()) {
883            final Rect drawingBounds = mDrawingBounds;
884            final Rect dirtyBounds = mDirtyBounds;
885            dirtyBounds.set(drawingBounds);
886            drawingBounds.setEmpty();
887
888            final int cX = (int) mHotspotBounds.exactCenterX();
889            final int cY = (int) mHotspotBounds.exactCenterY();
890            final Rect rippleBounds = mTempRect;
891
892            final Ripple[] activeRipples = mExitingRipples;
893            final int N = mExitingRipplesCount;
894            for (int i = 0; i < N; i++) {
895                activeRipples[i].getBounds(rippleBounds);
896                rippleBounds.offset(cX, cY);
897                drawingBounds.union(rippleBounds);
898            }
899
900            final RippleBackground background = mBackground;
901            if (background != null) {
902                background.getBounds(rippleBounds);
903                rippleBounds.offset(cX, cY);
904                drawingBounds.union(rippleBounds);
905            }
906
907            dirtyBounds.union(drawingBounds);
908            dirtyBounds.union(super.getDirtyBounds());
909            return dirtyBounds;
910        } else {
911            return getBounds();
912        }
913    }
914
915    @Override
916    public ConstantState getConstantState() {
917        return mState;
918    }
919
920    static class RippleState extends LayerState {
921        int[] mTouchThemeAttrs;
922        ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
923        int mMaxRadius = RADIUS_AUTO;
924
925        public RippleState(RippleState orig, RippleDrawable owner, Resources res) {
926            super(orig, owner, res);
927
928            if (orig != null) {
929                mTouchThemeAttrs = orig.mTouchThemeAttrs;
930                mColor = orig.mColor;
931                mMaxRadius = orig.mMaxRadius;
932            }
933        }
934
935        @Override
936        public boolean canApplyTheme() {
937            return mTouchThemeAttrs != null || super.canApplyTheme();
938        }
939
940        @Override
941        public Drawable newDrawable() {
942            return new RippleDrawable(this, null, null);
943        }
944
945        @Override
946        public Drawable newDrawable(Resources res) {
947            return new RippleDrawable(this, res, null);
948        }
949
950        @Override
951        public Drawable newDrawable(Resources res, Theme theme) {
952            return new RippleDrawable(this, res, theme);
953        }
954    }
955
956    /**
957     * Sets the maximum ripple radius in pixels. The default value of
958     * {@link #RADIUS_AUTO} defines the radius as the distance from the center
959     * of the drawable bounds (or hotspot bounds, if specified) to a corner.
960     *
961     * @param maxRadius the maximum ripple radius in pixels or
962     *            {@link #RADIUS_AUTO} to automatically determine the maximum
963     *            radius based on the bounds
964     * @see #getMaxRadius()
965     * @see #setHotspotBounds(int, int, int, int)
966     * @hide
967     */
968    public void setMaxRadius(int maxRadius) {
969        if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
970            throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
971        }
972
973        mState.mMaxRadius = maxRadius;
974    }
975
976    /**
977     * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
978     *         the radius is determined automatically
979     * @see #setMaxRadius(int)
980     * @hide
981     */
982    public int getMaxRadius() {
983        return mState.mMaxRadius;
984    }
985
986    private RippleDrawable(RippleState state, Resources res, Theme theme) {
987        boolean needsTheme = false;
988
989        final RippleState ns;
990        if (theme != null && state != null && state.canApplyTheme()) {
991            ns = new RippleState(state, this, res);
992            needsTheme = true;
993        } else if (state == null) {
994            ns = new RippleState(null, this, res);
995        } else {
996            // We always need a new state since child drawables contain local
997            // state but live within the parent's constant state.
998            // TODO: Move child drawables into local state.
999            ns = new RippleState(state, this, res);
1000        }
1001
1002        if (res != null) {
1003            mDensity = res.getDisplayMetrics().density;
1004        }
1005
1006        mState = ns;
1007        mLayerState = ns;
1008
1009        if (ns.mNum > 0) {
1010            ensurePadding();
1011        }
1012
1013        if (needsTheme) {
1014            applyTheme(theme);
1015        }
1016
1017        initializeFromState();
1018    }
1019
1020    private void initializeFromState() {
1021        // Initialize from constant state.
1022        mMask = findDrawableByLayerId(R.id.mask);
1023    }
1024}
1025