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