RippleDrawable.java revision f90b6bd54f60960a790b5bd507a0d75d961a981a
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, we have
723        // an explicit mask, or if the non-mask content is all opaque.
724        boolean needsLayer = false;
725        if ((mExitingRipplesCount > 0 || mBackground != null) && mMask == null) {
726            for (int i = 0; i < count; i++) {
727                if (array[i].mId != R.id.mask
728                        && array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
729                    needsLayer = true;
730                    break;
731                }
732            }
733        }
734
735        final Paint maskingPaint = getMaskingPaint(mode);
736        final int restoreToCount = needsLayer ? canvas.saveLayer(bounds.left, bounds.top,
737                bounds.right, bounds.bottom, maskingPaint) : -1;
738
739        // Draw everything except the mask.
740        for (int i = 0; i < count; i++) {
741            if (array[i].mId != R.id.mask) {
742                array[i].mDrawable.draw(canvas);
743            }
744        }
745
746        return restoreToCount;
747    }
748
749    private int drawBackgroundLayer(
750            Canvas canvas, Rect bounds, PorterDuffXfermode mode, boolean drawMask) {
751        int saveCount = -1;
752
753        if (mBackground != null && mBackground.shouldDraw()) {
754            // TODO: We can avoid saveLayer here if we push the xfermode into
755            // the background's render thread animator at exit() time.
756            if (drawMask || mode != SRC_OVER) {
757                saveCount = canvas.saveLayer(bounds.left, bounds.top, bounds.right,
758                        bounds.bottom, getMaskingPaint(mode));
759            }
760
761            final float x = mHotspotBounds.exactCenterX();
762            final float y = mHotspotBounds.exactCenterY();
763            canvas.translate(x, y);
764            mBackground.draw(canvas, getRipplePaint());
765            canvas.translate(-x, -y);
766        }
767
768        return saveCount;
769    }
770
771    private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
772        boolean drewRipples = false;
773        int restoreToCount = -1;
774        int restoreTranslate = -1;
775
776        // Draw ripples and update the animating ripples array.
777        final int count = mExitingRipplesCount;
778        final Ripple[] ripples = mExitingRipples;
779        for (int i = 0; i <= count; i++) {
780            final Ripple ripple;
781            if (i < count) {
782                ripple = ripples[i];
783            } else if (mRipple != null) {
784                ripple = mRipple;
785            } else {
786                continue;
787            }
788
789            // If we're masking the ripple layer, make sure we have a layer
790            // first. This will merge SRC_OVER (directly) onto the canvas.
791            if (restoreToCount < 0) {
792                final Paint maskingPaint = getMaskingPaint(mode);
793                final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
794                final int alpha = Color.alpha(color);
795                maskingPaint.setAlpha(alpha / 2);
796
797                // TODO: We can avoid saveLayer here if we're only drawing one
798                // ripple and we don't have content or a translucent mask.
799                restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
800                        bounds.right, bounds.bottom, maskingPaint);
801
802                // Translate the canvas to the current hotspot bounds.
803                restoreTranslate = canvas.save();
804                canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
805            }
806
807            drewRipples |= ripple.draw(canvas, getRipplePaint());
808        }
809
810        // Always restore the translation.
811        if (restoreTranslate >= 0) {
812            canvas.restoreToCount(restoreTranslate);
813        }
814
815        // If we created a layer with no content, merge it immediately.
816        if (restoreToCount >= 0 && !drewRipples) {
817            canvas.restoreToCount(restoreToCount);
818            restoreToCount = -1;
819        }
820
821        return restoreToCount;
822    }
823
824    private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
825        final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
826                bounds.right, bounds.bottom, getMaskingPaint(mode));
827
828        // Ensure that DST_IN blends using the entire layer.
829        canvas.drawColor(Color.TRANSPARENT);
830
831        mMask.draw(canvas);
832
833        return restoreToCount;
834    }
835
836    private Paint getRipplePaint() {
837        if (mRipplePaint == null) {
838            mRipplePaint = new Paint();
839            mRipplePaint.setAntiAlias(true);
840        }
841        return mRipplePaint;
842    }
843
844    private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
845        if (mMaskingPaint == null) {
846            mMaskingPaint = new Paint();
847        }
848        mMaskingPaint.setXfermode(xfermode);
849        mMaskingPaint.setAlpha(0xFF);
850        return mMaskingPaint;
851    }
852
853    @Override
854    public Rect getDirtyBounds() {
855        if (getNumberOfLayers() == 0) {
856            final Rect drawingBounds = mDrawingBounds;
857            final Rect dirtyBounds = mDirtyBounds;
858            dirtyBounds.set(drawingBounds);
859            drawingBounds.setEmpty();
860
861            final int cX = (int) mHotspotBounds.exactCenterX();
862            final int cY = (int) mHotspotBounds.exactCenterY();
863            final Rect rippleBounds = mTempRect;
864
865            final Ripple[] activeRipples = mExitingRipples;
866            final int N = mExitingRipplesCount;
867            for (int i = 0; i < N; i++) {
868                activeRipples[i].getBounds(rippleBounds);
869                rippleBounds.offset(cX, cY);
870                drawingBounds.union(rippleBounds);
871            }
872
873            final RippleBackground background = mBackground;
874            if (background != null) {
875                background.getBounds(rippleBounds);
876                rippleBounds.offset(cX, cY);
877                drawingBounds.union(rippleBounds);
878            }
879
880            dirtyBounds.union(drawingBounds);
881            dirtyBounds.union(super.getDirtyBounds());
882            return dirtyBounds;
883        } else {
884            return getBounds();
885        }
886    }
887
888    @Override
889    public ConstantState getConstantState() {
890        return mState;
891    }
892
893    @Override
894    public Drawable mutate() {
895        super.mutate();
896
897        // LayerDrawable creates a new state using createConstantState, so
898        // this should always be a safe cast.
899        mState = (RippleState) mLayerState;
900        return this;
901    }
902
903    @Override
904    RippleState createConstantState(LayerState state, Resources res) {
905        return new RippleState(state, this, res);
906    }
907
908    static class RippleState extends LayerState {
909        int[] mTouchThemeAttrs;
910        ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
911        int mMaxRadius = RADIUS_AUTO;
912
913        public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
914            super(orig, owner, res);
915
916            if (orig != null && orig instanceof RippleState) {
917                final RippleState origs = (RippleState) orig;
918                mTouchThemeAttrs = origs.mTouchThemeAttrs;
919                mColor = origs.mColor;
920                mMaxRadius = origs.mMaxRadius;
921            }
922        }
923
924        @Override
925        public boolean canApplyTheme() {
926            return mTouchThemeAttrs != null || super.canApplyTheme();
927        }
928
929        @Override
930        public Drawable newDrawable() {
931            return new RippleDrawable(this, null, null);
932        }
933
934        @Override
935        public Drawable newDrawable(Resources res) {
936            return new RippleDrawable(this, res, null);
937        }
938
939        @Override
940        public Drawable newDrawable(Resources res, Theme theme) {
941            return new RippleDrawable(this, res, theme);
942        }
943    }
944
945    /**
946     * Sets the maximum ripple radius in pixels. The default value of
947     * {@link #RADIUS_AUTO} defines the radius as the distance from the center
948     * of the drawable bounds (or hotspot bounds, if specified) to a corner.
949     *
950     * @param maxRadius the maximum ripple radius in pixels or
951     *            {@link #RADIUS_AUTO} to automatically determine the maximum
952     *            radius based on the bounds
953     * @see #getMaxRadius()
954     * @see #setHotspotBounds(int, int, int, int)
955     * @hide
956     */
957    public void setMaxRadius(int maxRadius) {
958        if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
959            throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
960        }
961
962        mState.mMaxRadius = maxRadius;
963    }
964
965    /**
966     * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
967     *         the radius is determined automatically
968     * @see #setMaxRadius(int)
969     * @hide
970     */
971    public int getMaxRadius() {
972        return mState.mMaxRadius;
973    }
974
975    private RippleDrawable(RippleState state, Resources res, Theme theme) {
976        boolean needsTheme = false;
977
978        final RippleState ns;
979        if (theme != null && state != null && state.canApplyTheme()) {
980            ns = new RippleState(state, this, res);
981            needsTheme = true;
982        } else if (state == null) {
983            ns = new RippleState(null, this, res);
984        } else {
985            // We always need a new state since child drawables contain local
986            // state but live within the parent's constant state.
987            // TODO: Move child drawables into local state.
988            ns = new RippleState(state, this, res);
989        }
990
991        if (res != null) {
992            mDensity = res.getDisplayMetrics().density;
993        }
994
995        mState = ns;
996        mLayerState = ns;
997
998        if (ns.mNum > 0) {
999            ensurePadding();
1000        }
1001
1002        if (needsTheme) {
1003            applyTheme(theme);
1004        }
1005
1006        initializeFromState();
1007    }
1008
1009    private void initializeFromState() {
1010        // Initialize from constant state.
1011        mMask = findDrawableByLayerId(R.id.mask);
1012    }
1013}
1014