RippleDrawable.java revision 7a98f74438ac8da8bed5ebdb54c70ce24557a9d8
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 current hotspot. May be actively animating or pending entry. */
99    private Ripple mHotspot;
100
101    /**
102     * Lazily-created array of actively animating ripples. Inactive ripples are
103     * pruned during draw(). The locations of these will not change.
104     */
105    private Ripple[] mAnimatingRipples;
106    private int mAnimatingRipplesCount = 0;
107
108    /** Paint used to control appearance of ripples. */
109    private Paint mRipplePaint;
110
111    /** Paint used to control reveal layer masking. */
112    private Paint mMaskingPaint;
113
114    /** Target density of the display into which ripples are drawn. */
115    private float mDensity = 1.0f;
116
117    /** Whether bounds are being overridden. */
118    private boolean mOverrideBounds;
119
120    /** Whether the hotspot is currently active (e.g. focused or pressed). */
121    private boolean mActive;
122
123    RippleDrawable() {
124        this(null, null);
125    }
126
127    /**
128     * Creates a new ripple drawable with the specified content and mask
129     * drawables.
130     *
131     * @param content The content drawable, may be {@code null}
132     * @param mask The mask drawable, may be {@code null}
133     */
134    public RippleDrawable(Drawable content, Drawable mask) {
135        this(new RippleState(null, null, null), null, null);
136
137        if (content != null) {
138            addLayer(content, null, 0, 0, 0, 0, 0);
139        }
140
141        if (mask != null) {
142            addLayer(content, null, android.R.id.mask, 0, 0, 0, 0);
143        }
144
145        ensurePadding();
146    }
147
148    @Override
149    public void setAlpha(int alpha) {
150        super.setAlpha(alpha);
151
152        // TODO: Should we support this?
153    }
154
155    @Override
156    public void setColorFilter(ColorFilter cf) {
157        super.setColorFilter(cf);
158
159        // TODO: Should we support this?
160    }
161
162    @Override
163    public int getOpacity() {
164        // Worst-case scenario.
165        return PixelFormat.TRANSLUCENT;
166    }
167
168    @Override
169    protected boolean onStateChange(int[] stateSet) {
170        super.onStateChange(stateSet);
171
172        boolean active = false;
173        final int N = stateSet.length;
174        for (int i = 0; i < N; i++) {
175            if (stateSet[i] == R.attr.state_focused
176                    || stateSet[i] == R.attr.state_pressed) {
177                active = true;
178                break;
179            }
180        }
181        setActive(active);
182
183        // Update the paint color. Only applicable when animated in software.
184        if (mRipplePaint != null && mState.mTint != null) {
185            final ColorStateList stateList = mState.mTint;
186            final int newColor = stateList.getColorForState(stateSet, 0);
187            final int oldColor = mRipplePaint.getColor();
188            if (oldColor != newColor) {
189                mRipplePaint.setColor(newColor);
190                invalidateSelf();
191                return true;
192            }
193        }
194
195        return false;
196    }
197
198    private void setActive(boolean active) {
199        if (mActive != active) {
200            mActive = active;
201
202            if (active) {
203                activateHotspot();
204            } else {
205                removeHotspot();
206            }
207        }
208    }
209
210    @Override
211    protected void onBoundsChange(Rect bounds) {
212        super.onBoundsChange(bounds);
213
214        if (!mOverrideBounds) {
215            mHotspotBounds.set(bounds);
216            onHotspotBoundsChanged();
217        }
218
219        invalidateSelf();
220    }
221
222    @Override
223    public boolean setVisible(boolean visible, boolean restart) {
224        if (!visible) {
225            clearHotspots();
226        }
227
228        return super.setVisible(visible, restart);
229    }
230
231    /**
232     * @hide
233     */
234    @Override
235    public boolean isProjected() {
236        return getNumberOfLayers() == 0;
237    }
238
239    @Override
240    public boolean isStateful() {
241        return true;
242    }
243
244    @Override
245    public void setTint(ColorStateList tint, Mode tintMode) {
246        mState.mTint = tint;
247        mState.setTintMode(tintMode);
248        invalidateSelf();
249    }
250
251    @Override
252    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
253            throws XmlPullParserException, IOException {
254        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
255        updateStateFromTypedArray(a);
256        a.recycle();
257
258        // Force padding default to STACK before inflating.
259        setPaddingMode(PADDING_MODE_STACK);
260
261        super.inflate(r, parser, attrs, theme);
262
263        setTargetDensity(r.getDisplayMetrics());
264
265        // Find the mask
266        final int N = getNumberOfLayers();
267        for (int i = 0; i < N; i++) {
268            if (mLayerState.mChildren[i].mId == R.id.mask) {
269                mState.mMask = mLayerState.mChildren[i].mDrawable;
270            }
271        }
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                mState.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
366    @Override
367    public boolean canApplyTheme() {
368        return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null;
369    }
370
371    @Override
372    public void setHotspot(float x, float y) {
373        if (mState.mPinned && !circleContains(mHotspotBounds, x, y)) {
374            x = mHotspotBounds.exactCenterX();
375            y = mHotspotBounds.exactCenterY();
376        }
377
378        if (mHotspot == null) {
379            mHotspot = new Ripple(this, mHotspotBounds, x, y);
380
381            if (mActive) {
382                activateHotspot();
383            }
384        } else {
385            mHotspot.move(x, y);
386        }
387    }
388
389    private boolean circleContains(Rect bounds, float x, float y) {
390        final float pX = bounds.exactCenterX() - x;
391        final float pY = bounds.exactCenterY() - y;
392        final double pointRadius = Math.sqrt(pX * pX + pY * pY);
393
394        final float bX = bounds.width() / 2.0f;
395        final float bY = bounds.height() / 2.0f;
396        final double boundsRadius = Math.sqrt(bX * bX + bY * bY);
397
398        return pointRadius < boundsRadius;
399    }
400
401    /**
402     * Creates an active hotspot at the specified location.
403     */
404    private void activateHotspot() {
405        if (mAnimatingRipplesCount >= MAX_RIPPLES) {
406            // This should never happen unless the user is tapping like a maniac
407            // or there is a bug that's preventing ripples from being removed.
408            Log.d(LOG_TAG, "Max ripple count exceeded", new RuntimeException());
409            return;
410        }
411
412        if (mHotspot == null) {
413            final float x = mHotspotBounds.exactCenterX();
414            final float y = mHotspotBounds.exactCenterY();
415            mHotspot = new Ripple(this, mHotspotBounds, x, y);
416        }
417
418        final int color = mState.mTint.getColorForState(getState(), Color.TRANSPARENT);
419        mHotspot.setup(mState.mMaxRadius, color, mDensity);
420        mHotspot.enter();
421
422        if (mAnimatingRipples == null) {
423            mAnimatingRipples = new Ripple[MAX_RIPPLES];
424        }
425        mAnimatingRipples[mAnimatingRipplesCount++] = mHotspot;
426    }
427
428    private void removeHotspot() {
429        if (mHotspot != null) {
430            mHotspot.exit();
431            mHotspot = null;
432        }
433    }
434
435    private void clearHotspots() {
436        if (mHotspot != null) {
437            mHotspot.cancel();
438            mHotspot = null;
439        }
440
441        final int count = mAnimatingRipplesCount;
442        final Ripple[] ripples = mAnimatingRipples;
443        for (int i = 0; i < count; i++) {
444            // Calling cancel may remove the ripple from the animating ripple
445            // array, so cache the reference before nulling it out.
446            final Ripple ripple = ripples[i];
447            ripples[i] = null;
448            ripple.cancel();
449        }
450
451        mAnimatingRipplesCount = 0;
452        invalidateSelf();
453    }
454
455    @Override
456    public void setHotspotBounds(int left, int top, int right, int bottom) {
457        mOverrideBounds = true;
458        mHotspotBounds.set(left, top, right, bottom);
459
460        onHotspotBoundsChanged();
461    }
462
463    /**
464     * Notifies all the animating ripples that the hotspot bounds have changed.
465     */
466    private void onHotspotBoundsChanged() {
467        final int count = mAnimatingRipplesCount;
468        final Ripple[] ripples = mAnimatingRipples;
469        for (int i = 0; i < count; i++) {
470            ripples[i].onHotspotBoundsChanged();
471        }
472    }
473
474    @Override
475    public void draw(Canvas canvas) {
476        final Rect bounds = isProjected() ? getDirtyBounds() : getBounds();
477
478        // Draw the content into a layer first.
479        final int contentLayer = drawContentLayer(canvas, bounds, SRC_OVER);
480
481        // Next, draw the ripples into a layer.
482        final int rippleLayer = drawRippleLayer(canvas, bounds, mState.mTintXfermode);
483
484        // If we have ripples, draw the masking layer.
485        if (rippleLayer >= 0) {
486            drawMaskingLayer(canvas, bounds, DST_IN);
487        }
488
489        // Composite the layers if needed.
490        if (contentLayer >= 0) {
491            canvas.restoreToCount(contentLayer);
492        } else if (rippleLayer >= 0) {
493            canvas.restoreToCount(rippleLayer);
494        }
495    }
496
497    /**
498     * Removes a ripple from the animating ripple list.
499     *
500     * @param ripple the ripple to remove
501     */
502    void removeRipple(Ripple ripple) {
503        // Ripple ripple ripple ripple. Ripple ripple.
504        final Ripple[] ripples = mAnimatingRipples;
505        final int count = mAnimatingRipplesCount;
506        final int index = getRippleIndex(ripple);
507        if (index >= 0) {
508            for (int i = index + 1; i < count; i++) {
509                ripples[i - 1] = ripples[i];
510            }
511            ripples[count - 1] = null;
512            mAnimatingRipplesCount--;
513            invalidateSelf();
514        }
515    }
516
517    private int getRippleIndex(Ripple ripple) {
518        final Ripple[] ripples = mAnimatingRipples;
519        final int count = mAnimatingRipplesCount;
520        for (int i = 0; i < count; i++) {
521            if (ripples[i] == ripple) {
522                return i;
523            }
524        }
525        return -1;
526    }
527
528    private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
529        final int count = mLayerState.mNum;
530        if (count == 0 || (mState.mMask != null && count == 1)) {
531            return -1;
532        }
533
534        final Paint maskingPaint = getMaskingPaint(mode);
535        final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
536                bounds.right, bounds.bottom, maskingPaint);
537
538        // Draw everything except the mask.
539        final ChildDrawable[] array = mLayerState.mChildren;
540        for (int i = 0; i < count; i++) {
541            if (array[i].mId != R.id.mask) {
542                array[i].mDrawable.draw(canvas);
543            }
544        }
545
546        return restoreToCount;
547    }
548
549    private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
550        final int count = mAnimatingRipplesCount;
551        if (count == 0) {
552            return -1;
553        }
554
555        // Separate the ripple color and alpha channel. The alpha will be
556        // applied when we merge the ripples down to the canvas.
557        final int rippleARGB;
558        if (mState.mTint != null) {
559            rippleARGB = mState.mTint.getColorForState(getState(), Color.TRANSPARENT);
560        } else {
561            rippleARGB = Color.TRANSPARENT;
562        }
563
564        final int rippleAlpha = Color.alpha(rippleARGB);
565        final int rippleColor = rippleARGB | (0xFF << 24);
566        if (mRipplePaint == null) {
567            mRipplePaint = new Paint();
568            mRipplePaint.setAntiAlias(true);
569        }
570        final Paint ripplePaint = mRipplePaint;
571        ripplePaint.setColor(rippleColor);
572
573        boolean drewRipples = false;
574        int restoreToCount = -1;
575        int restoreTranslate = -1;
576
577        // Draw ripples and update the animating ripples array.
578        final Ripple[] ripples = mAnimatingRipples;
579        for (int i = 0; i < count; i++) {
580            final Ripple ripple = ripples[i];
581
582            // If we're masking the ripple layer, make sure we have a layer
583            // first. This will merge SRC_OVER (directly) onto the canvas.
584            if (restoreToCount < 0) {
585                final Paint maskingPaint = getMaskingPaint(mode);
586                maskingPaint.setAlpha(rippleAlpha);
587                restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
588                        bounds.right, bounds.bottom, maskingPaint);
589                maskingPaint.setAlpha(255);
590
591                restoreTranslate = canvas.save();
592                // Translate the canvas to the current hotspot bounds.
593                canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
594            }
595
596            drewRipples |= ripple.draw(canvas, ripplePaint);
597        }
598
599        // Always restore the translation.
600        if (restoreTranslate >= 0) {
601            canvas.restoreToCount(restoreTranslate);
602        }
603
604        // If we created a layer with no content, merge it immediately.
605        if (restoreToCount >= 0 && !drewRipples) {
606            canvas.restoreToCount(restoreToCount);
607            restoreToCount = -1;
608        }
609
610        return restoreToCount;
611    }
612
613    private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
614        final Drawable mask = mState.mMask;
615        if (mask == null) {
616            return -1;
617        }
618
619        final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
620                bounds.right, bounds.bottom, getMaskingPaint(mode));
621
622        mask.draw(canvas);
623
624        return restoreToCount;
625    }
626
627    private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
628        if (mMaskingPaint == null) {
629            mMaskingPaint = new Paint();
630        }
631        mMaskingPaint.setXfermode(xfermode);
632        return mMaskingPaint;
633    }
634
635    @Override
636    public Rect getDirtyBounds() {
637        final Rect drawingBounds = mDrawingBounds;
638        final Rect dirtyBounds = mDirtyBounds;
639        dirtyBounds.set(drawingBounds);
640        drawingBounds.setEmpty();
641
642        final int cX = (int) mHotspotBounds.exactCenterX();
643        final int cY = (int) mHotspotBounds.exactCenterY();
644        final Rect rippleBounds = mTempRect;
645        final Ripple[] activeRipples = mAnimatingRipples;
646        final int N = mAnimatingRipplesCount;
647        for (int i = 0; i < N; i++) {
648            activeRipples[i].getBounds(rippleBounds);
649            rippleBounds.offset(cX, cY);
650            drawingBounds.union(rippleBounds);
651        }
652
653        dirtyBounds.union(drawingBounds);
654        dirtyBounds.union(super.getDirtyBounds());
655        return dirtyBounds;
656    }
657
658    @Override
659    public ConstantState getConstantState() {
660        return mState;
661    }
662
663    static class RippleState extends LayerState {
664        int[] mTouchThemeAttrs;
665        ColorStateList mTint = null;
666        PorterDuffXfermode mTintXfermode = SRC_ATOP;
667        Drawable mMask;
668        int mMaxRadius = RADIUS_AUTO;
669        boolean mPinned = false;
670
671        public RippleState(RippleState orig, RippleDrawable owner, Resources res) {
672            super(orig, owner, res);
673
674            if (orig != null) {
675                mTouchThemeAttrs = orig.mTouchThemeAttrs;
676                mTint = orig.mTint;
677                mTintXfermode = orig.mTintXfermode;
678                mMaxRadius = orig.mMaxRadius;
679                mPinned = orig.mPinned;
680            }
681        }
682
683        public void setTintMode(Mode mode) {
684            mTintXfermode = new PorterDuffXfermode(mode);
685        }
686
687        public PorterDuffXfermode getTintXfermode() {
688            return mTintXfermode;
689        }
690
691        @Override
692        public boolean canApplyTheme() {
693            return mTouchThemeAttrs != null || super.canApplyTheme();
694        }
695
696        @Override
697        public Drawable newDrawable() {
698            return new RippleDrawable(this, null, null);
699        }
700
701        @Override
702        public Drawable newDrawable(Resources res) {
703            return new RippleDrawable(this, res, null);
704        }
705
706        @Override
707        public Drawable newDrawable(Resources res, Theme theme) {
708            return new RippleDrawable(this, res, theme);
709        }
710    }
711
712    /**
713     * Sets the maximum ripple radius in pixels. The default value of
714     * {@link #RADIUS_AUTO} defines the radius as the distance from the center
715     * of the drawable bounds (or hotspot bounds, if specified) to a corner.
716     *
717     * @param maxRadius the maximum ripple radius in pixels or
718     *            {@link #RADIUS_AUTO} to automatically determine the maximum
719     *            radius based on the bounds
720     * @see #getMaxRadius()
721     * @see #setHotspotBounds(int, int, int, int)
722     * @hide
723     */
724    public void setMaxRadius(int maxRadius) {
725        if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
726            throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
727        }
728
729        mState.mMaxRadius = maxRadius;
730    }
731
732    /**
733     * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
734     *         the radius is determined automatically
735     * @see #setMaxRadius(int)
736     * @hide
737     */
738    public int getMaxRadius() {
739        return mState.mMaxRadius;
740    }
741
742    private RippleDrawable(RippleState state, Resources res, Theme theme) {
743        boolean needsTheme = false;
744
745        final RippleState ns;
746        if (theme != null && state != null && state.canApplyTheme()) {
747            ns = new RippleState(state, this, res);
748            needsTheme = true;
749        } else if (state == null) {
750            ns = new RippleState(null, this, res);
751        } else {
752            // We always need a new state since child drawables contain local
753            // state but live within the parent's constant state.
754            // TODO: Move child drawables into local state.
755            ns = new RippleState(state, this, res);
756        }
757
758        if (res != null) {
759            mDensity = res.getDisplayMetrics().density;
760        }
761
762        mState = ns;
763        mState.mMask = findDrawableByLayerId(R.id.mask);
764
765        mLayerState = ns;
766
767        if (ns.mNum > 0) {
768            ensurePadding();
769        }
770
771        if (needsTheme) {
772            applyTheme(theme);
773        }
774    }
775}
776