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