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