blob: e5e4d4527fca18b6fe2f67fc9163e8fc47643d9d [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.graphics.drawable;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ActivityInfo.Config;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
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.util.AttributeSet;
import com.android.internal.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.Arrays;
/**
* 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(..., android.R.id.mask)} or an existing mask layer
* may be replaced using {@code setDrawableByLayerId(android.R.id.mask, ...)}.
* <pre>
* <code>&lt;!-- A red ripple masked against an opaque rectangle. --/>
* &lt;ripple android:color="#ffff0000">
* &lt;item android:id="@android:id/mask"
* android:drawable="@android:color/white" />
* &lt;/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>&lt;!-- A green ripple drawn atop a black rectangle. --/>
* &lt;ripple android:color="#ff00ff00">
* &lt;item android:drawable="@android:color/black" />
* &lt;/ripple>
*
* &lt;!-- A blue ripple drawn atop a drawable resource. --/>
* &lt;ripple android:color="#ff0000ff">
* &lt;item android:drawable="@drawable/my_drawable" />
* &lt;/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>&lt;!-- An unbounded red ripple. --/>
* &lt;ripple android:color="#ffff0000" /></code>
* </pre>
*
* @attr ref android.R.styleable#RippleDrawable_color
*/
public class RippleDrawable extends LayerDrawable {
/**
* 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. */
@UnsupportedAppUsage
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;
/** 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. */
@UnsupportedAppUsage
private int mDensity;
/** Whether bounds are being overridden. */
private boolean mOverrideBounds;
/**
* If set, force all ripple animations to not run on RenderThread, even if it would be
* available.
*/
private boolean mForceSoftware;
/**
* Constructor used for drawable inflation.
*/
RippleDrawable() {
this(new RippleState(null, null, null), null);
}
/**
* 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 RippleDrawable(@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, android.R.id.mask, 0, 0, 0, 0);
}
setColor(color);
ensurePadding();
refreshPadding();
updateLocalState();
}
@Override
public void jumpToCurrentState() {
super.jumpToCurrentState();
if (mRipple != null) {
mRipple.end();
}
if (mBackground != null) {
mBackground.jumpToFinal();
}
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;
boolean hovered = false;
for (int state : stateSet) {
if (state == R.attr.state_enabled) {
enabled = true;
} else if (state == R.attr.state_focused) {
focused = true;
} else if (state == R.attr.state_pressed) {
pressed = true;
} else if (state == R.attr.state_hovered) {
hovered = true;
}
}
setRippleActive(enabled && pressed);
setBackgroundActive(hovered, focused, pressed);
return changed;
}
private void setRippleActive(boolean active) {
if (mRippleActive != active) {
mRippleActive = active;
if (active) {
tryRippleEnter();
} else {
tryRippleExit();
}
}
}
private void setBackgroundActive(boolean hovered, boolean focused, boolean pressed) {
if (mBackground == null && (hovered || focused)) {
mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
mBackground.setup(mState.mMaxRadius, mDensity);
}
if (mBackground != null) {
mBackground.setState(focused, hovered, pressed);
}
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
if (!mOverrideBounds) {
mHotspotBounds.set(bounds);
onHotspotBoundsChanged();
}
final int count = mExitingRipplesCount;
final RippleForeground[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].onBoundsChange();
}
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();
}
// 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;
}
// 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;
}
return true;
}
private boolean isBounded() {
return getNumberOfLayers() > 0;
}
@Override
public boolean isStateful() {
return true;
}
/** @hide */
@Override
public boolean hasFocusStateSpecified() {
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(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
// Force padding default to STACK before inflating.
setPaddingMode(PADDING_MODE_STACK);
// Inflation will advance the XmlPullParser and AttributeSet.
super.inflate(r, parser, attrs, theme);
updateStateFromTypedArray(a);
verifyRequiredAttributes(a);
a.recycle();
updateLocalState();
}
@Override
public boolean setDrawableByLayerId(int id, Drawable drawable) {
if (super.setDrawableByLayerId(id, drawable)) {
if (id == R.id.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(@NonNull TypedArray a) throws XmlPullParserException {
final RippleState state = mState;
// Account for any configuration changes.
state.mChangingConfigurations |= a.getChangingConfigurations();
// Extract the theme attributes, if any.
state.mTouchThemeAttrs = a.extractThemeAttrs();
final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
if (color != null) {
mState.mColor = color;
}
mState.mMaxRadius = a.getDimensionPixelSize(
R.styleable.RippleDrawable_radius, mState.mMaxRadius);
}
private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
if (mState.mColor == null && (mState.mTouchThemeAttrs == null
|| mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
throw new XmlPullParserException(a.getPositionDescription() +
": <ripple> requires a valid color attribute");
}
}
@Override
public void applyTheme(@NonNull 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);
verifyRequiredAttributes(a);
} catch (XmlPullParserException e) {
rethrowAsRuntimeException(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);
}
}
/**
* 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();
}
mRipple = new RippleForeground(this, mHotspotBounds, x, y, mForceSoftware);
}
mRipple.setup(mState.mMaxRadius, mDensity);
mRipple.enter();
}
/**
* 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.setState(false, false, 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
public void getOutline(@NonNull Outline outline) {
final LayerState state = mLayerState;
final ChildDrawable[] children = state.mChildren;
final int N = state.mNumChildren;
for (int i = 0; i < N; i++) {
if (children[i].mId != R.id.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);
if (isBounded()) {
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.mNumChildren;
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.mNumChildren;
for (int i = 0; i < count; i++) {
if (array[i].mId != R.id.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);
final Paint p = getRipplePaint();
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);
}
@UnsupportedAppUsage
Paint getRipplePaint() {
if (mRipplePaint == null) {
mRipplePaint = new Paint();
mRipplePaint.setAntiAlias(true);
mRipplePaint.setStyle(Paint.Style.FILL);
}
final float x = mHotspotBounds.exactCenterX();
final float y = mHotspotBounds.exactCenterY();
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.
int color = mState.mColor.getColorForState(getState(), Color.BLACK);
if (Color.alpha(color) > 128) {
color = (color & 0x00FFFFFF) | 0x80000000;
}
final Paint p = mRipplePaint;
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.
int maskColor = color | 0xFF000000;
if (mMaskColorFilter.getColor() != maskColor) {
mMaskColorFilter = new PorterDuffColorFilter(maskColor, mMaskColorFilter.getMode());
}
p.setColor(color & 0xFF000000);
p.setColorFilter(mMaskColorFilter);
p.setShader(mMaskShader);
} else {
p.setColor(color);
p.setColorFilter(null);
p.setShader(null);
}
return p;
}
@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);
dirtyBounds.union(super.getDirtyBounds());
return dirtyBounds;
} else {
return getBounds();
}
}
/**
* Sets whether to disable RenderThread animations for this ripple.
*
* @param forceSoftware true if RenderThread animations should be disabled, false otherwise
* @hide
*/
@UnsupportedAppUsage
public void setForceSoftware(boolean forceSoftware) {
mForceSoftware = forceSoftware;
}
@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.mask);
return this;
}
@Override
RippleState createConstantState(LayerState state, Resources res) {
return new RippleState(state, this, res);
}
static class RippleState extends LayerState {
int[] mTouchThemeAttrs;
@UnsupportedAppUsage
ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
int mMaxRadius = RADIUS_AUTO;
public RippleState(LayerState orig, RippleDrawable 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;
if (origs.mDensity != mDensity) {
applyDensityScaling(orig.mDensity, mDensity);
}
}
}
@Override
protected void onDensityChanged(int sourceDensity, int targetDensity) {
super.onDensityChanged(sourceDensity, targetDensity);
applyDensityScaling(sourceDensity, targetDensity);
}
private void applyDensityScaling(int sourceDensity, int targetDensity) {
if (mMaxRadius != RADIUS_AUTO) {
mMaxRadius = Drawable.scaleFromDensity(
mMaxRadius, sourceDensity, targetDensity, true);
}
}
@Override
public boolean canApplyTheme() {
return mTouchThemeAttrs != null
|| (mColor != null && mColor.canApplyTheme())
|| super.canApplyTheme();
}
@Override
public Drawable newDrawable() {
return new RippleDrawable(this, null);
}
@Override
public Drawable newDrawable(Resources res) {
return new RippleDrawable(this, res);
}
@Override
public @Config int getChangingConfigurations() {
return super.getChangingConfigurations()
| (mColor != null ? mColor.getChangingConfigurations() : 0);
}
}
private RippleDrawable(RippleState state, Resources res) {
mState = new RippleState(state, this, res);
mLayerState = mState;
mDensity = Drawable.resolveDensity(res, mState.mDensity);
if (mState.mNumChildren > 0) {
ensurePadding();
refreshPadding();
}
updateLocalState();
}
private void updateLocalState() {
// Initialize from constant state.
mMask = findDrawableByLayerId(R.id.mask);
}
}