package com.marshalchen.common.uimodule.rippleDrawable;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.View;
import java.util.Arrays;
public class RippleDrawable extends Drawable {
private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP);
private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER);
/**
* Constant for automatically determining the maximum ripple radius.
*
* @see #setMaxRadius(int)
* @hide
*/
public static final int RADIUS_AUTO = -1;
/** The maximum number of ripples supported. */
private static final int MAX_RIPPLES = 10;
/** Current ripple effect bounds, used to constrain ripple effects. */
private final Rect mHotspotBounds = new Rect();
ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
int mMaxRadius = RADIUS_AUTO;
private Drawable mContent;
/** 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;
/** Whether we expect to draw a background when visible. */
private boolean mBackgroundActive;
/** The current ripple. May be actively animating or pending entry. */
private Ripple 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 Ripple[] mExitingRipples;
private int mExitingRipplesCount = 0;
/** Paint used to control appearance of ripples. */
private Paint mRipplePaint;
/** Paint used to control reveal layer masking. */
private Paint mMaskingPaint;
/** Target density of the display into which ripples are drawn. */
private float mDensity = 1.0f;
/** Whether bounds are being overridden. */
private boolean mOverrideBounds;
/**
* Whether the next draw MUST draw something to canvas. Used to work around
* a bug in hardware invalidation following a render thread-accelerated
* animation.
*/
private boolean mNeedsDraw;
/**
* Creates a new ripple drawable with the specified ripple color and
* optional content and mask drawables.
*
* @param color The ripple color
*/
public RippleDrawable(ColorStateList color) {
setColor(color);
}
public RippleDrawable(ColorStateList color, Drawable content){
this(color);
mContent = content;
}
@Override
public void jumpToCurrentState() {
if(Build.VERSION.SDK_INT > 11) {
super.jumpToCurrentState();
}
boolean needsDraw;
if (mRipple != null) {
mRipple.jump();
}
if (mBackground != null) {
mBackground.jump();
}
needsDraw = cancelExitingRipples();
mNeedsDraw = needsDraw;
invalidateSelf();
}
private boolean cancelExitingRipples() {
final int count = mExitingRipplesCount;
final Ripple[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].cancel();
}
if (ripples != null) {
Arrays.fill(ripples, 0, count, null);
}
mExitingRipplesCount = 0;
return false;
}
@Override
public void setAlpha(int alpha) {
mColor.withAlpha(alpha);
}
@Override
public int getAlpha() {
return Color.alpha(mColor.getDefaultColor());
}
@Override
public void setColorFilter(ColorFilter cf) {
//TODO how to implement?
}
@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;
}
if (state == android.R.attr.state_focused) {
focused = true;
}
if (state == android.R.attr.state_pressed) {
pressed = true;
}
}
setRippleActive(enabled && pressed);
setBackgroundActive(focused || (enabled && pressed));
return changed;
}
private void setRippleActive(boolean active) {
if (mRippleActive != active) {
mRippleActive = active;
if (active) {
tryRippleEnter();
} else {
tryRippleExit();
}
}
}
private void setBackgroundActive(boolean active) {
if (mBackgroundActive != active) {
mBackgroundActive = active;
if (active) {
tryBackgroundEnter();
} else {
tryBackgroundExit();
}
}
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
if (!mOverrideBounds) {
mHotspotBounds.set(bounds);
onHotspotBoundsChanged();
}
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();
}
}
return changed;
}
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 Ripple(this, mHotspotBounds, x, y);
}
final int color = mColor.getColorForState(getState(), Color.TRANSPARENT);
mRipple.setup(mMaxRadius, color, 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 Ripple[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() {
boolean needsDraw = false;
if (mRipple != null) {
needsDraw = false;
mRipple.cancel();
mRipple = null;
}
if (mBackground != null) {
needsDraw = mBackground.isHardwareAnimating();
mBackground.cancel();
mBackground = null;
}
needsDraw |= cancelExitingRipples();
mNeedsDraw = needsDraw;
invalidateSelf();
}
public void setHotspotBounds(int left, int top, int right, int bottom) {
mOverrideBounds = true;
mHotspotBounds.set(left, top, right, bottom);
onHotspotBoundsChanged();
}
/** @hide */
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 Ripple[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].onHotspotBoundsChanged();
}
if (mRipple != null) {
mRipple.onHotspotBoundsChanged();
}
if (mBackground != null) {
mBackground.onHotspotBoundsChanged();
}
}
/**
* Creates an active hotspot at the specified location.
*/
private void tryBackgroundEnter() {
if (mBackground == null) {
mBackground = new RippleBackground(this, mHotspotBounds);
}
final int color = mColor.getColorForState(getState(), Color.TRANSPARENT);
mBackground.setup(mMaxRadius, color, mDensity);
mBackground.enter();
}
private void tryBackgroundExit() {
if (mBackground != null) {
// Don't null out the background, we need it to draw!
mBackground.exit();
}
}
@Override
public void draw(Canvas canvas) {
final boolean hasMask = mMask != null;
final boolean drawNonMaskContent = mContent != null;//TODO if contentDrawable is not null
final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE;
final Rect bounds = getDirtyBounds();
final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipRect(bounds);
// If we have content, draw it into a layer first.
if (drawNonMaskContent) {
drawContentLayer(canvas, bounds, SRC_OVER);
}
// Next, try to draw the ripples (into a layer if necessary). If we need
// to mask against the underlying content, set the xfermode to SRC_ATOP.
final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP;
// If we have a background and a non-opaque mask, draw the masking layer.
final int backgroundLayer = drawBackgroundLayer(canvas, bounds, xfermode, drawMask);
if (backgroundLayer >= 0) {
if (drawMask) {
drawMaskingLayer(canvas, bounds, DST_IN);
}
canvas.restoreToCount(backgroundLayer);
}
// If we have ripples and a non-opaque mask, draw the masking layer.
final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode);
if (rippleLayer >= 0) {
if (drawMask) {
drawMaskingLayer(canvas, bounds, DST_IN);
}
canvas.restoreToCount(rippleLayer);
}
// If we failed to draw anything and we just canceled animations, at
// least draw a color so that hardware invalidation works correctly.
if (mNeedsDraw) {
canvas.drawColor(Color.TRANSPARENT);
// Request another draw so we can avoid adding a transparent layer
// during the next display list refresh.
invalidateSelf();
}
mNeedsDraw = false;
canvas.restoreToCount(saveCount);
}
/**
* Removes a ripple from the exiting ripple list.
*
* @param ripple the ripple to remove
*/
void removeRipple(Ripple ripple) {
// 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();
}
}
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 int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
mContent.setBounds(bounds);
mContent.draw(canvas);
return -1;
}
private int drawBackgroundLayer(
Canvas canvas, Rect bounds, PorterDuffXfermode mode, boolean drawMask) {
int saveCount = -1;
if (mBackground != null && mBackground.shouldDraw()) {
// TODO: We can avoid saveLayer here if we push the xfermode into
// the background's render thread animator at exit() time.
if (drawMask || mode != SRC_OVER) {
//saveCount = canvas.saveLayer(bounds.left, bounds.top, bounds.right,
// bounds.bottom, getMaskingPaint(mode));
}
final float x = mHotspotBounds.exactCenterX();
final float y = mHotspotBounds.exactCenterY();
canvas.translate(x, y);
mBackground.draw(canvas, getRipplePaint());
canvas.translate(-x, -y);
}
return saveCount;
}
private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
boolean drewRipples = false;
int restoreToCount = -1;
int restoreTranslate = -1;
// Draw ripples and update the animating ripples array.
final int count = mExitingRipplesCount;
final Ripple[] ripples = mExitingRipples;
for (int i = 0; i <= count; i++) {
final Ripple ripple;
if (i < count) {
ripple = ripples[i];
} else if (mRipple != null) {
ripple = mRipple;
} else {
continue;
}
// If we're masking the ripple layer, make sure we have a layer
// first. This will merge SRC_OVER (directly) onto the canvas.
if (restoreToCount < 0) {
final Paint maskingPaint = getMaskingPaint(mode);
final int color = mColor.getColorForState(getState(), Color.TRANSPARENT);
final int alpha = Color.alpha(color);
maskingPaint.setAlpha(alpha / 2);
// Translate the canvas to the current hotspot bounds.
restoreTranslate = canvas.save();
canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
}
drewRipples |= ripple.draw(canvas, getRipplePaint());
}
// Always restore the translation.
if (restoreTranslate >= 0) {
canvas.restoreToCount(restoreTranslate);
}
// If we created a layer with no content, merge it immediately.
if (restoreToCount >= 0 && !drewRipples) {
canvas.restoreToCount(restoreToCount);
restoreToCount = -1;
}
return restoreToCount;
}
private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
// Ensure that DST_IN blends using the entire layer.
canvas.drawColor(Color.TRANSPARENT);
mMask.draw(canvas);
return -1;
}
private Paint getRipplePaint() {
if (mRipplePaint == null) {
mRipplePaint = new Paint();
mRipplePaint.setAntiAlias(true);
}
return mRipplePaint;
}
private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
if (mMaskingPaint == null) {
mMaskingPaint = new Paint();
}
mMaskingPaint.setXfermode(xfermode);
mMaskingPaint.setAlpha(0xFF);
return mMaskingPaint;
}
/**
* 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();
}
}
@Override
public boolean isStateful() {
return true;
}
public void setColor(ColorStateList color) {
mColor = color;
invalidateSelf();
}
@Override
public Rect getDirtyBounds() {
return getBounds();
}
/**
* 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.
*
* @param maxRadius the maximum ripple radius in pixels or
* {@link #RADIUS_AUTO} to automatically determine the maximum
* radius based on the bounds
* @see #getMaxRadius()
* @see #setHotspotBounds(int, int, int, int)
* @hide
*/
public void setMaxRadius(int maxRadius) {
if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
}
mMaxRadius = maxRadius;
}
/**
* @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
* the radius is determined automatically
* @see #setMaxRadius(int)
* @hide
*/
public int getMaxRadius() {
return mMaxRadius;
}
/**
* @deprecated
*/
public static RippleDrawable createRipple(View target, int color){
return For(target, color);
}
public static RippleDrawable For(View target, int color){
return makeFor(target, ColorStateList.valueOf(color));
}
public static RippleDrawable makeFor(View target, ColorStateList colors){
return makeFor(target, colors, false);
}
public static RippleDrawable makeFor(View target, ColorStateList colors, boolean parentIsScrollContainer){
RippleDrawable drawable = new RippleDrawable(colors, target.getBackground());
TouchTracker tracker = new TouchTracker(drawable);
tracker.setInsideScrollContainer(parentIsScrollContainer);
setBackground(target, drawable);
target.setOnTouchListener(tracker);
return drawable;
}
private static void setBackground(View target, Drawable drawable){
if(Build.VERSION.SDK_INT > 16){
target.setBackground(drawable);
}else{
target.setBackgroundDrawable(drawable);
}
}
}