package com.zdvdev.checkview; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.PointF; import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import static android.util.TypedValue.COMPLEX_UNIT_DIP; import static android.util.TypedValue.applyDimension; public class CheckView extends View { public static final int FLAG_STATE_PLUS = 0; public static final int FLAG_STATE_CHECK = 1; private static final long ANIMATION_DURATION_MS = 300L; private static final int DEFAULT_STROKE_WIDTH_DP = 4; private static final int DEFAULT_PADDING_DP = 12; private static final int DEFAULT_COLOR = Color.BLACK; // Arcs that define the set of all points between which the two lines are drawn // Names (top, bottom, etc) are from the reference point of the "plus" configuration. private Path firstPath; private Path secondPath; private Path thirdPath; private Path fourPath; // Pre-compute arc lengths when layout changes private float firstPathLength; private float secondPathLength; private float thirdPathLength; private float fourPathLength; private Paint paint; private int color = DEFAULT_COLOR; private float strokeWidth = DEFAULT_STROKE_WIDTH_DP; private PathMeasure pathMeasure; private float[] fromXY; private float[] toXY; /** * Internal state flag for the drawn appearance, plus or check. * The default starting position is "plus". This represents the real configuration, whereas * {@code percent} holds the frame-by-frame position when animating between * the states. */ private int state = FLAG_STATE_PLUS; /** * The percent value upon the arcs that line endpoints should be found * when drawing. */ private float percent = 1f; private boolean paddingDefined; OnClickListener onClickListener; boolean autoToggleEnabled; int padding; public CheckView(Context context) { super(context); } public CheckView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public CheckView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { int[] attributes = new int[]{android.R.attr.padding, android.R.attr.paddingLeft, android.R.attr.paddingTop, android.R.attr.paddingBottom, android.R.attr.paddingRight}; TypedArray arr = context.obtainStyledAttributes(attrs, attributes); for (int i = 0, length = attributes.length; i < length; i++) { paddingDefined |= arr.hasValue(i); } if (!paddingDefined) { int padding = (int) applyDimension(COMPLEX_UNIT_DIP, DEFAULT_PADDING_DP, getResources().getDisplayMetrics()); setPadding(padding, padding, padding, padding); } arr.recycle(); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CheckView, 0, 0); color = a.getColor(R.styleable.CheckView_cvlineColor, DEFAULT_COLOR); strokeWidth = a.getDimension(R.styleable.CheckView_cvstrokeWidth, -1); if (strokeWidth == -1) { strokeWidth = applyDimension(COMPLEX_UNIT_DIP, DEFAULT_STROKE_WIDTH_DP, getResources().getDisplayMetrics()); } autoToggleEnabled = a.getBoolean(R.styleable.CheckView_cvautoToggle, true); super.setOnClickListener(onClickListenerDelegate); a.recycle(); } public void setAutoToggle(boolean enable) { autoToggleEnabled = enable; } final OnClickListener onClickListenerDelegate = new OnClickListener() { @Override public void onClick(View v) { if (autoToggleEnabled) toggle(); if (onClickListener != null) onClickListener.onClick(v); } }; @Override public void setOnClickListener(OnClickListener l) { onClickListener = l; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.translate(padding, padding); //TODO this could be moved as a class attribute float percentFromState = state == FLAG_STATE_PLUS ? percent : 1 - percent; setPointFromPercent(firstPath, firstPathLength, percentFromState, fromXY); setPointFromPercent(secondPath, secondPathLength, percentFromState, toXY); canvas.drawLine(fromXY[0], fromXY[1], toXY[0], toXY[1], paint); setPointFromPercent(thirdPath, thirdPathLength, percentFromState, fromXY); setPointFromPercent(fourPath, fourPathLength, percentFromState, toXY); canvas.drawLine(fromXY[0], fromXY[1], toXY[0], toXY[1], paint); canvas.restore(); } /** * Given some path and its length, find the point ([x,y]) on that path at * the given percentage of length. Store the result in {@code points}. */ private void setPointFromPercent(Path path, float length, float percent, float[] points) { pathMeasure.setPath(path, false); pathMeasure.getPosTan(length * percent, points, null); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (changed) { measurePaths(); invalidate(); } } @Override public void setPadding(int left, int top, int right, int bottom) { super.setPadding(left, top, right, bottom); measurePaths(); } @Override @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public void setPaddingRelative(int start, int top, int end, int bottom) { super.setPaddingRelative(start, top, end, bottom); measurePaths(); } /** * Perform measurements and pre-calculations. This should be called any time * the view measurements or visuals are changed, such as with a call to {@link #setPadding(int, int, int, int)} * or an operating system callback like {@link #onLayout(boolean, int, int, int, int)}. */ private void measurePaths() { int maxSize; float middle; maxSize = Math.min(getWidth(), getHeight()); padding = Math.max( Math.max(getPaddingBottom(), getPaddingTop()), Math.max(getPaddingRight(), getPaddingLeft())); maxSize -= padding * 2; middle = maxSize / 2f; pathMeasure = new PathMeasure(); PointF p1a = new PointF(middle, 0); PointF p1b = getCheckRightPoint(maxSize); firstPath = new Path(); firstPath.moveTo(p1a.x, p1a.y); firstPath.lineTo(p1b.x, p1b.y); pathMeasure.setPath(firstPath, false); firstPathLength = pathMeasure.getLength(); PointF p2a = new PointF(middle, maxSize); PointF p2b = getCheckMiddlePoint(maxSize); secondPath = new Path(); secondPath.moveTo(p2a.x, p2a.y); secondPath.lineTo(p2b.x, p2b.y); pathMeasure.setPath(secondPath, false); secondPathLength = pathMeasure.getLength(); PointF p3a = new PointF(0, middle); PointF p3b = getCheckLeftPoint(maxSize); thirdPath = new Path(); thirdPath.moveTo(p3a.x, p3a.y); thirdPath.lineTo(p3b.x, p3b.y); pathMeasure.setPath(thirdPath, false); thirdPathLength = pathMeasure.getLength(); PointF p4a = new PointF(maxSize, middle); fourPath = new Path(); fourPath.moveTo(p4a.x, p4a.y); fourPath.lineTo(p2b.x, p2b.y); pathMeasure.setPath(fourPath, false); fourPathLength = pathMeasure.getLength(); paint = new Paint(); paint.setAntiAlias(true); paint.setColor(color); paint.setStyle(Paint.Style.STROKE); paint.setStrokeCap(Paint.Cap.SQUARE); paint.setStrokeWidth(strokeWidth); fromXY = new float[]{0f, 0f}; toXY = new float[]{0f, 0f}; } private PointF getCheckLeftPoint(int maxSize) { return new PointF(1, maxSize / 2); } private PointF getCheckMiddlePoint(int maxSize) { return new PointF(5 * maxSize / 16f, 13 * maxSize / 16f); } private PointF getCheckRightPoint(int maxSize) { return new PointF(maxSize, maxSize / 8f); } public void setColor(int argb) { color = argb; if (paint == null) { paint = new Paint(); } paint.setColor(argb); invalidate(); } /** * Transition to check status */ public void check() { check(ANIMATION_DURATION_MS); } /** * Transition to check status over the given animation duration */ public void check(long animationDurationMS) { if (state == FLAG_STATE_CHECK) { return; } toggle(animationDurationMS); } /** * Transition to "+" */ public void plus() { plus(ANIMATION_DURATION_MS); } /** * Transition to "+" over the given animation duration */ public void plus(long animationDurationMS) { if (state == FLAG_STATE_PLUS) { return; } toggle(animationDurationMS); } /** * Tell this view to switch states from check to plus, or back, using the default animation duration. * * @return an integer flag that represents the new state after toggling. * This will be either {@link #FLAG_STATE_PLUS} or {@link #FLAG_STATE_CHECK} */ public int toggle() { return toggle(ANIMATION_DURATION_MS); } /** * Tell this view to switch states from check to plus, or back. * * @param animationDurationMS duration in milliseconds for the toggle animation * @return an integer flag that represents the new state after toggling. * This will be either {@link #FLAG_STATE_PLUS} or {@link #FLAG_STATE_CHECK} */ public int toggle(long animationDurationMS) { state = state == FLAG_STATE_PLUS ? FLAG_STATE_CHECK : FLAG_STATE_PLUS; // invert percent, because state was just flipped percent = 1 - percent; ValueAnimator animator = ValueAnimator.ofFloat(percent, 1); animator.setInterpolator(new AccelerateDecelerateInterpolator()); animator.setDuration(animationDurationMS); animator.addUpdateListener(animationListener); animator.start(); return state; } private final AnimatorUpdateListener animationListener = new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { setPercent(animation.getAnimatedFraction()); } }; void setPercent(float percent) { this.percent = percent; invalidate(); } @Override protected Parcelable onSaveInstanceState() { Parcelable parcelable = super.onSaveInstanceState(); if (parcelable == null) { parcelable = new Bundle(); } CheckViewState savedState = new CheckViewState(parcelable); savedState.flagState = state; return savedState; } @Override protected void onRestoreInstanceState(Parcelable state) { if (!(state instanceof CheckViewState)) { super.onRestoreInstanceState(state); return; } CheckViewState ss = (CheckViewState) state; this.state = ss.flagState; if (this.state != FLAG_STATE_PLUS && this.state != FLAG_STATE_CHECK) { this.state = FLAG_STATE_PLUS; } super.onRestoreInstanceState(ss.getSuperState()); } static class CheckViewState extends BaseSavedState { int flagState; CheckViewState(Parcelable superState) { super(superState); } CheckViewState(Parcel in) { super(in); flagState = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(flagState); } public static final Creator<CheckViewState> CREATOR = new Creator<CheckViewState>() { @Override public CheckViewState createFromParcel(Parcel in) { return new CheckViewState(in); } @Override public CheckViewState[] newArray(int size) { return new CheckViewState[size]; } }; } }