RippleDrawable.java revision 6ae2d7cefafd7fbf5e7c8d7d3c1869e66056b7f8
1/*
2 * Copyright (C) 2013 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 android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.res.ColorStateList;
22import android.content.res.Resources;
23import android.content.res.Resources.Theme;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.ColorFilter;
28import android.graphics.Paint;
29import android.graphics.PixelFormat;
30import android.graphics.PorterDuff.Mode;
31import android.graphics.PorterDuffXfermode;
32import android.graphics.Rect;
33import android.util.AttributeSet;
34import android.util.DisplayMetrics;
35import android.util.Log;
36
37import com.android.internal.R;
38
39import org.xmlpull.v1.XmlPullParser;
40import org.xmlpull.v1.XmlPullParserException;
41
42import java.io.IOException;
43
44/**
45 * Drawable that shows a ripple effect in response to state changes. The
46 * anchoring position of the ripple for a given state may be specified by
47 * calling {@link #setHotspot(float, float)} with the corresponding state
48 * attribute identifier.
49 * <p>
50 * A touch feedback drawable may contain multiple child layers, including a
51 * special mask layer that is not drawn to the screen. A single layer may be set
52 * as the mask by specifying its android:id value as {@link android.R.id#mask}.
53 * <pre>
54 * <code>&lt!-- A red ripple masked against an opaque rectangle. --/>
55 * &ltripple android:color="#ffff0000">
56 *   &ltitem android:id="@android:id/mask"
57 *         android:drawable="@android:color/white" />
58 * &ltripple /></code>
59 * </pre>
60 * <p>
61 * If a mask layer is set, the ripple effect will be masked against that layer
62 * before it is drawn over the composite of the remaining child layers.
63 * <p>
64 * If no mask layer is set, the ripple effect is masked against the composite
65 * of the child layers.
66 * <pre>
67 * <code>&lt!-- A blue ripple drawn atop a black rectangle. --/>
68 * &ltripple android:color="#ff00ff00">
69 *   &ltitem android:drawable="@android:color/black" />
70 * &ltripple />
71 *
72 * &lt!-- A red ripple drawn atop a drawable resource. --/>
73 * &ltripple android:color="#ff00ff00">
74 *   &ltitem android:drawable="@drawable/my_drawable" />
75 * &ltripple /></code>
76 * </pre>
77 * <p>
78 * If no child layers or mask is specified and the ripple is set as a View
79 * background, the ripple will be drawn atop the first available parent
80 * background within the View's hierarchy. In this case, the drawing region
81 * may extend outside of the Drawable bounds.
82 * <pre>
83 * <code>&lt!-- An unbounded green ripple. --/>
84 * &ltripple android:color="#ff0000ff" /></code>
85 * </pre>
86 *
87 * @attr ref android.R.styleable#RippleDrawable_color
88 */
89public class RippleDrawable extends LayerDrawable {
90    private static final String LOG_TAG = RippleDrawable.class.getSimpleName();
91    private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN);
92    private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP);
93    private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(Mode.SRC_OVER);
94
95    /**
96     * Constant for automatically determining the maximum ripple radius.
97     *
98     * @see #setMaxRadius(int)
99     * @hide
100     */
101    public static final int RADIUS_AUTO = -1;
102
103    /** The maximum number of ripples supported. */
104    private static final int MAX_RIPPLES = 10;
105
106    private final Rect mTempRect = new Rect();
107
108    /** Current ripple effect bounds, used to constrain ripple effects. */
109    private final Rect mHotspotBounds = new Rect();
110
111    /** Current drawing bounds, used to compute dirty region. */
112    private final Rect mDrawingBounds = new Rect();
113
114    /** Current dirty bounds, union of current and previous drawing bounds. */
115    private final Rect mDirtyBounds = new Rect();
116
117    private final RippleState mState;
118
119    /** The masking layer, e.g. the layer with id R.id.mask. */
120    private Drawable mMask;
121
122    /** The current hotspot. May be actively animating or pending entry. */
123    private Ripple mHotspot;
124
125    /**
126     * Lazily-created array of actively animating ripples. Inactive ripples are
127     * pruned during draw(). The locations of these will not change.
128     */
129    private Ripple[] mAnimatingRipples;
130    private int mAnimatingRipplesCount = 0;
131
132    /** Paint used to control appearance of ripples. */
133    private Paint mRipplePaint;
134
135    /** Paint used to control reveal layer masking. */
136    private Paint mMaskingPaint;
137
138    /** Target density of the display into which ripples are drawn. */
139    private float mDensity = 1.0f;
140
141    /** Whether bounds are being overridden. */
142    private boolean mOverrideBounds;
143
144    /** Whether the hotspot is currently active (e.g. focused or pressed). */
145    private boolean mActive;
146
147    /**
148     * Constructor used for drawable inflation.
149     */
150    RippleDrawable() {
151        this(new RippleState(null, null, null), null, null);
152    }
153
154    /**
155     * @hide TO BE REMOVED FOR L-RELEASE
156     */
157    public RippleDrawable(Drawable content, Drawable mask) {
158        this(new RippleState(null, null, null), null, null);
159
160        if (content != null) {
161            addLayer(content, null, 0, 0, 0, 0, 0);
162        }
163
164        if (mask != null) {
165            addLayer(content, null, android.R.id.mask, 0, 0, 0, 0);
166        }
167
168        ensurePadding();
169
170        Log.e(LOG_TAG, "This constructor is being removed", new RuntimeException());
171    }
172
173    /**
174     * Creates a new ripple drawable with the specified ripple color and
175     * optional content and mask drawables.
176     *
177     * @param color The ripple color
178     * @param content The content drawable, may be {@code null}
179     * @param mask The mask drawable, may be {@code null}
180     */
181    public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content,
182            @Nullable Drawable mask) {
183        this(new RippleState(null, null, null), null, null);
184
185        if (color == null) {
186            throw new IllegalArgumentException("RippleDrawable requires a non-null color");
187        }
188
189        if (content != null) {
190            addLayer(content, null, 0, 0, 0, 0, 0);
191        }
192
193        if (mask != null) {
194            addLayer(content, null, android.R.id.mask, 0, 0, 0, 0);
195        }
196
197        setColor(color);
198        ensurePadding();
199    }
200
201    @Override
202    public void setAlpha(int alpha) {
203        super.setAlpha(alpha);
204
205        // TODO: Should we support this?
206    }
207
208    @Override
209    public void setColorFilter(ColorFilter cf) {
210        super.setColorFilter(cf);
211
212        // TODO: Should we support this?
213    }
214
215    @Override
216    public int getOpacity() {
217        // Worst-case scenario.
218        return PixelFormat.TRANSLUCENT;
219    }
220
221    @Override
222    protected boolean onStateChange(int[] stateSet) {
223        super.onStateChange(stateSet);
224
225        // TODO: This would make more sense in a StateListDrawable.
226        boolean active = false;
227        boolean enabled = false;
228        final int N = stateSet.length;
229        for (int i = 0; i < N; i++) {
230            if (stateSet[i] == R.attr.state_enabled) {
231                enabled = true;
232            }
233            if (stateSet[i] == R.attr.state_focused
234                    || stateSet[i] == R.attr.state_pressed) {
235                active = true;
236            }
237        }
238        setActive(active && enabled);
239
240        // Update the paint color. Only applicable when animated in software.
241        if (mRipplePaint != null && mState.mColor != null) {
242            final ColorStateList stateList = mState.mColor;
243            final int newColor = stateList.getColorForState(stateSet, 0);
244            final int oldColor = mRipplePaint.getColor();
245            if (oldColor != newColor) {
246                mRipplePaint.setColor(newColor);
247                invalidateSelf();
248                return true;
249            }
250        }
251
252        return false;
253    }
254
255    private void setActive(boolean active) {
256        if (mActive != active) {
257            mActive = active;
258
259            if (active) {
260                activateHotspot();
261            } else {
262                removeHotspot();
263            }
264        }
265    }
266
267    @Override
268    protected void onBoundsChange(Rect bounds) {
269        super.onBoundsChange(bounds);
270
271        if (!mOverrideBounds) {
272            mHotspotBounds.set(bounds);
273            onHotspotBoundsChanged();
274        }
275
276        invalidateSelf();
277    }
278
279    @Override
280    public boolean setVisible(boolean visible, boolean restart) {
281        if (!visible) {
282            clearHotspots();
283        }
284
285        return super.setVisible(visible, restart);
286    }
287
288    /**
289     * @hide
290     */
291    @Override
292    public boolean isProjected() {
293        return getNumberOfLayers() == 0;
294    }
295
296    @Override
297    public boolean isStateful() {
298        return true;
299    }
300
301    public void setColor(ColorStateList color) {
302        mState.mColor = color;
303        invalidateSelf();
304    }
305
306    @Override
307    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
308            throws XmlPullParserException, IOException {
309        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
310        updateStateFromTypedArray(a);
311        a.recycle();
312
313        // Force padding default to STACK before inflating.
314        setPaddingMode(PADDING_MODE_STACK);
315
316        super.inflate(r, parser, attrs, theme);
317
318        setTargetDensity(r.getDisplayMetrics());
319        initializeFromState();
320    }
321
322    @Override
323    public boolean setDrawableByLayerId(int id, Drawable drawable) {
324        if (super.setDrawableByLayerId(id, drawable)) {
325            if (id == R.id.mask) {
326                mMask = drawable;
327            }
328
329            return true;
330        }
331
332        return false;
333    }
334
335    /**
336     * Specifies how layer padding should affect the bounds of subsequent
337     * layers. The default and recommended value for RippleDrawable is
338     * {@link #PADDING_MODE_STACK}.
339     *
340     * @param mode padding mode, one of:
341     *            <ul>
342     *            <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
343     *            padding of the previous layer
344     *            <li>{@link #PADDING_MODE_STACK} to stack each layer directly
345     *            atop the previous layer
346     *            </ul>
347     * @see #getPaddingMode()
348     */
349    @Override
350    public void setPaddingMode(int mode) {
351        super.setPaddingMode(mode);
352    }
353
354    /**
355     * Initializes the constant state from the values in the typed array.
356     */
357    private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
358        final RippleState state = mState;
359
360        // Extract the theme attributes, if any.
361        state.mTouchThemeAttrs = a.extractThemeAttrs();
362
363        final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
364        if (color != null) {
365            mState.mColor = color;
366        }
367
368        // If we're not waiting on a theme, verify required attributes.
369        if (state.mTouchThemeAttrs == null && mState.mColor == null) {
370            throw new XmlPullParserException(a.getPositionDescription() +
371                    ": <ripple> requires a valid color attribute");
372        }
373    }
374
375    /**
376     * Set the density at which this drawable will be rendered.
377     *
378     * @param metrics The display metrics for this drawable.
379     */
380    private void setTargetDensity(DisplayMetrics metrics) {
381        if (mDensity != metrics.density) {
382            mDensity = metrics.density;
383            invalidateSelf();
384        }
385    }
386
387    @Override
388    public void applyTheme(Theme t) {
389        super.applyTheme(t);
390
391        final RippleState state = mState;
392        if (state == null || state.mTouchThemeAttrs == null) {
393            return;
394        }
395
396        final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
397                R.styleable.RippleDrawable);
398        try {
399            updateStateFromTypedArray(a);
400        } catch (XmlPullParserException e) {
401            throw new RuntimeException(e);
402        } finally {
403            a.recycle();
404        }
405
406        initializeFromState();
407    }
408
409    @Override
410    public boolean canApplyTheme() {
411        return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null;
412    }
413
414    @Override
415    public void setHotspot(float x, float y) {
416        if (mHotspot == null) {
417            mHotspot = new Ripple(this, mHotspotBounds, x, y);
418
419            if (mActive) {
420                activateHotspot();
421            }
422        } else {
423            mHotspot.move(x, y);
424        }
425    }
426
427    /**
428     * Creates an active hotspot at the specified location.
429     */
430    private void activateHotspot() {
431        if (mAnimatingRipplesCount >= MAX_RIPPLES) {
432            // This should never happen unless the user is tapping like a maniac
433            // or there is a bug that's preventing ripples from being removed.
434            Log.d(LOG_TAG, "Max ripple count exceeded", new RuntimeException());
435            return;
436        }
437
438        if (mHotspot == null) {
439            final float x = mHotspotBounds.exactCenterX();
440            final float y = mHotspotBounds.exactCenterY();
441            mHotspot = new Ripple(this, mHotspotBounds, x, y);
442        }
443
444        final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
445        mHotspot.setup(mState.mMaxRadius, color, mDensity);
446        mHotspot.enter();
447
448        if (mAnimatingRipples == null) {
449            mAnimatingRipples = new Ripple[MAX_RIPPLES];
450        }
451        mAnimatingRipples[mAnimatingRipplesCount++] = mHotspot;
452    }
453
454    private void removeHotspot() {
455        if (mHotspot != null) {
456            mHotspot.exit();
457            mHotspot = null;
458        }
459    }
460
461    private void clearHotspots() {
462        if (mHotspot != null) {
463            mHotspot.cancel();
464            mHotspot = null;
465        }
466
467        final int count = mAnimatingRipplesCount;
468        final Ripple[] ripples = mAnimatingRipples;
469        for (int i = 0; i < count; i++) {
470            // Calling cancel may remove the ripple from the animating ripple
471            // array, so cache the reference before nulling it out.
472            final Ripple ripple = ripples[i];
473            ripples[i] = null;
474            ripple.cancel();
475        }
476
477        mAnimatingRipplesCount = 0;
478        invalidateSelf();
479    }
480
481    @Override
482    public void setHotspotBounds(int left, int top, int right, int bottom) {
483        mOverrideBounds = true;
484        mHotspotBounds.set(left, top, right, bottom);
485
486        onHotspotBoundsChanged();
487    }
488
489    /**
490     * Notifies all the animating ripples that the hotspot bounds have changed.
491     */
492    private void onHotspotBoundsChanged() {
493        final int count = mAnimatingRipplesCount;
494        final Ripple[] ripples = mAnimatingRipples;
495        for (int i = 0; i < count; i++) {
496            ripples[i].onHotspotBoundsChanged();
497        }
498    }
499
500    @Override
501    public void draw(Canvas canvas) {
502        final boolean isProjected = isProjected();
503        final boolean hasMask = mMask != null;
504        final boolean drawNonMaskContent = mLayerState.mNum > (hasMask ? 1 : 0);
505        final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE;
506        final Rect bounds = isProjected ? getDirtyBounds() : getBounds();
507
508        // If we have content, draw it into a layer first.
509        final int contentLayer = drawNonMaskContent ?
510                drawContentLayer(canvas, bounds, SRC_OVER) : -1;
511
512        // Next, try to draw the ripples (into a layer if necessary). If we need
513        // to mask against the underlying content, set the xfermode to SRC_ATOP.
514        final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP;
515        final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode);
516
517        // If we have ripples and a non-opaque mask, draw the masking layer.
518        if (rippleLayer >= 0 && drawMask) {
519            drawMaskingLayer(canvas, bounds, DST_IN);
520        }
521
522        // Composite the layers if needed.
523        if (contentLayer >= 0) {
524            canvas.restoreToCount(contentLayer);
525        } else if (rippleLayer >= 0) {
526            canvas.restoreToCount(rippleLayer);
527        }
528    }
529
530    /**
531     * Removes a ripple from the animating ripple list.
532     *
533     * @param ripple the ripple to remove
534     */
535    void removeRipple(Ripple ripple) {
536        // Ripple ripple ripple ripple. Ripple ripple.
537        final Ripple[] ripples = mAnimatingRipples;
538        final int count = mAnimatingRipplesCount;
539        final int index = getRippleIndex(ripple);
540        if (index >= 0) {
541            for (int i = index + 1; i < count; i++) {
542                ripples[i - 1] = ripples[i];
543            }
544            ripples[count - 1] = null;
545            mAnimatingRipplesCount--;
546            invalidateSelf();
547        }
548    }
549
550    private int getRippleIndex(Ripple ripple) {
551        final Ripple[] ripples = mAnimatingRipples;
552        final int count = mAnimatingRipplesCount;
553        for (int i = 0; i < count; i++) {
554            if (ripples[i] == ripple) {
555                return i;
556            }
557        }
558        return -1;
559    }
560
561    private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
562        final ChildDrawable[] array = mLayerState.mChildren;
563        final int count = mLayerState.mNum;
564
565        // We don't need a layer if we don't expect to draw any ripples, we have
566        // an explicit mask, or if the non-mask content is all opaque.
567        boolean needsLayer = false;
568        if (mAnimatingRipplesCount > 0 && mMask == null) {
569            for (int i = 0; i < count; i++) {
570                if (array[i].mId != R.id.mask
571                        && array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
572                    needsLayer = true;
573                    break;
574                }
575            }
576        }
577
578        final Paint maskingPaint = getMaskingPaint(mode);
579        final int restoreToCount = needsLayer ? canvas.saveLayer(bounds.left, bounds.top,
580                bounds.right, bounds.bottom, maskingPaint) : -1;
581
582        // Draw everything except the mask.
583        for (int i = 0; i < count; i++) {
584            if (array[i].mId != R.id.mask) {
585                array[i].mDrawable.draw(canvas);
586            }
587        }
588
589        return restoreToCount;
590    }
591
592    private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
593        final int count = mAnimatingRipplesCount;
594        if (count == 0) {
595            return -1;
596        }
597
598        // Separate the ripple color and alpha channel. The alpha will be
599        // applied when we merge the ripples down to the canvas.
600        final int rippleARGB;
601        if (mState.mColor != null) {
602            rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
603        } else {
604            rippleARGB = Color.TRANSPARENT;
605        }
606
607        if (mRipplePaint == null) {
608            mRipplePaint = new Paint();
609            mRipplePaint.setAntiAlias(true);
610        }
611
612        final int rippleAlpha = Color.alpha(rippleARGB);
613        final Paint ripplePaint = mRipplePaint;
614        ripplePaint.setColor(rippleARGB);
615        ripplePaint.setAlpha(0xFF);
616
617        boolean drewRipples = false;
618        int restoreToCount = -1;
619        int restoreTranslate = -1;
620
621        // Draw ripples and update the animating ripples array.
622        final Ripple[] ripples = mAnimatingRipples;
623        for (int i = 0; i < count; i++) {
624            final Ripple ripple = ripples[i];
625
626            // If we're masking the ripple layer, make sure we have a layer
627            // first. This will merge SRC_OVER (directly) onto the canvas.
628            if (restoreToCount < 0) {
629                final Paint maskingPaint = getMaskingPaint(mode);
630                maskingPaint.setAlpha(rippleAlpha);
631                restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
632                        bounds.right, bounds.bottom, maskingPaint);
633
634                restoreTranslate = canvas.save();
635                // Translate the canvas to the current hotspot bounds.
636                canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
637            }
638
639            drewRipples |= ripple.draw(canvas, ripplePaint);
640        }
641
642        // Always restore the translation.
643        if (restoreTranslate >= 0) {
644            canvas.restoreToCount(restoreTranslate);
645        }
646
647        // If we created a layer with no content, merge it immediately.
648        if (restoreToCount >= 0 && !drewRipples) {
649            canvas.restoreToCount(restoreToCount);
650            restoreToCount = -1;
651        }
652
653        return restoreToCount;
654    }
655
656    private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
657        final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
658                bounds.right, bounds.bottom, getMaskingPaint(mode));
659
660        // Ensure that DST_IN blends using the entire layer.
661        canvas.drawColor(Color.TRANSPARENT);
662
663        mMask.draw(canvas);
664
665        return restoreToCount;
666    }
667
668    private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
669        if (mMaskingPaint == null) {
670            mMaskingPaint = new Paint();
671        }
672        mMaskingPaint.setXfermode(xfermode);
673        mMaskingPaint.setAlpha(0xFF);
674        return mMaskingPaint;
675    }
676
677    @Override
678    public Rect getDirtyBounds() {
679        if (isProjected()) {
680            final Rect drawingBounds = mDrawingBounds;
681            final Rect dirtyBounds = mDirtyBounds;
682            dirtyBounds.set(drawingBounds);
683            drawingBounds.setEmpty();
684
685            final int cX = (int) mHotspotBounds.exactCenterX();
686            final int cY = (int) mHotspotBounds.exactCenterY();
687            final Rect rippleBounds = mTempRect;
688            final Ripple[] activeRipples = mAnimatingRipples;
689            final int N = mAnimatingRipplesCount;
690            for (int i = 0; i < N; i++) {
691                activeRipples[i].getBounds(rippleBounds);
692                rippleBounds.offset(cX, cY);
693                drawingBounds.union(rippleBounds);
694            }
695
696            dirtyBounds.union(drawingBounds);
697            dirtyBounds.union(super.getDirtyBounds());
698            return dirtyBounds;
699        } else {
700            return getBounds();
701        }
702    }
703
704    @Override
705    public ConstantState getConstantState() {
706        return mState;
707    }
708
709    static class RippleState extends LayerState {
710        int[] mTouchThemeAttrs;
711        ColorStateList mColor = null;
712        int mMaxRadius = RADIUS_AUTO;
713
714        public RippleState(RippleState orig, RippleDrawable owner, Resources res) {
715            super(orig, owner, res);
716
717            if (orig != null) {
718                mTouchThemeAttrs = orig.mTouchThemeAttrs;
719                mColor = orig.mColor;
720                mMaxRadius = orig.mMaxRadius;
721            }
722        }
723
724        @Override
725        public boolean canApplyTheme() {
726            return mTouchThemeAttrs != null || super.canApplyTheme();
727        }
728
729        @Override
730        public Drawable newDrawable() {
731            return new RippleDrawable(this, null, null);
732        }
733
734        @Override
735        public Drawable newDrawable(Resources res) {
736            return new RippleDrawable(this, res, null);
737        }
738
739        @Override
740        public Drawable newDrawable(Resources res, Theme theme) {
741            return new RippleDrawable(this, res, theme);
742        }
743    }
744
745    /**
746     * Sets the maximum ripple radius in pixels. The default value of
747     * {@link #RADIUS_AUTO} defines the radius as the distance from the center
748     * of the drawable bounds (or hotspot bounds, if specified) to a corner.
749     *
750     * @param maxRadius the maximum ripple radius in pixels or
751     *            {@link #RADIUS_AUTO} to automatically determine the maximum
752     *            radius based on the bounds
753     * @see #getMaxRadius()
754     * @see #setHotspotBounds(int, int, int, int)
755     * @hide
756     */
757    public void setMaxRadius(int maxRadius) {
758        if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
759            throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
760        }
761
762        mState.mMaxRadius = maxRadius;
763    }
764
765    /**
766     * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
767     *         the radius is determined automatically
768     * @see #setMaxRadius(int)
769     * @hide
770     */
771    public int getMaxRadius() {
772        return mState.mMaxRadius;
773    }
774
775    private RippleDrawable(RippleState state, Resources res, Theme theme) {
776        boolean needsTheme = false;
777
778        final RippleState ns;
779        if (theme != null && state != null && state.canApplyTheme()) {
780            ns = new RippleState(state, this, res);
781            needsTheme = true;
782        } else if (state == null) {
783            ns = new RippleState(null, this, res);
784        } else {
785            // We always need a new state since child drawables contain local
786            // state but live within the parent's constant state.
787            // TODO: Move child drawables into local state.
788            ns = new RippleState(state, this, res);
789        }
790
791        if (res != null) {
792            mDensity = res.getDisplayMetrics().density;
793        }
794
795        mState = ns;
796        mLayerState = ns;
797
798        if (ns.mNum > 0) {
799            ensurePadding();
800        }
801
802        if (needsTheme) {
803            applyTheme(theme);
804        }
805
806        initializeFromState();
807    }
808
809    private void initializeFromState() {
810        // Initialize from constant state.
811        mMask = findDrawableByLayerId(R.id.mask);
812    }
813}
814