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