package carbon.drawable.ripple; import android.annotation.TargetApi; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.DisplayMetrics; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.Arrays; import carbon.Carbon; import carbon.R; /** * Drawable that shows a ripple effect in response to state changes. The * anchoring position of the ripple for a given state may be specified by * calling {@link #setHotspot(float, float)} with the corresponding state * attribute identifier. * <p/> * A touch feedback drawable may contain multiple child layers, including a * special mask layer that is not drawn to the screen. A single layer may be * set as the mask from XML by specifying its {@code android:id} value as * {@link android.R.id#mask}. At run time, a single layer may be set as the * mask using {@code setId(..., R.id.carbon_mask)} or an existing mask layer * may be replaced using {@code setDrawableByLayerId(R.id.carbon_mask, ...)}. * <pre> * <code><!-- A red ripple masked against an opaque rectangle. --/> * <ripple android:color="#ffff0000"> * <item android:id="@android:id/mask" * android:drawable="@android:color/white" /> * </ripple></code> * </pre> * <p/> * If a mask layer is set, the ripple effect will be masked against that layer * before it is drawn over the composite of the remaining child layers. * <p/> * If no mask layer is set, the ripple effect is masked against the composite * of the child layers. * <pre> * <code><!-- A green ripple drawn atop a black rectangle. --/> * <ripple android:color="#ff00ff00"> * <item android:drawable="@android:color/black" /> * </ripple> * * <!-- A blue ripple drawn atop a drawable resource. --/> * <ripple android:color="#ff0000ff"> * <item android:drawable="@drawable/my_drawable" /> * </ripple></code> * </pre> * <p/> * If no child layers or mask is specified and the ripple is set as a View * background, the ripple will be drawn atop the first available parent * background within the View's hierarchy. In this case, the drawing region * may extend outside of the Drawable bounds. * <pre> * <code><!-- An unbounded red ripple. --/> * <ripple android:color="#ffff0000" /></code> * </pre> * * @attr ref android.R.styleable#RippleDrawable_color */ public class RippleDrawableICS extends LayerDrawable implements RippleDrawable { /** * Radius value that specifies the ripple radius should be computed based * on the size of the ripple's container. */ public static final int RADIUS_AUTO = -1; private static final int MASK_UNKNOWN = -1; private static final int MASK_NONE = 0; private static final int MASK_CONTENT = 1; private static final int MASK_EXPLICIT = 2; /** * The maximum number of ripples supported. */ private static final int MAX_RIPPLES = 10; private final Rect mTempRect = new Rect(); /** * Current ripple effect bounds, used to constrain ripple effects. */ private final Rect mHotspotBounds = new Rect(); /** * Current drawing bounds, used to compute dirty region. */ private final Rect mDrawingBounds = new Rect(); /** * Current dirty bounds, union of current and previous drawing bounds. */ private final Rect mDirtyBounds = new Rect(); /** * Mirrors mLayerState with some extra information. */ private RippleState mState; /** * The masking layer, e.g. the layer with id R.id.mask. */ private Drawable mMask; /** * The current background. May be actively animating or pending entry. */ private RippleBackground mBackground; private Bitmap mMaskBuffer; private BitmapShader mMaskShader; private Canvas mMaskCanvas; private Matrix mMaskMatrix; private PorterDuffColorFilter mMaskColorFilter; private boolean mHasValidMask; /** * Whether we expect to draw a background when visible. */ private boolean mBackgroundActive; /** * The current ripple. May be actively animating or pending entry. */ private RippleForeground mRipple; /** * Whether we expect to draw a ripple when visible. */ private boolean mRippleActive; // Hotspot coordinates that are awaiting activation. private float mPendingX; private float mPendingY; private boolean mHasPending; /** * Lazily-created array of actively animating ripples. Inactive ripples are * pruned during draw(). The locations of these will not change. */ private RippleForeground[] mExitingRipples; private int mExitingRipplesCount = 0; /** * Paint used to control appearance of ripples. */ private Paint mRipplePaint; /** * Target density of the display into which ripples are drawn. */ private float mDensity = 1.0f; /** * Whether bounds are being overridden. */ private boolean mOverrideBounds; Drawable background; private Style style; private boolean useHotspot; /** * Constructor used for drawable inflation. */ RippleDrawableICS() { this(new RippleState(null, null, null), null); } public RippleDrawableICS(ColorStateList color, Drawable background, Style style) { this(color, background, style == Style.Borderless ? null : new ColorDrawable(0xffffffff)); this.style = style; } /** * Creates a new ripple drawable with the specified ripple color and * optional content and mask drawables. * * @param color The ripple color * @param content The content drawable, may be {@code null} * @param mask The mask drawable, may be {@code null} */ public RippleDrawableICS(@NonNull ColorStateList color, @Nullable Drawable content, @Nullable Drawable mask) { this(new RippleState(null, null, null), null); if (color == null) { throw new IllegalArgumentException("RippleDrawable requires a non-null color"); } if (content != null) { addLayer(content, null, 0, 0, 0, 0, 0); } if (mask != null) { addLayer(mask, null, R.id.carbon_mask, 0, 0, 0, 0); } background = content; setColor(color); ensurePadding(); refreshPadding(); updateLocalState(); } @Override public void jumpToCurrentState() { super.jumpToCurrentState(); if (mRipple != null) { mRipple.end(); } if (mBackground != null) { mBackground.end(); } cancelExitingRipples(); } private void cancelExitingRipples() { final int count = mExitingRipplesCount; final RippleForeground[] ripples = mExitingRipples; for (int i = 0; i < count; i++) { ripples[i].end(); } if (ripples != null) { Arrays.fill(ripples, 0, count, null); } mExitingRipplesCount = 0; // Always draw an additional "clean" frame after canceling animations. invalidateSelf(false); } @Override public int getOpacity() { // Worst-case scenario. return PixelFormat.TRANSLUCENT; } @Override protected boolean onStateChange(int[] stateSet) { final boolean changed = super.onStateChange(stateSet); boolean enabled = false; boolean pressed = false; boolean focused = false; for (int state : stateSet) { if (state == android.R.attr.state_enabled) { enabled = true; } else if (state == android.R.attr.state_focused) { focused = true; } else if (state == android.R.attr.state_pressed) { pressed = true; } } setRippleActive(enabled && pressed); setBackgroundActive(focused || (enabled && pressed), focused); return changed; } private void setRippleActive(boolean active) { if (mRippleActive != active) { mRippleActive = active; if (active) { tryRippleEnter(); } else { tryRippleExit(); } } } private void setBackgroundActive(boolean active, boolean focused) { if (mBackgroundActive != active) { mBackgroundActive = active; if (active) { tryBackgroundEnter(focused); } else { tryBackgroundExit(); } } } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); if (!mOverrideBounds) { mHotspotBounds.set(bounds); onHotspotBoundsChanged(); } if (mBackground != null) { mBackground.onBoundsChange(); } if (mRipple != null) { mRipple.onBoundsChange(); } invalidateSelf(); } @Override public boolean setVisible(boolean visible, boolean restart) { final boolean changed = super.setVisible(visible, restart); if (!visible) { clearHotspots(); } else if (changed) { // If we just became visible, ensure the background and ripple // visibilities are consistent with their internal states. if (mRippleActive) { tryRippleEnter(); } if (mBackgroundActive) { tryBackgroundEnter(false); } // Skip animations, just show the correct final states. jumpToCurrentState(); } return changed; } /** * @hide / * @Override public boolean isProjected() { // If the layer is bounded, then we don't need to * project. if (isBounded()) { return false; } * <p/> * // Otherwise, if the maximum radius is contained entirely within the // bounds then we don't * need to project. This is sort of a hack to // prevent check box ripples from being projected * across the edges of // scroll views. It does not impact rendering performance, and it can // * be removed once we have better handling of projection in scrollable // views. final int * radius = mState.mMaxRadius; final Rect drawableBounds = getBounds(); final Rect hotspotBounds * = mHotspotBounds; if (radius != RADIUS_AUTO && radius <= hotspotBounds.width() / 2 && radius * <= hotspotBounds.height() / 2 && (drawableBounds.equals(hotspotBounds) || * drawableBounds.contains(hotspotBounds))) { return false; } * <p/> * return true; } */ private boolean isBounded() { return getNumberOfLayers() > 0; } @Override public boolean isStateful() { return true; } /** * Sets the ripple color. * * @param color Ripple color as a color state list. * @attr ref android.R.styleable#RippleDrawable_color */ public void setColor(ColorStateList color) { mState.mColor = color; invalidateSelf(false); } /** * Sets the radius in pixels of the fully expanded ripple. * * @param radius ripple radius in pixels, or {@link #RADIUS_AUTO} to compute the radius based on * the container size * @attr ref android.R.styleable#RippleDrawable_radius */ public void setRadius(int radius) { mState.mMaxRadius = radius; invalidateSelf(false); } /** * @return the radius in pixels of the fully expanded ripple if an explicit radius has been set, * or {@link #RADIUS_AUTO} if the radius is computed based on the container size * @attr ref android.R.styleable#RippleDrawable_radius */ public int getRadius() { return mState.mMaxRadius; } @Override public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme) throws XmlPullParserException, IOException { final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable); updateStateFromTypedArray(a); a.recycle(); // Force padding default to STACK before inflating. setPaddingMode(PADDING_MODE_STACK); super.inflate(r, parser, attrs, theme); setTargetDensity(r.getDisplayMetrics()); updateLocalState(); } @Override public boolean setDrawableByLayerId(int id, Drawable drawable) { if (super.setDrawableByLayerId(id, drawable)) { if (id == R.id.carbon_mask) { mMask = drawable; mHasValidMask = false; } return true; } return false; } /** * Specifies how layer padding should affect the bounds of subsequent * layers. The default and recommended value for RippleDrawable is * {@link #PADDING_MODE_STACK}. * * @param mode padding mode, one of: <ul> <li>{@link #PADDING_MODE_NEST} to nest each layer * inside the padding of the previous layer <li>{@link #PADDING_MODE_STACK} to stack * each layer directly atop the previous layer </ul> * @see #getPaddingMode() */ @Override public void setPaddingMode(int mode) { super.setPaddingMode(mode); } /** * Initializes the constant state from the values in the typed array. */ private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { final RippleState state = mState; // Account for any configuration changes. state.mChangingConfigurations |= TypedArrayCompat.getChangingConfigurations(a); // Extract the theme attributes, if any. state.mTouchThemeAttrs = TypedArrayCompat.extractThemeAttrs(a); final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_android_color); if (color != null) { mState.mColor = color; } mState.mMaxRadius = a.getDimensionPixelSize( R.styleable.RippleDrawable_android_radius, mState.mMaxRadius); verifyRequiredAttributes(a); } private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { if (mState.mColor == null && (mState.mTouchThemeAttrs == null || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_android_color] == 0)) { throw new XmlPullParserException(a.getPositionDescription() + ": <ripple> requires a valid color attribute"); } } /** * Set the density at which this drawable will be rendered. * * @param metrics The display metrics for this drawable. */ private void setTargetDensity(DisplayMetrics metrics) { if (mDensity != metrics.density) { mDensity = metrics.density; invalidateSelf(false); } } @Override public void applyTheme(Resources.Theme t) { super.applyTheme(t); final RippleState state = mState; if (state == null) { return; } /* if (state.mTouchThemeAttrs != null) { final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, R.styleable.RippleDrawable); try { updateStateFromTypedArray(a); } catch (XmlPullParserException e) { throw new RuntimeException(e); } finally { a.recycle(); } }*/ /* if (state.mColor != null && state.mColor.canApplyTheme()) { state.mColor = state.mColor.obtainForTheme(t); }*/ updateLocalState(); } @Override public boolean canApplyTheme() { return (mState != null && mState.canApplyTheme()) || super.canApplyTheme(); } @Override public void setHotspot(float x, float y) { if (mRipple == null || mBackground == null) { mPendingX = x; mPendingY = y; mHasPending = true; } if (mRipple != null) { mRipple.move(x, y); } } /** * Creates an active hotspot at the specified location. */ private void tryBackgroundEnter(boolean focused) { if (mBackground == null) { mBackground = new RippleBackground(this, mHotspotBounds); } mBackground.setup(mState.mMaxRadius, mDensity); mBackground.enter(focused); } private void tryBackgroundExit() { if (mBackground != null) { // Don't null out the background, we need it to draw! mBackground.exit(); } } /** * Attempts to start an enter animation for the active hotspot. Fails if * there are too many animating ripples. */ private void tryRippleEnter() { if (mExitingRipplesCount >= MAX_RIPPLES) { // This should never happen unless the user is tapping like a maniac // or there is a bug that's preventing ripples from being removed. return; } if (mRipple == null) { final float x; final float y; if (mHasPending) { mHasPending = false; x = mPendingX; y = mPendingY; } else { x = mHotspotBounds.exactCenterX(); y = mHotspotBounds.exactCenterY(); } final boolean isBounded = isBounded(); mRipple = new RippleForeground(this, mHotspotBounds, x, y, isBounded); } mRipple.setup(mState.mMaxRadius, mDensity); mRipple.enter(false); } /** * Attempts to start an exit animation for the active hotspot. Fails if * there is no active hotspot. */ private void tryRippleExit() { if (mRipple != null) { if (mExitingRipples == null) { mExitingRipples = new RippleForeground[MAX_RIPPLES]; } mExitingRipples[mExitingRipplesCount++] = mRipple; mRipple.exit(); mRipple = null; } } /** * Cancels and removes the active ripple, all exiting ripples, and the * background. Nothing will be drawn after this method is called. */ private void clearHotspots() { if (mRipple != null) { mRipple.end(); mRipple = null; mRippleActive = false; } if (mBackground != null) { mBackground.end(); mBackground = null; mBackgroundActive = false; } cancelExitingRipples(); } @Override public void setHotspotBounds(int left, int top, int right, int bottom) { mOverrideBounds = true; mHotspotBounds.set(left, top, right, bottom); onHotspotBoundsChanged(); } @Override public void getHotspotBounds(Rect outRect) { outRect.set(mHotspotBounds); } /** * Notifies all the animating ripples that the hotspot bounds have changed. */ private void onHotspotBoundsChanged() { final int count = mExitingRipplesCount; final RippleForeground[] ripples = mExitingRipples; for (int i = 0; i < count; i++) { ripples[i].onHotspotBoundsChanged(); } if (mRipple != null) { mRipple.onHotspotBoundsChanged(); } if (mBackground != null) { mBackground.onHotspotBoundsChanged(); } } /** * Populates <code>outline</code> with the first available layer outline, * excluding the mask layer. * * @param outline Outline in which to place the first available layer outline */ @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) public void getOutline(@NonNull Outline outline) { final LayerState state = mLayerState; final ChildDrawable[] children = state.mChildren; final int N = state.mNum; for (int i = 0; i < N; i++) { if (children[i].mId != R.id.carbon_mask) { children[i].mDrawable.getOutline(outline); if (!outline.isEmpty()) return; } } } /** * Optimized for drawing ripples with a mask layer and optional content. */ @Override public void draw(@NonNull Canvas canvas) { pruneRipples(); // Clip to the dirty bounds, which will be the drawable bounds if we // have a mask or content and the ripple bounds if we're projecting. final Rect bounds = getDirtyBounds(); final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(bounds); drawContent(canvas); drawBackgroundAndRipples(canvas); canvas.restoreToCount(saveCount); } @Override public void invalidateSelf() { invalidateSelf(true); } void invalidateSelf(boolean invalidateMask) { super.invalidateSelf(); if (invalidateMask) { // Force the mask to update on the next draw(). mHasValidMask = false; } } private void pruneRipples() { int remaining = 0; // Move remaining entries into pruned spaces. final RippleForeground[] ripples = mExitingRipples; final int count = mExitingRipplesCount; for (int i = 0; i < count; i++) { if (!ripples[i].hasFinishedExit()) { ripples[remaining++] = ripples[i]; } } // Null out the remaining entries. for (int i = remaining; i < count; i++) { ripples[i] = null; } mExitingRipplesCount = remaining; } /** * @return whether we need to use a mask */ private void updateMaskShaderIfNeeded() { if (mHasValidMask) { return; } final int maskType = getMaskType(); if (maskType == MASK_UNKNOWN) { return; } mHasValidMask = true; final Rect bounds = getBounds(); if (maskType == MASK_NONE || bounds.isEmpty()) { if (mMaskBuffer != null) { mMaskBuffer.recycle(); mMaskBuffer = null; mMaskShader = null; mMaskCanvas = null; } mMaskMatrix = null; mMaskColorFilter = null; return; } // Ensure we have a correctly-sized buffer. if (mMaskBuffer == null || mMaskBuffer.getWidth() != bounds.width() || mMaskBuffer.getHeight() != bounds.height()) { if (mMaskBuffer != null) { mMaskBuffer.recycle(); } mMaskBuffer = Bitmap.createBitmap( bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8); mMaskShader = new BitmapShader(mMaskBuffer, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mMaskCanvas = new Canvas(mMaskBuffer); } else { mMaskBuffer.eraseColor(Color.TRANSPARENT); } if (mMaskMatrix == null) { mMaskMatrix = new Matrix(); } else { mMaskMatrix.reset(); } if (mMaskColorFilter == null) { mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN); } // Draw the appropriate mask anchored to (0,0). final int left = bounds.left; final int top = bounds.top; mMaskCanvas.translate(-left, -top); if (maskType == MASK_EXPLICIT) { drawMask(mMaskCanvas); } else if (maskType == MASK_CONTENT) { drawContent(mMaskCanvas); } mMaskCanvas.translate(left, top); } private int getMaskType() { if (mRipple == null && mExitingRipplesCount <= 0 && (mBackground == null || !mBackground.isVisible())) { // We might need a mask later. return MASK_UNKNOWN; } if (mMask != null) { if (mMask.getOpacity() == PixelFormat.OPAQUE) { // Clipping handles opaque explicit masks. return MASK_NONE; } else { return MASK_EXPLICIT; } } // Check for non-opaque, non-mask content. final ChildDrawable[] array = mLayerState.mChildren; final int count = mLayerState.mNum; for (int i = 0; i < count; i++) { if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) { return MASK_CONTENT; } } // Clipping handles opaque content. return MASK_NONE; } private void drawContent(Canvas canvas) { // Draw everything except the mask. final ChildDrawable[] array = mLayerState.mChildren; final int count = mLayerState.mNum; for (int i = 0; i < count; i++) { if (array[i].mId != R.id.carbon_mask) { array[i].mDrawable.draw(canvas); } } } private void drawBackgroundAndRipples(Canvas canvas) { final RippleForeground active = mRipple; final RippleBackground background = mBackground; final int count = mExitingRipplesCount; if (active == null && count <= 0 && (background == null || !background.isVisible())) { // Move along, nothing to draw here. return; } final float x = mHotspotBounds.exactCenterX(); final float y = mHotspotBounds.exactCenterY(); canvas.translate(x, y); updateMaskShaderIfNeeded(); // Position the shader to account for canvas translation. if (mMaskShader != null) { final Rect bounds = getBounds(); mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y); mMaskShader.setLocalMatrix(mMaskMatrix); } // Grab the color for the current state and cut the alpha channel in // half so that the ripple and background together yield full alpha. final int color = mState.mColor.getColorForState(getState(), Color.BLACK); final int halfAlpha = (Color.alpha(color) / 2) << 24; final Paint p = getRipplePaint(); if (mMaskColorFilter != null) { // The ripple timing depends on the paint's alpha value, so we need // to push just the alpha channel into the paint and let the filter // handle the full-alpha color. final int fullAlphaColor = color | (0xFF << 24); DrawableReflectiveUtils.setColor(mMaskColorFilter, fullAlphaColor, PorterDuff.Mode.SRC_IN); //mMaskColorFilter.setColor(fullAlphaColor); p.setColor(halfAlpha); p.setColorFilter(mMaskColorFilter); p.setShader(mMaskShader); } else { final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha; p.setColor(halfAlphaColor); p.setColorFilter(null); p.setShader(null); } if (background != null && background.isVisible()) { background.draw(canvas, p); } if (count > 0) { final RippleForeground[] ripples = mExitingRipples; for (int i = 0; i < count; i++) { ripples[i].draw(canvas, p); } } if (active != null) { active.draw(canvas, p); } canvas.translate(-x, -y); } private void drawMask(Canvas canvas) { mMask.draw(canvas); } private Paint getRipplePaint() { if (mRipplePaint == null) { mRipplePaint = new Paint(); mRipplePaint.setAntiAlias(true); mRipplePaint.setStyle(Paint.Style.FILL); } return mRipplePaint; } @Override public Rect getDirtyBounds() { if (!isBounded()) { final Rect drawingBounds = mDrawingBounds; final Rect dirtyBounds = mDirtyBounds; dirtyBounds.set(drawingBounds); drawingBounds.setEmpty(); final int cX = (int) mHotspotBounds.exactCenterX(); final int cY = (int) mHotspotBounds.exactCenterY(); final Rect rippleBounds = mTempRect; final RippleForeground[] activeRipples = mExitingRipples; final int N = mExitingRipplesCount; for (int i = 0; i < N; i++) { activeRipples[i].getBounds(rippleBounds); rippleBounds.offset(cX, cY); drawingBounds.union(rippleBounds); } final RippleBackground background = mBackground; if (background != null) { background.getBounds(rippleBounds); rippleBounds.offset(cX, cY); drawingBounds.union(rippleBounds); } dirtyBounds.union(drawingBounds); if (Build.VERSION.SDK_INT >= 21) dirtyBounds.union(super.getDirtyBounds()); return dirtyBounds; } else { return getBounds(); } } @Override public ConstantState getConstantState() { return mState; } @Override public Drawable mutate() { super.mutate(); // LayerDrawable creates a new state using createConstantState, so // this should always be a safe cast. mState = (RippleState) mLayerState; // The locally cached drawable may have changed. mMask = findDrawableByLayerId(R.id.carbon_mask); return this; } @Override RippleState createConstantState(LayerState state, Resources res) { return new RippleState(state, this, res); } static class RippleState extends LayerState { int[] mTouchThemeAttrs; ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); int mMaxRadius = RADIUS_AUTO; public RippleState(LayerState orig, RippleDrawableICS owner, Resources res) { super(orig, owner, res); if (orig != null && orig instanceof RippleState) { final RippleState origs = (RippleState) orig; mTouchThemeAttrs = origs.mTouchThemeAttrs; mColor = origs.mColor; mMaxRadius = origs.mMaxRadius; } } /* @Override public boolean canApplyTheme() { return mTouchThemeAttrs != null || (mColor != null && mColor.canApplyTheme()) || super.canApplyTheme(); }*/ @Override public Drawable newDrawable() { return new RippleDrawableICS(this, null); } @Override public Drawable newDrawable(Resources res) { return new RippleDrawableICS(this, res); } /* @Override public int getChangingConfigurations() { return super.getChangingConfigurations() | (mColor != null ? mColor.getChangingConfigurations() : 0); }*/ } private RippleDrawableICS(RippleState state, Resources res) { mState = new RippleState(state, this, res); mLayerState = mState; if (mState.mNum > 0) { ensurePadding(); refreshPadding(); } if (res != null) { mDensity = res.getDisplayMetrics().density; } updateLocalState(); } private void updateLocalState() { // Initialize from constant state. mMask = findDrawableByLayerId(R.id.carbon_mask); } @Override public Drawable getBackground() { return background; } @Override public Style getStyle() { return style; } @Override public boolean isHotspotEnabled() { return useHotspot; } @Override public void setHotspotEnabled(boolean useHotspot) { this.useHotspot = useHotspot; } @Override public ColorStateList getColor() { return mState.mColor; } @Override public int getAlpha() { return Carbon.getDrawableAlpha(background); } }