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