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