Fields Summary |
---|
private static final int | MASK_UNKNOWN |
private static final int | MASK_NONE |
private static final int | MASK_CONTENT |
private static final int | MASK_EXPLICIT |
public static final int | RADIUS_AUTOConstant for automatically determining the maximum ripple radius. |
private static final int | MAX_RIPPLESThe maximum number of ripples supported. |
private final android.graphics.Rect | mTempRect |
private final android.graphics.Rect | mHotspotBoundsCurrent ripple effect bounds, used to constrain ripple effects. |
private final android.graphics.Rect | mDrawingBoundsCurrent drawing bounds, used to compute dirty region. |
private final android.graphics.Rect | mDirtyBoundsCurrent dirty bounds, union of current and previous drawing bounds. |
private RippleState | mStateMirrors mLayerState with some extra information. |
private Drawable | mMaskThe masking layer, e.g. the layer with id R.id.mask. |
private RippleBackground | mBackgroundThe current background. May be actively animating or pending entry. |
private android.graphics.Bitmap | mMaskBuffer |
private android.graphics.BitmapShader | mMaskShader |
private android.graphics.Canvas | mMaskCanvas |
private android.graphics.Matrix | mMaskMatrix |
private android.graphics.PorterDuffColorFilter | mMaskColorFilter |
private boolean | mHasValidMask |
private boolean | mBackgroundActiveWhether we expect to draw a background when visible. |
private Ripple | mRippleThe current ripple. May be actively animating or pending entry. |
private boolean | mRippleActiveWhether we expect to draw a ripple when visible. |
private float | mPendingX |
private float | mPendingY |
private boolean | mHasPending |
private Ripple[] | mExitingRipplesLazily-created array of actively animating ripples. Inactive ripples are
pruned during draw(). The locations of these will not change. |
private int | mExitingRipplesCount |
private android.graphics.Paint | mRipplePaintPaint used to control appearance of ripples. |
private float | mDensityTarget density of the display into which ripples are drawn. |
private boolean | mOverrideBoundsWhether bounds are being overridden. |
Methods Summary |
---|
public void | applyTheme(android.content.res.Resources.Theme t)
super.applyTheme(t);
final RippleState state = mState;
if (state == null || state.mTouchThemeAttrs == null) {
return;
}
final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
R.styleable.RippleDrawable);
try {
updateStateFromTypedArray(a);
} catch (XmlPullParserException e) {
throw new RuntimeException(e);
} finally {
a.recycle();
}
initializeFromState();
|
public boolean | canApplyTheme()
return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
|
private boolean | cancelExitingRipples()
boolean needsDraw = false;
final int count = mExitingRipplesCount;
final Ripple[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
needsDraw |= ripples[i].isHardwareAnimating();
ripples[i].cancel();
}
if (ripples != null) {
Arrays.fill(ripples, 0, count, null);
}
mExitingRipplesCount = 0;
return needsDraw;
|
private void | clearHotspots()Cancels and removes the active ripple, all exiting ripples, and the
background. Nothing will be drawn after this method is called.
if (mRipple != null) {
mRipple.cancel();
mRipple = null;
mRippleActive = false;
}
if (mBackground != null) {
mBackground.cancel();
mBackground = null;
mBackgroundActive = false;
}
cancelExitingRipples();
invalidateSelf();
|
android.graphics.drawable.RippleDrawable$RippleState | createConstantState(LayerState state, android.content.res.Resources res)
return new RippleState(state, this, res);
|
public void | draw(android.graphics.Canvas canvas)Optimized for drawing ripples with a mask layer and optional content.
// 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);
|
private void | drawBackgroundAndRipples(android.graphics.Canvas canvas)
final Ripple active = mRipple;
final RippleBackground background = mBackground;
final int count = mExitingRipplesCount;
if (active == null && count <= 0 && (background == null || !background.shouldDraw())) {
// 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) {
mMaskMatrix.setTranslate(-x, -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);
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.shouldDraw()) {
background.draw(canvas, p);
}
if (count > 0) {
final Ripple[] 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 | drawContent(android.graphics.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.mask) {
array[i].mDrawable.draw(canvas);
}
}
|
private void | drawMask(android.graphics.Canvas canvas)
mMask.draw(canvas);
|
public ConstantState | getConstantState()
return mState;
|
public android.graphics.Rect | getDirtyBounds()
if (isProjected()) {
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 Ripple[] 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();
}
|
public void | getHotspotBounds(android.graphics.Rect outRect)
outRect.set(mHotspotBounds);
|
private int | getMaskType()
if (mRipple == null && mExitingRipplesCount <= 0
&& (mBackground == null || !mBackground.shouldDraw())) {
// 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;
|
public int | getMaxRadius()
return mState.mMaxRadius;
|
public int | getOpacity()
// Worst-case scenario.
return PixelFormat.TRANSLUCENT;
|
public void | getOutline(android.graphics.Outline outline)Populates outline with the first available layer outline,
excluding the mask layer.
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.mask) {
children[i].mDrawable.getOutline(outline);
if (!outline.isEmpty()) return;
}
}
|
private int | getRippleIndex(Ripple ripple)
final Ripple[] ripples = mExitingRipples;
final int count = mExitingRipplesCount;
for (int i = 0; i < count; i++) {
if (ripples[i] == ripple) {
return i;
}
}
return -1;
|
private android.graphics.Paint | getRipplePaint()
if (mRipplePaint == null) {
mRipplePaint = new Paint();
mRipplePaint.setAntiAlias(true);
mRipplePaint.setStyle(Paint.Style.FILL);
}
return mRipplePaint;
|
public void | inflate(android.content.res.Resources r, org.xmlpull.v1.XmlPullParser parser, android.util.AttributeSet attrs, android.content.res.Resources.Theme theme)
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());
initializeFromState();
|
private void | initializeFromState()
// Initialize from constant state.
mMask = findDrawableByLayerId(R.id.mask);
|
public void | invalidateSelf()
super.invalidateSelf();
// Force the mask to update on the next draw().
mHasValidMask = false;
|
public boolean | isProjected()
return getNumberOfLayers() == 0;
|
public boolean | isStateful()
return true;
|
public void | jumpToCurrentState()
super.jumpToCurrentState();
if (mRipple != null) {
mRipple.jump();
}
if (mBackground != null) {
mBackground.jump();
}
cancelExitingRipples();
invalidateSelf();
|
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;
|
protected void | onBoundsChange(android.graphics.Rect bounds)
super.onBoundsChange(bounds);
if (!mOverrideBounds) {
mHotspotBounds.set(bounds);
onHotspotBoundsChanged();
}
invalidateSelf();
|
private void | onHotspotBoundsChanged()Notifies all the animating ripples that the hotspot bounds have changed.
final int count = mExitingRipplesCount;
final Ripple[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].onHotspotBoundsChanged();
}
if (mRipple != null) {
mRipple.onHotspotBoundsChanged();
}
if (mBackground != null) {
mBackground.onHotspotBoundsChanged();
}
|
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 == R.attr.state_enabled) {
enabled = true;
}
if (state == R.attr.state_focused) {
focused = true;
}
if (state == R.attr.state_pressed) {
pressed = true;
}
}
setRippleActive(enabled && pressed);
setBackgroundActive(focused || (enabled && pressed), focused);
return changed;
|
void | removeRipple(Ripple ripple)Removes a ripple from the exiting ripple list.
// Ripple ripple ripple ripple. Ripple ripple.
final Ripple[] ripples = mExitingRipples;
final int count = mExitingRipplesCount;
final int index = getRippleIndex(ripple);
if (index >= 0) {
System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
ripples[count - 1] = null;
mExitingRipplesCount--;
invalidateSelf();
}
|
public void | setAlpha(int alpha)
super.setAlpha(alpha);
// TODO: Should we support this?
|
private void | setBackgroundActive(boolean active, boolean focused)
if (mBackgroundActive != active) {
mBackgroundActive = active;
if (active) {
tryBackgroundEnter(focused);
} else {
tryBackgroundExit();
}
}
|
public void | setColor(android.content.res.ColorStateList color)
mState.mColor = color;
invalidateSelf();
|
public void | setColorFilter(android.graphics.ColorFilter cf)
super.setColorFilter(cf);
// TODO: Should we support this?
|
public boolean | setDrawableByLayerId(int id, Drawable drawable)
if (super.setDrawableByLayerId(id, drawable)) {
if (id == R.id.mask) {
mMask = drawable;
}
return true;
}
return false;
|
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);
}
|
public void | setHotspotBounds(int left, int top, int right, int bottom)
mOverrideBounds = true;
mHotspotBounds.set(left, top, right, bottom);
onHotspotBoundsChanged();
|
public void | setMaxRadius(int maxRadius)Sets the maximum ripple radius in pixels. The default value of
{@link #RADIUS_AUTO} defines the radius as the distance from the center
of the drawable bounds (or hotspot bounds, if specified) to a corner.
if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
}
mState.mMaxRadius = maxRadius;
|
public void | setPaddingMode(int mode)Specifies how layer padding should affect the bounds of subsequent
layers. The default and recommended value for RippleDrawable is
{@link #PADDING_MODE_STACK}.
super.setPaddingMode(mode);
|
private void | setRippleActive(boolean active)
if (mRippleActive != active) {
mRippleActive = active;
if (active) {
tryRippleEnter();
} else {
tryRippleExit();
}
}
|
private void | setTargetDensity(android.util.DisplayMetrics metrics)Set the density at which this drawable will be rendered.
if (mDensity != metrics.density) {
mDensity = metrics.density;
invalidateSelf();
}
|
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;
|
private void | tryBackgroundEnter(boolean focused)Creates an active hotspot at the specified location.
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();
}
|
private void | tryRippleEnter()Attempts to start an enter animation for the active hotspot. Fails if
there are too many animating ripples.
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 Ripple(this, mHotspotBounds, x, y);
}
mRipple.setup(mState.mMaxRadius, mDensity);
mRipple.enter();
|
private void | tryRippleExit()Attempts to start an exit animation for the active hotspot. Fails if
there is no active hotspot.
if (mRipple != null) {
if (mExitingRipples == null) {
mExitingRipples = new Ripple[MAX_RIPPLES];
}
mExitingRipples[mExitingRipplesCount++] = mRipple;
mRipple.exit();
mRipple = null;
}
|
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.
if (maskType == MASK_EXPLICIT) {
drawMask(mMaskCanvas);
} else if (maskType == MASK_CONTENT) {
drawContent(mMaskCanvas);
}
|
private void | updateStateFromTypedArray(android.content.res.TypedArray a)Initializes the constant state from the values in the typed array.
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;
}
verifyRequiredAttributes(a);
|
private void | verifyRequiredAttributes(android.content.res.TypedArray a)
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");
}
|