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