package carbon.drawable;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.util.Log;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import carbon.R;
import carbon.animation.AnimatedColorStateList;
public class CheckableDrawable extends Drawable {
private float currRadius;
private int currAnim;
private ValueAnimator animator;
public enum CheckedState {
UNCHECKED, CHECKED, INDETERMINATE
}
private static final long CHECK_DURATION = 100;
private static final long FILL_DURATION = 100;
private static final int ANIMATION_FILL = 0;
private static final int ANIMATION_CHECK = 1;
private final Context context;
private final int checkedRes;
private final int uncheckedRes;
private final int filledRes;
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG), maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private PorterDuffXfermode porterDuffClear = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
private PorterDuffXfermode porterDuffSrcIn = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
private Bitmap checkedBitmap, uncheckedBitmap, filledBitmap, maskBitmap;
private Canvas maskCanvas;
private float radius;
private boolean enabled;
private CheckedState checkedState = CheckedState.UNCHECKED;
private PorterDuffColorFilter checkedFilter;
private PorterDuffColorFilter uncheckedFilter;
private BitmapShader checkedShader;
private PointF offset;
private ColorStateList tint;
private PorterDuff.Mode tintMode;
public CheckableDrawable(Context context, int checkedRes, int uncheckedRes, int filledRes, PointF offset) {
this.context = context;
this.checkedRes = checkedRes;
this.uncheckedRes = uncheckedRes;
this.filledRes = filledRes;
this.offset = offset;
maskPaint.setAlpha(255);
maskPaint.setColor(0xffffffff);
}
@Override
public void setBounds(@NonNull Rect bounds) {
if (!getBounds().equals(bounds))
checkedBitmap = uncheckedBitmap = filledBitmap = null;
super.setBounds(bounds);
}
@Override
public void setBounds(int left, int top, int right, int bottom) {
Rect bounds = getBounds();
if (bounds.left != left || bounds.right != right || bounds.bottom != bottom || bounds.top != top)
checkedBitmap = uncheckedBitmap = filledBitmap = null;
super.setBounds(left, top, right, bottom);
}
private void renderSVGs() {
Rect bounds = getBounds();
if (bounds.width() <= 0 && bounds.height() <= 0)
return;
try {
{
SVG svg = SVG.getFromResource(context, checkedRes);
checkedBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(checkedBitmap);
svg.setDocumentWidth(checkedBitmap.getWidth());
svg.setDocumentHeight(checkedBitmap.getHeight());
svg.renderToCanvas(canvas);
checkedShader = new BitmapShader(checkedBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Matrix matrix = new Matrix();
matrix.postTranslate(bounds.left, bounds.top);
checkedShader.setLocalMatrix(matrix);
}
{
SVG svg2 = SVG.getFromResource(context, uncheckedRes);
uncheckedBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(uncheckedBitmap);
svg2.setDocumentWidth(uncheckedBitmap.getWidth());
svg2.setDocumentHeight(uncheckedBitmap.getHeight());
svg2.renderToCanvas(canvas);
}
{
SVG svg3 = SVG.getFromResource(context, filledRes);
filledBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(filledBitmap);
svg3.setDocumentWidth(filledBitmap.getWidth());
svg3.setDocumentHeight(filledBitmap.getHeight());
svg3.renderToCanvas(canvas);
}
maskBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888);
maskCanvas = new Canvas(maskBitmap);
radius = (float) (Math.sqrt(2) * bounds.width() / 2);
} catch (SVGParseException e) {
Log.e(CheckableDrawable.class.getSimpleName(), "There was an error parsing SVG");
} catch (NullPointerException e) { // TODO: what is this catch for?
}
}
@Override
public void draw(@NonNull Canvas canvas) {
if (checkedBitmap == null)
renderSVGs();
Rect bounds = getBounds();
if (animator != null && animator.isRunning()) {
if (currAnim == ANIMATION_FILL) {
drawUnchecked(canvas, currRadius);
} else {
drawChecked(canvas, currRadius);
}
} else {
if (checkedState == CheckedState.CHECKED) {
paint.setColorFilter(checkedFilter);
canvas.drawBitmap(checkedBitmap, bounds.left, bounds.top, paint);
} else if (checkedState == CheckedState.UNCHECKED) {
paint.setColorFilter(uncheckedFilter);
canvas.drawBitmap(uncheckedBitmap, bounds.left, bounds.top, paint);
} else {
paint.setColorFilter(uncheckedFilter);
canvas.drawBitmap(filledBitmap, bounds.left, bounds.top, paint);
}
}
}
private ValueAnimator animateFill() {
if (animator != null && animator.isRunning())
animator.cancel();
animator = ValueAnimator.ofFloat(1, 0);
animator.setDuration(FILL_DURATION);
animator.addUpdateListener(animation -> {
currAnim = ANIMATION_FILL;
float value = (float) animation.getAnimatedValue();
currRadius = value * radius;
invalidateSelf();
});
return animator;
}
private ValueAnimator animateCheck() {
if (animator != null && animator.isRunning())
animator.cancel();
animator = ValueAnimator.ofFloat(0, 1);
animator.setDuration(CHECK_DURATION);
animator.addUpdateListener(animation -> {
currAnim = ANIMATION_CHECK;
float value = (float) animation.getAnimatedValue();
currRadius = value * radius;
invalidateSelf();
});
return animator;
}
private void drawUnchecked(@NonNull Canvas canvas, float radius) {
Rect bounds = getBounds();
paint.setColorFilter(uncheckedFilter);
canvas.drawBitmap(uncheckedBitmap, bounds.left, bounds.top, paint);
maskCanvas.drawColor(0xffffffff);
maskPaint.setXfermode(porterDuffClear);
maskCanvas.drawCircle(maskBitmap.getWidth() / 2, maskBitmap.getHeight() / 2, radius, maskPaint);
maskPaint.setXfermode(porterDuffSrcIn);
maskCanvas.drawBitmap(filledBitmap, 0, 0, maskPaint);
canvas.drawBitmap(maskBitmap, bounds.left, bounds.top, paint);
}
private void drawChecked(@NonNull Canvas canvas, float radius) {
Rect bounds = getBounds();
paint.setShader(null);
paint.setColorFilter(uncheckedFilter);
canvas.drawBitmap(uncheckedBitmap, bounds.left, bounds.top, paint);
maskCanvas.drawColor(0xffffffff);
maskPaint.setXfermode(porterDuffClear);
maskCanvas.drawCircle(maskBitmap.getWidth() / 2 + bounds.width() * offset.x, maskBitmap.getHeight() / 2 + bounds.height() * offset.y, radius, maskPaint);
maskPaint.setXfermode(porterDuffSrcIn);
maskCanvas.drawBitmap(filledBitmap, 0, 0, maskPaint);
canvas.drawBitmap(maskBitmap, bounds.left, bounds.top, paint);
paint.setShader(checkedShader);
paint.setColorFilter(checkedFilter);
canvas.drawCircle(bounds.centerX() + bounds.width() * offset.x, bounds.centerY() + bounds.height() * offset.y, radius, paint);
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
paint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
protected boolean onStateChange(int[] states) {
boolean changed = false;
if (states != null) {
boolean newChecked = false;
boolean newIndeterminate = false;
boolean newEnabled = false;
for (int state : states) {
if (state == android.R.attr.state_checked)
newChecked = true;
if (state == R.attr.carbon_state_indeterminate)
newIndeterminate = true;
if (state == android.R.attr.state_enabled)
newEnabled = true;
}
CheckedState newCheckedState = newIndeterminate ? CheckedState.INDETERMINATE : newChecked ? CheckedState.CHECKED : CheckedState.UNCHECKED;
if (checkedState != newCheckedState) {
setChecked(newCheckedState);
changed = true;
}
if (enabled != newEnabled) {
setEnabled(newEnabled);
changed = true;
}
}
boolean result = super.onStateChange(states);
if (changed && tint != null && tint instanceof AnimatedColorStateList)
((AnimatedColorStateList) tint).setState(states);
return result && changed;
}
public boolean isChecked() {
return checkedState == CheckedState.CHECKED;
}
public void setChecked(boolean checked) {
setChecked(checked ? CheckedState.CHECKED : CheckedState.UNCHECKED);
}
public void setChecked(CheckedState state) {
if (checkedState == state)
return;
if (checkedState == CheckedState.UNCHECKED) {
if (state == CheckedState.CHECKED) {
Animator fill = animateFill();
fill.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animateCheck().start();
}
});
fill.start();
} else {
animateFill().start();
}
}
if (checkedState == CheckedState.CHECKED) {
if (state == CheckedState.UNCHECKED) {
ValueAnimator check = animateCheck();
check.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animateFill().reverse();
}
});
check.reverse();
} else {
animateCheck().reverse();
}
}
if (checkedState == CheckedState.INDETERMINATE) {
if (state == CheckedState.CHECKED) {
animateCheck().start();
} else {
animateFill().reverse();
}
}
checkedState = state;
invalidateSelf();
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
invalidateSelf();
}
@Override
public void jumpToCurrentState() {
super.jumpToCurrentState();
if (animator != null)
animator.end();
invalidateSelf();
}
@Override
public int getIntrinsicWidth() {
return context.getResources().getDimensionPixelSize(R.dimen.carbon_iconSize);
}
@Override
public int getIntrinsicHeight() {
return getIntrinsicWidth();
}
@NonNull
@Override
public Rect getDirtyBounds() {
return super.getDirtyBounds();
}
@Override
public boolean isStateful() {
return true;
}
private boolean animateColorChanges = true;
private ValueAnimator.AnimatorUpdateListener tintAnimatorListener = animation -> {
updateTint();
invalidateSelf();
};
public boolean isAnimateColorChangesEnabled() {
return animateColorChanges;
}
public void setAnimateColorChangesEnabled(boolean animateColorChanges) {
this.animateColorChanges = animateColorChanges;
if (tint != null && !(tint instanceof AnimatedColorStateList))
setTint(AnimatedColorStateList.fromList(tint, tintAnimatorListener));
}
public void setTint(ColorStateList list) {
this.tint = animateColorChanges && !(list instanceof AnimatedColorStateList) ? AnimatedColorStateList.fromList(list, tintAnimatorListener) : list;
updateTint();
}
public ColorStateList getTint() {
return tint;
}
@Override
public void setTintMode(@NonNull PorterDuff.Mode mode) {
this.tintMode = mode;
updateTint();
}
private void updateTint() {
if (tint == null || tintMode == null) {
checkedFilter = null;
uncheckedFilter = null;
} else {
checkedFilter = new PorterDuffColorFilter(tint.getColorForState(new int[]{android.R.attr.state_checked, enabled ? android.R.attr.state_enabled : -android.R.attr.state_enabled}, tint.getDefaultColor()), tintMode);
uncheckedFilter = new PorterDuffColorFilter(tint.getColorForState(new int[]{-android.R.attr.state_checked, enabled ? android.R.attr.state_enabled : -android.R.attr.state_enabled}, tint.getDefaultColor()), tintMode);
}
invalidateSelf();
}
}