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