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