RippleDrawable.java revision 7068c39526459c18a020e29c1ebfa6aed54e2d0f
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    /** @hide */
475    @Override
476    public void getHotspotBounds(Rect outRect) {
477        outRect.set(mHotspotBounds);
478    }
479
480    /**
481     * Notifies all the animating ripples that the hotspot bounds have changed.
482     */
483    private void onHotspotBoundsChanged() {
484        final int count = mAnimatingRipplesCount;
485        final Ripple[] ripples = mAnimatingRipples;
486        for (int i = 0; i < count; i++) {
487            ripples[i].onHotspotBoundsChanged();
488        }
489    }
490
491    /**
492     * Populates <code>outline</code> with the first available layer outline,
493     * excluding the mask layer. Returns <code>true</code> if an outline is
494     * available, <code>false</code> otherwise.
495     *
496     * @param outline Outline in which to place the first available layer outline
497     * @return <code>true</code> if an outline is available
498     */
499    @Override
500    public boolean getOutline(@NonNull Outline outline) {
501        final LayerState state = mLayerState;
502        final ChildDrawable[] children = state.mChildren;
503        final int N = state.mNum;
504        for (int i = 0; i < N; i++) {
505            if (children[i].mId != R.id.mask && children[i].mDrawable.getOutline(outline)) {
506                return true;
507            }
508        }
509        return false;
510    }
511
512    @Override
513    public void draw(@NonNull Canvas canvas) {
514        final boolean isProjected = isProjected();
515        final boolean hasMask = mMask != null;
516        final boolean drawNonMaskContent = mLayerState.mNum > (hasMask ? 1 : 0);
517        final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE;
518        final Rect bounds = isProjected ? getDirtyBounds() : getBounds();
519
520        // If we have content, draw it into a layer first.
521        final int contentLayer = drawNonMaskContent ?
522                drawContentLayer(canvas, bounds, SRC_OVER) : -1;
523
524        // Next, try to draw the ripples (into a layer if necessary). If we need
525        // to mask against the underlying content, set the xfermode to SRC_ATOP.
526        final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP;
527        final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode);
528
529        // If we have ripples and a non-opaque mask, draw the masking layer.
530        if (rippleLayer >= 0 && drawMask) {
531            drawMaskingLayer(canvas, bounds, DST_IN);
532        }
533
534        // Composite the layers if needed.
535        if (contentLayer >= 0) {
536            canvas.restoreToCount(contentLayer);
537        } else if (rippleLayer >= 0) {
538            canvas.restoreToCount(rippleLayer);
539        }
540    }
541
542    /**
543     * Removes a ripple from the animating ripple list.
544     *
545     * @param ripple the ripple to remove
546     */
547    void removeRipple(Ripple ripple) {
548        // Ripple ripple ripple ripple. Ripple ripple.
549        final Ripple[] ripples = mAnimatingRipples;
550        final int count = mAnimatingRipplesCount;
551        final int index = getRippleIndex(ripple);
552        if (index >= 0) {
553            for (int i = index + 1; i < count; i++) {
554                ripples[i - 1] = ripples[i];
555            }
556            ripples[count - 1] = null;
557            mAnimatingRipplesCount--;
558            invalidateSelf();
559        }
560    }
561
562    private int getRippleIndex(Ripple ripple) {
563        final Ripple[] ripples = mAnimatingRipples;
564        final int count = mAnimatingRipplesCount;
565        for (int i = 0; i < count; i++) {
566            if (ripples[i] == ripple) {
567                return i;
568            }
569        }
570        return -1;
571    }
572
573    private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
574        final ChildDrawable[] array = mLayerState.mChildren;
575        final int count = mLayerState.mNum;
576
577        // We don't need a layer if we don't expect to draw any ripples, we have
578        // an explicit mask, or if the non-mask content is all opaque.
579        boolean needsLayer = false;
580        if (mAnimatingRipplesCount > 0 && mMask == null) {
581            for (int i = 0; i < count; i++) {
582                if (array[i].mId != R.id.mask
583                        && array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
584                    needsLayer = true;
585                    break;
586                }
587            }
588        }
589
590        final Paint maskingPaint = getMaskingPaint(mode);
591        final int restoreToCount = needsLayer ? canvas.saveLayer(bounds.left, bounds.top,
592                bounds.right, bounds.bottom, maskingPaint) : -1;
593
594        // Draw everything except the mask.
595        for (int i = 0; i < count; i++) {
596            if (array[i].mId != R.id.mask) {
597                array[i].mDrawable.draw(canvas);
598            }
599        }
600
601        return restoreToCount;
602    }
603
604    private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
605        final int count = mAnimatingRipplesCount;
606        if (count == 0) {
607            return -1;
608        }
609
610        // Separate the ripple color and alpha channel. The alpha will be
611        // applied when we merge the ripples down to the canvas.
612        final int rippleARGB;
613        if (mState.mColor != null) {
614            rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
615        } else {
616            rippleARGB = Color.TRANSPARENT;
617        }
618
619        if (mRipplePaint == null) {
620            mRipplePaint = new Paint();
621            mRipplePaint.setAntiAlias(true);
622        }
623
624        final int rippleAlpha = Color.alpha(rippleARGB);
625        final Paint ripplePaint = mRipplePaint;
626        ripplePaint.setColor(rippleARGB);
627        ripplePaint.setAlpha(0xFF);
628
629        boolean drewRipples = false;
630        int restoreToCount = -1;
631        int restoreTranslate = -1;
632
633        // Draw ripples and update the animating ripples array.
634        final Ripple[] ripples = mAnimatingRipples;
635        for (int i = 0; i < count; i++) {
636            final Ripple ripple = ripples[i];
637
638            // If we're masking the ripple layer, make sure we have a layer
639            // first. This will merge SRC_OVER (directly) onto the canvas.
640            if (restoreToCount < 0) {
641                final Paint maskingPaint = getMaskingPaint(mode);
642                maskingPaint.setAlpha(rippleAlpha);
643                restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
644                        bounds.right, bounds.bottom, maskingPaint);
645
646                restoreTranslate = canvas.save();
647                // Translate the canvas to the current hotspot bounds.
648                canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
649            }
650
651            drewRipples |= ripple.draw(canvas, ripplePaint);
652        }
653
654        // Always restore the translation.
655        if (restoreTranslate >= 0) {
656            canvas.restoreToCount(restoreTranslate);
657        }
658
659        // If we created a layer with no content, merge it immediately.
660        if (restoreToCount >= 0 && !drewRipples) {
661            canvas.restoreToCount(restoreToCount);
662            restoreToCount = -1;
663        }
664
665        return restoreToCount;
666    }
667
668    private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
669        final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
670                bounds.right, bounds.bottom, getMaskingPaint(mode));
671
672        // Ensure that DST_IN blends using the entire layer.
673        canvas.drawColor(Color.TRANSPARENT);
674
675        mMask.draw(canvas);
676
677        return restoreToCount;
678    }
679
680    private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
681        if (mMaskingPaint == null) {
682            mMaskingPaint = new Paint();
683        }
684        mMaskingPaint.setXfermode(xfermode);
685        mMaskingPaint.setAlpha(0xFF);
686        return mMaskingPaint;
687    }
688
689    @Override
690    public Rect getDirtyBounds() {
691        if (isProjected()) {
692            final Rect drawingBounds = mDrawingBounds;
693            final Rect dirtyBounds = mDirtyBounds;
694            dirtyBounds.set(drawingBounds);
695            drawingBounds.setEmpty();
696
697            final int cX = (int) mHotspotBounds.exactCenterX();
698            final int cY = (int) mHotspotBounds.exactCenterY();
699            final Rect rippleBounds = mTempRect;
700            final Ripple[] activeRipples = mAnimatingRipples;
701            final int N = mAnimatingRipplesCount;
702            for (int i = 0; i < N; i++) {
703                activeRipples[i].getBounds(rippleBounds);
704                rippleBounds.offset(cX, cY);
705                drawingBounds.union(rippleBounds);
706            }
707
708            dirtyBounds.union(drawingBounds);
709            dirtyBounds.union(super.getDirtyBounds());
710            return dirtyBounds;
711        } else {
712            return getBounds();
713        }
714    }
715
716    @Override
717    public ConstantState getConstantState() {
718        return mState;
719    }
720
721    static class RippleState extends LayerState {
722        int[] mTouchThemeAttrs;
723        ColorStateList mColor = null;
724        int mMaxRadius = RADIUS_AUTO;
725
726        public RippleState(RippleState orig, RippleDrawable owner, Resources res) {
727            super(orig, owner, res);
728
729            if (orig != null) {
730                mTouchThemeAttrs = orig.mTouchThemeAttrs;
731                mColor = orig.mColor;
732                mMaxRadius = orig.mMaxRadius;
733            }
734        }
735
736        @Override
737        public boolean canApplyTheme() {
738            return mTouchThemeAttrs != null || super.canApplyTheme();
739        }
740
741        @Override
742        public Drawable newDrawable() {
743            return new RippleDrawable(this, null, null);
744        }
745
746        @Override
747        public Drawable newDrawable(Resources res) {
748            return new RippleDrawable(this, res, null);
749        }
750
751        @Override
752        public Drawable newDrawable(Resources res, Theme theme) {
753            return new RippleDrawable(this, res, theme);
754        }
755    }
756
757    /**
758     * Sets the maximum ripple radius in pixels. The default value of
759     * {@link #RADIUS_AUTO} defines the radius as the distance from the center
760     * of the drawable bounds (or hotspot bounds, if specified) to a corner.
761     *
762     * @param maxRadius the maximum ripple radius in pixels or
763     *            {@link #RADIUS_AUTO} to automatically determine the maximum
764     *            radius based on the bounds
765     * @see #getMaxRadius()
766     * @see #setHotspotBounds(int, int, int, int)
767     * @hide
768     */
769    public void setMaxRadius(int maxRadius) {
770        if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
771            throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
772        }
773
774        mState.mMaxRadius = maxRadius;
775    }
776
777    /**
778     * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
779     *         the radius is determined automatically
780     * @see #setMaxRadius(int)
781     * @hide
782     */
783    public int getMaxRadius() {
784        return mState.mMaxRadius;
785    }
786
787    private RippleDrawable(RippleState state, Resources res, Theme theme) {
788        boolean needsTheme = false;
789
790        final RippleState ns;
791        if (theme != null && state != null && state.canApplyTheme()) {
792            ns = new RippleState(state, this, res);
793            needsTheme = true;
794        } else if (state == null) {
795            ns = new RippleState(null, this, res);
796        } else {
797            // We always need a new state since child drawables contain local
798            // state but live within the parent's constant state.
799            // TODO: Move child drawables into local state.
800            ns = new RippleState(state, this, res);
801        }
802
803        if (res != null) {
804            mDensity = res.getDisplayMetrics().density;
805        }
806
807        mState = ns;
808        mLayerState = ns;
809
810        if (ns.mNum > 0) {
811            ensurePadding();
812        }
813
814        if (needsTheme) {
815            applyTheme(theme);
816        }
817
818        initializeFromState();
819    }
820
821    private void initializeFromState() {
822        // Initialize from constant state.
823        mMask = findDrawableByLayerId(R.id.mask);
824    }
825}
826