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