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