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