package org.lab99.mdt.drawable;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
/**
* Ripple Drawable of Material Design Ripple effect
*/
class RippleDrawable extends Drawable {
private final static int DEFAULT_MIN_RADIUS = 30;
private float mMinRadius;
private float mMaxRadius;
private Bitmap mBitmapRipple;
private Bitmap mBitmapBackground;
private RippleState mState;
private float mRadius;
public RippleDrawable() {
this(null);
setRippleEnabled(false);
setOverlayEnabled(false);
mState.mOnStateChangedListener = new RippleStateChanger(this);
mMinRadius = DEFAULT_MIN_RADIUS;
}
RippleDrawable(RippleState state) {
mState = new RippleState(state);
}
public float getRippleProgress() {
return mState.mRippleProgress;
}
public void setRippleProgress(float progress) {
//System.out.println("setRippleProgress(" + progress + ")");
mState.mRippleProgress = progress;
mRadius = calculateRadius();
// Call 'invalidateSelf()' will trigger 'View.invalidate()', so this will redrawn later.
invalidateSelf();
}
private float calculateRadius() {
return mState.mRippleProgress * (mMaxRadius - mMinRadius) + mMinRadius;
}
public boolean isEnabled() {
return mState.mEnabled;
}
public void setEnabled(boolean enabled) {
mState.mEnabled = enabled;
invalidateSelf();
}
public float getOverlayAlpha() {
return mState.mOverlayAlpha;
}
public void setOverlayAlpha(float alpha) {
//System.out.println("setOverlayAlpha(" + alpha + ")");
mState.mOverlayAlpha = alpha;
invalidateSelf();
}
public OnStateChangedListener getOnStateChangedListener() {
return mState.mOnStateChangedListener;
}
public void setOnStateChangedListener(OnStateChangedListener listener) {
mState.mOnStateChangedListener = listener;
}
public PointF getRippleCenter() {
return mState.mRippleCenter;
}
public RippleDrawable setRippleCenter(float x, float y) {
mState.mRippleCenter.set(x, y);
mMaxRadius = calculateMaxRadius();
invalidateSelf();
return this;
}
public int getRippleColor() {
return mState.mRippleColor;
}
public RippleDrawable setRippleColor(int color) {
mState.mRippleColor = color;
invalidateSelf();
return this;
}
public int getOverlayColor() {
return mState.mOverlayColor;
}
public RippleDrawable setOverlayColor(int color) {
mState.mOverlayColor = color;
mState.mOverlayAlpha = 1;
invalidateSelf();
return this;
}
boolean isRippleEnabled() {
return mState.mRippleEnabled;
}
RippleDrawable setRippleEnabled(boolean value) {
mState.mRippleEnabled = value;
invalidateSelf();
return this;
}
boolean isOverlayEnabled() {
return mState.mOverlayEnabled;
}
RippleDrawable setOverlayEnabled(boolean value) {
mState.mOverlayEnabled = value;
invalidateSelf();
return this;
}
/**
* @param alpha Range is [0,1)
* @return
*/
public RippleDrawable setAlpha(float alpha) {
mState.mAlpha = alpha;
invalidateSelf();
return this;
}
public Drawer getMaskDrawer() {
return mState.mMaskDrawer;
}
public void setMaskDrawer(Drawer drawer) {
mState.mMaskDrawer = drawer;
}
public TouchTracker getTouchTracker() {
return mState.mTouchTracker;
}
public void setTouchTracker(TouchTracker tracker) {
mState.mTouchTracker = tracker;
}
@Override
public void draw(Canvas canvas) {
if (isEnabled() && !getBounds().isEmpty()) {
if (getMaskDrawer() != null && mRadius >= DEFAULT_MIN_RADIUS) {
// Clear bitmap
mBitmapBackground.eraseColor(Color.TRANSPARENT);
mBitmapRipple.eraseColor(Color.TRANSPARENT);
// Because we need PorterDuff.Mode.SRC_ATOP later, the canvas has to be empty
// So, we cannot use given canvas, which contains other stuff already.
Canvas c = new Canvas(mBitmapBackground);
// draw background
getMaskDrawer().draw(c);
// Render Ripple Layer
// Ripple has to be drawn on a Bitmap layer, otherwise later Mode.SRC_ATOP will be wrong
Canvas cr = new Canvas(mBitmapRipple);
// render ripple
drawRipple(cr);
// draw ripple layer on background
c.drawBitmap(mBitmapRipple, 0, 0, mState.mOverlayPaint);
// draw everything on given canvas
canvas.drawBitmap(mBitmapBackground, 0, 0, null);
} else {
// draw ripple directly
drawRipple(canvas);
}
}
}
@Override
public void setColorFilter(ColorFilter cf) {
mState.mOverlayPaint.setColorFilter(cf);
mState.mRipplePaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.UNKNOWN;
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
if (!bounds.isEmpty()) {
createBitmaps();
}
setRippleCenter(bounds.width() / 2, bounds.height() / 2);
mMaxRadius = calculateMaxRadius();
}
@Override
public void setAlpha(int alpha) {
mState.mAlpha = alpha / 255f;
}
@Override
public boolean isStateful() {
return true;
}
@Override
public boolean setState(int[] stateSet) {
boolean changed = false;
if (getOnStateChangedListener() != null) {
changed = getOnStateChangedListener().onStateChange(getState(), stateSet);
}
return super.setState(stateSet) || changed;
}
@Override
public ConstantState getConstantState() {
return mState;
}
private float calculateMaxRadius() {
float w = getBounds().width();
float h = getBounds().height();
PointF c = mState.mRippleCenter;
if (c.x == 0 && c.y == 0) {
return (float) Math.sqrt(w * w + h * h);
} else {
float l1 = PointF.length(c.x, c.y);
float l2 = PointF.length(c.x, h - c.y);
float l3 = PointF.length(w - c.x, c.y);
float l4 = PointF.length(w - c.x, h - c.y);
return Math.max(Math.max(l1, l2), Math.max(l3, l4));
}
}
private void createBitmaps() {
Rect bound = getBounds();
mBitmapRipple = Bitmap.createBitmap(bound.width(), bound.height(), Bitmap.Config.ARGB_8888);
mBitmapBackground = Bitmap.createBitmap(bound.width(), bound.height(), Bitmap.Config.ARGB_8888);
}
private void drawRipple(Canvas canvas) {
// set clip
canvas.save();
canvas.clipRect(getBounds());
// draw overlay layer
if (mState.mOverlayEnabled) {
// calculate the correct overlay color with current alpha
int c = Color.argb(
(int) (Color.alpha(mState.mOverlayColor) * mState.mOverlayAlpha * mState.mAlpha),
Color.red(mState.mOverlayColor),
Color.green(mState.mOverlayColor),
Color.blue(mState.mOverlayColor)
);
canvas.drawColor(c);
}
// draw ripple layer
if (mState.mRippleEnabled) {
mState.mRipplePaint.setColor(mState.mRippleColor);
mState.mRipplePaint.setAlpha((int) (Color.alpha(mState.mRippleColor) * mState.mAlpha));
canvas.drawCircle(mState.mRippleCenter.x, mState.mRippleCenter.y, mRadius, mState.mRipplePaint);
}
// restore setting
canvas.restore();
}
static class RippleState extends ConstantState {
// General
boolean mEnabled;
float mAlpha;
// Ripple
boolean mRippleEnabled;
PointF mRippleCenter;
int mRippleColor;
float mRippleProgress;
Paint mRipplePaint;
// Overlay
boolean mOverlayEnabled;
float mOverlayAlpha;
int mOverlayColor;
Paint mOverlayPaint;
// Handler
Drawer mMaskDrawer;
TouchTracker mTouchTracker;
OnStateChangedListener mOnStateChangedListener;
RippleState(RippleState orig) {
if (orig != null) {
initWithState(orig);
} else {
initWithoutState();
}
}
private void initWithState(RippleState orig) {
// General
mEnabled = orig.mEnabled;
mAlpha = orig.mAlpha;
// Ripple
mRippleEnabled = orig.mRippleEnabled;
mRippleCenter = new PointF(orig.mRippleCenter.x, orig.mRippleCenter.y);
mRippleColor = orig.mRippleColor;
mRippleProgress = orig.mRippleProgress;
mRipplePaint = new Paint(orig.mRipplePaint);
// Overlay
mOverlayEnabled = orig.mOverlayEnabled;
mOverlayAlpha = orig.mOverlayAlpha;
mOverlayColor = orig.mOverlayColor;
mOverlayPaint = new Paint(orig.mOverlayPaint);
// Handler
mMaskDrawer = orig.mMaskDrawer;
mTouchTracker = orig.mTouchTracker;
mOnStateChangedListener = orig.mOnStateChangedListener;
}
private void initWithoutState() {
// General
mEnabled = true;
mAlpha = 1;
// Ripple
mRippleEnabled = true;
mRippleCenter = new PointF();
mRippleColor = 0x40cccccc; // mdt_button_pressed_dark
mRippleProgress = 0;
mRipplePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// Overlay
mOverlayEnabled = true;
mOverlayAlpha = 0;
mOverlayColor = 0x26cccccc; // mdt_button_hover_dark
mOverlayPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mOverlayPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
}
@Override
public Drawable newDrawable() {
return new RippleDrawable(this);
}
@Override
public int getChangingConfigurations() {
return 0;
}
}
}