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