RippleDrawable.java revision a3f0c2b21a73a82a919abe247c4046d114f3712c
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        final boolean changed = super.onStateChange(stateSet);
246
247        boolean enabled = false;
248        boolean pressed = false;
249        boolean focused = false;
250
251        for (int state : stateSet) {
252            if (state == R.attr.state_enabled) {
253                enabled = true;
254            }
255            if (state == R.attr.state_focused) {
256                focused = true;
257            }
258            if (state == R.attr.state_pressed) {
259                pressed = true;
260            }
261        }
262
263        setRippleActive(enabled && pressed);
264        setBackgroundActive(focused || (enabled && pressed));
265
266        return changed;
267    }
268
269    private void setRippleActive(boolean active) {
270        if (mRippleActive != active) {
271            mRippleActive = active;
272            if (active) {
273                tryRippleEnter();
274            } else {
275                tryRippleExit();
276            }
277        }
278    }
279
280    private void setBackgroundActive(boolean active) {
281        if (mBackgroundActive != active) {
282            mBackgroundActive = active;
283            if (active) {
284                tryBackgroundEnter();
285            } else {
286                tryBackgroundExit();
287            }
288        }
289    }
290
291    @Override
292    protected void onBoundsChange(Rect bounds) {
293        super.onBoundsChange(bounds);
294
295        if (!mOverrideBounds) {
296            mHotspotBounds.set(bounds);
297            onHotspotBoundsChanged();
298        }
299
300        invalidateSelf();
301    }
302
303    @Override
304    public boolean setVisible(boolean visible, boolean restart) {
305        final boolean changed = super.setVisible(visible, restart);
306
307        if (!visible) {
308            clearHotspots();
309        } else if (changed) {
310            // If we just became visible, ensure the background and ripple
311            // visibilities are consistent with their internal states.
312            if (mRippleActive) {
313                tryRippleEnter();
314            }
315
316            if (mBackgroundActive) {
317                tryBackgroundEnter();
318            }
319        }
320
321        return changed;
322    }
323
324    /**
325     * @hide
326     */
327    @Override
328    public boolean isProjected() {
329        return getNumberOfLayers() == 0;
330    }
331
332    @Override
333    public boolean isStateful() {
334        return true;
335    }
336
337    public void setColor(ColorStateList color) {
338        mState.mColor = color;
339        invalidateSelf();
340    }
341
342    @Override
343    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
344            throws XmlPullParserException, IOException {
345        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
346        updateStateFromTypedArray(a);
347        a.recycle();
348
349        // Force padding default to STACK before inflating.
350        setPaddingMode(PADDING_MODE_STACK);
351
352        super.inflate(r, parser, attrs, theme);
353
354        setTargetDensity(r.getDisplayMetrics());
355        initializeFromState();
356    }
357
358    @Override
359    public boolean setDrawableByLayerId(int id, Drawable drawable) {
360        if (super.setDrawableByLayerId(id, drawable)) {
361            if (id == R.id.mask) {
362                mMask = drawable;
363            }
364
365            return true;
366        }
367
368        return false;
369    }
370
371    /**
372     * Specifies how layer padding should affect the bounds of subsequent
373     * layers. The default and recommended value for RippleDrawable is
374     * {@link #PADDING_MODE_STACK}.
375     *
376     * @param mode padding mode, one of:
377     *            <ul>
378     *            <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
379     *            padding of the previous layer
380     *            <li>{@link #PADDING_MODE_STACK} to stack each layer directly
381     *            atop the previous layer
382     *            </ul>
383     * @see #getPaddingMode()
384     */
385    @Override
386    public void setPaddingMode(int mode) {
387        super.setPaddingMode(mode);
388    }
389
390    /**
391     * Initializes the constant state from the values in the typed array.
392     */
393    private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
394        final RippleState state = mState;
395
396        // Account for any configuration changes.
397        state.mChangingConfigurations |= a.getChangingConfigurations();
398
399        // Extract the theme attributes, if any.
400        state.mTouchThemeAttrs = a.extractThemeAttrs();
401
402        final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
403        if (color != null) {
404            mState.mColor = color;
405        }
406
407        verifyRequiredAttributes(a);
408    }
409
410    private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
411        if (mState.mColor == null && (mState.mTouchThemeAttrs == null
412                || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
413            throw new XmlPullParserException(a.getPositionDescription() +
414                    ": <ripple> requires a valid color attribute");
415        }
416    }
417
418    /**
419     * Set the density at which this drawable will be rendered.
420     *
421     * @param metrics The display metrics for this drawable.
422     */
423    private void setTargetDensity(DisplayMetrics metrics) {
424        if (mDensity != metrics.density) {
425            mDensity = metrics.density;
426            invalidateSelf();
427        }
428    }
429
430    @Override
431    public void applyTheme(Theme t) {
432        super.applyTheme(t);
433
434        final RippleState state = mState;
435        if (state == null || state.mTouchThemeAttrs == null) {
436            return;
437        }
438
439        final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
440                R.styleable.RippleDrawable);
441        try {
442            updateStateFromTypedArray(a);
443        } catch (XmlPullParserException e) {
444            throw new RuntimeException(e);
445        } finally {
446            a.recycle();
447        }
448
449        initializeFromState();
450    }
451
452    @Override
453    public boolean canApplyTheme() {
454        return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null;
455    }
456
457    @Override
458    public void setHotspot(float x, float y) {
459        if (mRipple == null || mBackground == null) {
460            mPendingX = x;
461            mPendingY = y;
462            mHasPending = true;
463        }
464
465        if (mRipple != null) {
466            mRipple.move(x, y);
467        }
468    }
469
470    /**
471     * Creates an active hotspot at the specified location.
472     */
473    private void tryBackgroundEnter() {
474        if (mBackground == null) {
475            mBackground = new RippleBackground(this, mHotspotBounds);
476        }
477
478        final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
479        mBackground.setup(mState.mMaxRadius, color, mDensity);
480        mBackground.enter();
481    }
482
483    private void tryBackgroundExit() {
484        if (mBackground != null) {
485            // Don't null out the background, we need it to draw!
486            mBackground.exit();
487        }
488    }
489
490    /**
491     * Attempts to start an enter animation for the active hotspot. Fails if
492     * there are too many animating ripples.
493     */
494    private void tryRippleEnter() {
495        if (mExitingRipplesCount >= MAX_RIPPLES) {
496            // This should never happen unless the user is tapping like a maniac
497            // or there is a bug that's preventing ripples from being removed.
498            return;
499        }
500
501        if (mRipple == null) {
502            final float x;
503            final float y;
504            if (mHasPending) {
505                mHasPending = false;
506                x = mPendingX;
507                y = mPendingY;
508            } else {
509                x = mHotspotBounds.exactCenterX();
510                y = mHotspotBounds.exactCenterY();
511            }
512            mRipple = new Ripple(this, mHotspotBounds, x, y);
513        }
514
515        final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
516        mRipple.setup(mState.mMaxRadius, color, mDensity);
517        mRipple.enter();
518    }
519
520    /**
521     * Attempts to start an exit animation for the active hotspot. Fails if
522     * there is no active hotspot.
523     */
524    private void tryRippleExit() {
525        if (mRipple != null) {
526            if (mExitingRipples == null) {
527                mExitingRipples = new Ripple[MAX_RIPPLES];
528            }
529            mExitingRipples[mExitingRipplesCount++] = mRipple;
530            mRipple.exit();
531            mRipple = null;
532        }
533    }
534
535    /**
536     * Cancels and removes the active ripple, all exiting ripples, and the
537     * background. Nothing will be drawn after this method is called.
538     */
539    private void clearHotspots() {
540        if (mRipple != null) {
541            mRipple.cancel();
542            mRipple = null;
543        }
544
545        if (mBackground != null) {
546            mBackground.cancel();
547            mBackground = null;
548        }
549
550        cancelExitingRipples();
551        invalidateSelf();
552    }
553
554    @Override
555    public void setHotspotBounds(int left, int top, int right, int bottom) {
556        mOverrideBounds = true;
557        mHotspotBounds.set(left, top, right, bottom);
558
559        onHotspotBoundsChanged();
560    }
561
562    /** @hide */
563    @Override
564    public void getHotspotBounds(Rect outRect) {
565        outRect.set(mHotspotBounds);
566    }
567
568    /**
569     * Notifies all the animating ripples that the hotspot bounds have changed.
570     */
571    private void onHotspotBoundsChanged() {
572        final int count = mExitingRipplesCount;
573        final Ripple[] ripples = mExitingRipples;
574        for (int i = 0; i < count; i++) {
575            ripples[i].onHotspotBoundsChanged();
576        }
577
578        if (mRipple != null) {
579            mRipple.onHotspotBoundsChanged();
580        }
581
582        if (mBackground != null) {
583            mBackground.onHotspotBoundsChanged();
584        }
585    }
586
587    /**
588     * Populates <code>outline</code> with the first available layer outline,
589     * excluding the mask layer.
590     *
591     * @param outline Outline in which to place the first available layer outline
592     */
593    @Override
594    public void getOutline(@NonNull Outline outline) {
595        final LayerState state = mLayerState;
596        final ChildDrawable[] children = state.mChildren;
597        final int N = state.mNum;
598        for (int i = 0; i < N; i++) {
599            if (children[i].mId != R.id.mask) {
600                children[i].mDrawable.getOutline(outline);
601                if (!outline.isEmpty()) return;
602            }
603        }
604    }
605
606    @Override
607    public void draw(@NonNull Canvas canvas) {
608        final boolean hasMask = mMask != null;
609        final boolean drawNonMaskContent = mLayerState.mNum > (hasMask ? 1 : 0);
610        final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE;
611        final Rect bounds = getDirtyBounds();
612        final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
613        canvas.clipRect(bounds);
614
615        // If we have content, draw it into a layer first.
616        final int contentLayer;
617        if (drawNonMaskContent) {
618            contentLayer = drawContentLayer(canvas, bounds, SRC_OVER);
619        } else {
620            contentLayer = -1;
621        }
622
623        // Next, try to draw the ripples (into a layer if necessary). If we need
624        // to mask against the underlying content, set the xfermode to SRC_ATOP.
625        final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP;
626
627        // If we have a background and a non-opaque mask, draw the masking layer.
628        final int backgroundLayer = drawBackgroundLayer(canvas, bounds, xfermode, drawMask);
629        if (backgroundLayer >= 0) {
630            if (drawMask) {
631                drawMaskingLayer(canvas, bounds, DST_IN);
632            }
633            canvas.restoreToCount(backgroundLayer);
634        }
635
636        // If we have ripples and a non-opaque mask, draw the masking layer.
637        final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode);
638        if (rippleLayer >= 0) {
639            if (drawMask) {
640                drawMaskingLayer(canvas, bounds, DST_IN);
641            }
642            canvas.restoreToCount(rippleLayer);
643        }
644
645        // If we failed to draw anything, at least draw a color so that
646        // invalidation works correctly.
647        if (contentLayer < 0 && backgroundLayer < 0 && rippleLayer < 0) {
648            canvas.drawColor(Color.TRANSPARENT);
649        }
650
651        canvas.restoreToCount(saveCount);
652    }
653
654    /**
655     * Removes a ripple from the exiting ripple list.
656     *
657     * @param ripple the ripple to remove
658     */
659    void removeRipple(Ripple ripple) {
660        // Ripple ripple ripple ripple. Ripple ripple.
661        final Ripple[] ripples = mExitingRipples;
662        final int count = mExitingRipplesCount;
663        final int index = getRippleIndex(ripple);
664        if (index >= 0) {
665            System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
666            ripples[count - 1] = null;
667            mExitingRipplesCount--;
668
669            invalidateSelf();
670        }
671    }
672
673    private int getRippleIndex(Ripple ripple) {
674        final Ripple[] ripples = mExitingRipples;
675        final int count = mExitingRipplesCount;
676        for (int i = 0; i < count; i++) {
677            if (ripples[i] == ripple) {
678                return i;
679            }
680        }
681        return -1;
682    }
683
684    private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
685        final ChildDrawable[] array = mLayerState.mChildren;
686        final int count = mLayerState.mNum;
687
688        // We don't need a layer if we don't expect to draw any ripples, we have
689        // an explicit mask, or if the non-mask content is all opaque.
690        boolean needsLayer = false;
691        if ((mExitingRipplesCount > 0 || mBackground != null) && mMask == null) {
692            for (int i = 0; i < count; i++) {
693                if (array[i].mId != R.id.mask
694                        && array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
695                    needsLayer = true;
696                    break;
697                }
698            }
699        }
700
701        final Paint maskingPaint = getMaskingPaint(mode);
702        final int restoreToCount = needsLayer ? canvas.saveLayer(bounds.left, bounds.top,
703                bounds.right, bounds.bottom, maskingPaint) : -1;
704
705        // Draw everything except the mask.
706        for (int i = 0; i < count; i++) {
707            if (array[i].mId != R.id.mask) {
708                array[i].mDrawable.draw(canvas);
709            }
710        }
711
712        return restoreToCount;
713    }
714
715    private int drawBackgroundLayer(
716            Canvas canvas, Rect bounds, PorterDuffXfermode mode, boolean drawMask) {
717        int saveCount = -1;
718
719        if (mBackground != null && mBackground.shouldDraw()) {
720            // TODO: We can avoid saveLayer here if we push the xfermode into
721            // the background's render thread animator at exit() time.
722            if (drawMask || mode != SRC_OVER) {
723                saveCount = canvas.saveLayer(bounds.left, bounds.top, bounds.right,
724                        bounds.bottom, getMaskingPaint(mode));
725            }
726
727            final float x = mHotspotBounds.exactCenterX();
728            final float y = mHotspotBounds.exactCenterY();
729            canvas.translate(x, y);
730            mBackground.draw(canvas, getRipplePaint());
731            canvas.translate(-x, -y);
732        }
733
734        return saveCount;
735    }
736
737    private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
738        boolean drewRipples = false;
739        int restoreToCount = -1;
740        int restoreTranslate = -1;
741
742        // Draw ripples and update the animating ripples array.
743        final int count = mExitingRipplesCount;
744        final Ripple[] ripples = mExitingRipples;
745        for (int i = 0; i <= count; i++) {
746            final Ripple ripple;
747            if (i < count) {
748                ripple = ripples[i];
749            } else if (mRipple != null) {
750                ripple = mRipple;
751            } else {
752                continue;
753            }
754
755            // If we're masking the ripple layer, make sure we have a layer
756            // first. This will merge SRC_OVER (directly) onto the canvas.
757            if (restoreToCount < 0) {
758                final Paint maskingPaint = getMaskingPaint(mode);
759                final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
760                final int alpha = Color.alpha(color);
761                maskingPaint.setAlpha(alpha / 2);
762
763                // TODO: We can avoid saveLayer here if we're only drawing one
764                // ripple and we don't have content or a translucent mask.
765                restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
766                        bounds.right, bounds.bottom, maskingPaint);
767
768                // Translate the canvas to the current hotspot bounds.
769                restoreTranslate = canvas.save();
770                canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
771            }
772
773            drewRipples |= ripple.draw(canvas, getRipplePaint());
774        }
775
776        // Always restore the translation.
777        if (restoreTranslate >= 0) {
778            canvas.restoreToCount(restoreTranslate);
779        }
780
781        // If we created a layer with no content, merge it immediately.
782        if (restoreToCount >= 0 && !drewRipples) {
783            canvas.restoreToCount(restoreToCount);
784            restoreToCount = -1;
785        }
786
787        return restoreToCount;
788    }
789
790    private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
791        final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
792                bounds.right, bounds.bottom, getMaskingPaint(mode));
793
794        // Ensure that DST_IN blends using the entire layer.
795        canvas.drawColor(Color.TRANSPARENT);
796
797        mMask.draw(canvas);
798
799        return restoreToCount;
800    }
801
802    private Paint getRipplePaint() {
803        if (mRipplePaint == null) {
804            mRipplePaint = new Paint();
805            mRipplePaint.setAntiAlias(true);
806        }
807        return mRipplePaint;
808    }
809
810    private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
811        if (mMaskingPaint == null) {
812            mMaskingPaint = new Paint();
813        }
814        mMaskingPaint.setXfermode(xfermode);
815        mMaskingPaint.setAlpha(0xFF);
816        return mMaskingPaint;
817    }
818
819    @Override
820    public Rect getDirtyBounds() {
821        if (isProjected()) {
822            final Rect drawingBounds = mDrawingBounds;
823            final Rect dirtyBounds = mDirtyBounds;
824            dirtyBounds.set(drawingBounds);
825            drawingBounds.setEmpty();
826
827            final int cX = (int) mHotspotBounds.exactCenterX();
828            final int cY = (int) mHotspotBounds.exactCenterY();
829            final Rect rippleBounds = mTempRect;
830
831            final Ripple[] activeRipples = mExitingRipples;
832            final int N = mExitingRipplesCount;
833            for (int i = 0; i < N; i++) {
834                activeRipples[i].getBounds(rippleBounds);
835                rippleBounds.offset(cX, cY);
836                drawingBounds.union(rippleBounds);
837            }
838
839            final RippleBackground background = mBackground;
840            if (background != null) {
841                background.getBounds(rippleBounds);
842                rippleBounds.offset(cX, cY);
843                drawingBounds.union(rippleBounds);
844            }
845
846            dirtyBounds.union(drawingBounds);
847            dirtyBounds.union(super.getDirtyBounds());
848            return dirtyBounds;
849        } else {
850            return getBounds();
851        }
852    }
853
854    @Override
855    public ConstantState getConstantState() {
856        return mState;
857    }
858
859    static class RippleState extends LayerState {
860        int[] mTouchThemeAttrs;
861        ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
862        int mMaxRadius = RADIUS_AUTO;
863
864        public RippleState(RippleState orig, RippleDrawable owner, Resources res) {
865            super(orig, owner, res);
866
867            if (orig != null) {
868                mTouchThemeAttrs = orig.mTouchThemeAttrs;
869                mColor = orig.mColor;
870                mMaxRadius = orig.mMaxRadius;
871            }
872        }
873
874        @Override
875        public boolean canApplyTheme() {
876            return mTouchThemeAttrs != null || super.canApplyTheme();
877        }
878
879        @Override
880        public Drawable newDrawable() {
881            return new RippleDrawable(this, null, null);
882        }
883
884        @Override
885        public Drawable newDrawable(Resources res) {
886            return new RippleDrawable(this, res, null);
887        }
888
889        @Override
890        public Drawable newDrawable(Resources res, Theme theme) {
891            return new RippleDrawable(this, res, theme);
892        }
893    }
894
895    /**
896     * Sets the maximum ripple radius in pixels. The default value of
897     * {@link #RADIUS_AUTO} defines the radius as the distance from the center
898     * of the drawable bounds (or hotspot bounds, if specified) to a corner.
899     *
900     * @param maxRadius the maximum ripple radius in pixels or
901     *            {@link #RADIUS_AUTO} to automatically determine the maximum
902     *            radius based on the bounds
903     * @see #getMaxRadius()
904     * @see #setHotspotBounds(int, int, int, int)
905     * @hide
906     */
907    public void setMaxRadius(int maxRadius) {
908        if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
909            throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
910        }
911
912        mState.mMaxRadius = maxRadius;
913    }
914
915    /**
916     * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
917     *         the radius is determined automatically
918     * @see #setMaxRadius(int)
919     * @hide
920     */
921    public int getMaxRadius() {
922        return mState.mMaxRadius;
923    }
924
925    private RippleDrawable(RippleState state, Resources res, Theme theme) {
926        boolean needsTheme = false;
927
928        final RippleState ns;
929        if (theme != null && state != null && state.canApplyTheme()) {
930            ns = new RippleState(state, this, res);
931            needsTheme = true;
932        } else if (state == null) {
933            ns = new RippleState(null, this, res);
934        } else {
935            // We always need a new state since child drawables contain local
936            // state but live within the parent's constant state.
937            // TODO: Move child drawables into local state.
938            ns = new RippleState(state, this, res);
939        }
940
941        if (res != null) {
942            mDensity = res.getDisplayMetrics().density;
943        }
944
945        mState = ns;
946        mLayerState = ns;
947
948        if (ns.mNum > 0) {
949            ensurePadding();
950        }
951
952        if (needsTheme) {
953            applyTheme(theme);
954        }
955
956        initializeFromState();
957    }
958
959    private void initializeFromState() {
960        // Initialize from constant state.
961        mMask = findDrawableByLayerId(R.id.mask);
962    }
963}
964