package wearable.android.zaim.net.views; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import wearable.android.zaim.net.R; /** * SeekArc.java * <p/> * This is a class that functions much like a SeekBar but * follows a circle path instead of a straight line. * * @author Neil Davies */ public class SeekArc extends View { private static final String TAG = SeekArc.class.getSimpleName(); private static int INVALID_PROGRESS_VALUE = -1; /** * The Drawable for the seek arc thumbnail */ private Drawable mThumb; /** * The Maximum value that this SeekArc can be set to */ private int mMax = 100; /** * The Current value that the SeekArc is set to */ private int mProgress = 0; /** * The width of the progress line for this SeekArc */ private int mProgressWidth = 4; /** * The Width of the background arc for the SeekArc */ private int mArcWidth = 2; /** * The Angle to start drawing this Arc from */ private int mStartAngle = 0; /** * The Angle through which to draw the arc (Max is 360) */ private int mSweepAngle = 360; /** * The rotation of the SeekArc- 0 is twelve o'clock */ private int mRotation = 0; /** * Give the SeekArc rounded edges */ private boolean mRoundedEdges = false; /** * Enable touch inside the SeekArc */ private boolean mTouchInside = true; /** * Will the progress increase clockwise or anti-clockwise */ private boolean mClockwise = true; // Internal variables private int mArcRadius = 0; private float mProgressSweep = 0; private RectF mArcRect = new RectF(); private Paint mArcPaint; private Paint mProgressPaint; private int mTranslateX; private int mTranslateY; private int mThumbXPos; private int mThumbYPos; private double mTouchAngle; private float mTouchIgnoreRadius; private OnSeekArcChangeListener mOnSeekArcChangeListener; public SeekArc(Context context) { super(context); init(context, null, 0); } public SeekArc(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, R.attr.seekArcStyle); } public SeekArc(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs, defStyle); } private void init(Context context, AttributeSet attrs, int defStyle) { Log.d(TAG, "Initialising SeekArc"); final Resources res = getResources(); float density = context.getResources().getDisplayMetrics().density; // Defaults, may need to link this into theme settings int arcColor = res.getColor(R.color.progress_gray); int progressColor = res.getColor(android.R.color.holo_blue_light); int thumbHalfheight = 0; int thumbHalfWidth = 0; mThumb = res.getDrawable(R.drawable.seek_arc_control_selector); // Convert progress width to pixels for current density mProgressWidth = (int) (mProgressWidth * density); if (attrs != null) { // Attribute initialization final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SeekArc, defStyle, 0); Drawable thumb = a.getDrawable(R.styleable.SeekArc_thumb); if (thumb != null) { mThumb = thumb; } thumbHalfheight = (int) mThumb.getIntrinsicHeight() / 2; thumbHalfWidth = (int) mThumb.getIntrinsicWidth() / 2; mThumb.setBounds(-thumbHalfWidth, -thumbHalfheight, thumbHalfWidth, thumbHalfheight); mMax = a.getInteger(R.styleable.SeekArc_max, mMax); mProgress = a.getInteger(R.styleable.SeekArc_progress, mProgress); mProgressWidth = (int) a.getDimension( R.styleable.SeekArc_progressWidth, mProgressWidth); mArcWidth = (int) a.getDimension(R.styleable.SeekArc_arcWidth, mArcWidth); mStartAngle = a.getInt(R.styleable.SeekArc_startAngle, mStartAngle); mSweepAngle = a.getInt(R.styleable.SeekArc_sweepAngle, mSweepAngle); mRotation = a.getInt(R.styleable.SeekArc_rotation, mRotation); mRoundedEdges = a.getBoolean(R.styleable.SeekArc_roundEdges, mRoundedEdges); mTouchInside = a.getBoolean(R.styleable.SeekArc_touchInside, mTouchInside); mClockwise = a.getBoolean(R.styleable.SeekArc_clockwise, mClockwise); arcColor = a.getColor(R.styleable.SeekArc_arcColor, arcColor); progressColor = a.getColor(R.styleable.SeekArc_progressColor, progressColor); a.recycle(); } mProgress = (mProgress > mMax) ? mMax : mProgress; mProgress = (mProgress < 0) ? 0 : mProgress; mSweepAngle = (mSweepAngle > 360) ? 360 : mSweepAngle; mSweepAngle = (mSweepAngle < 0) ? 0 : mSweepAngle; mStartAngle = (mStartAngle > 360) ? 0 : mStartAngle; mStartAngle = (mStartAngle < 0) ? 0 : mStartAngle; mArcPaint = new Paint(); mArcPaint.setColor(arcColor); mArcPaint.setAntiAlias(true); mArcPaint.setStyle(Paint.Style.STROKE); mArcPaint.setStrokeWidth(mArcWidth); //mArcPaint.setAlpha(45); mProgressPaint = new Paint(); mProgressPaint.setColor(progressColor); mProgressPaint.setAntiAlias(true); mProgressPaint.setStyle(Paint.Style.STROKE); mProgressPaint.setStrokeWidth(mProgressWidth); if (mRoundedEdges) { mArcPaint.setStrokeCap(Paint.Cap.ROUND); mProgressPaint.setStrokeCap(Paint.Cap.ROUND); } } @Override protected void onDraw(Canvas canvas) { if (!mClockwise) { canvas.scale(-1, 1, mArcRect.centerX(), mArcRect.centerY()); } // Draw the arcs int mAngleOffset = -90; final int arcStart = mStartAngle + mAngleOffset + mRotation; final int arcSweep = mSweepAngle; canvas.drawArc(mArcRect, arcStart, arcSweep, false, mArcPaint); canvas.drawArc(mArcRect, arcStart, mProgressSweep, false, mProgressPaint); // Draw the thumb nail canvas.translate(mTranslateX - mThumbXPos, mTranslateY - mThumbYPos); mThumb.draw(canvas); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); final int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); final int min = Math.min(width, height); float top = 0; float left = 0; int arcDiameter = 0; mTranslateX = (int) (width * 0.5f); mTranslateY = (int) (height * 0.5f); arcDiameter = min - getPaddingLeft(); mArcRadius = arcDiameter / 2; top = height / 2 - (arcDiameter / 2); left = width / 2 - (arcDiameter / 2); mArcRect.set(left, top, left + arcDiameter, top + arcDiameter); int arcStart = (int) mProgressSweep + mStartAngle + mRotation + 90; mThumbXPos = (int) (mArcRadius * Math.cos(Math.toRadians(arcStart))); mThumbYPos = (int) (mArcRadius * Math.sin(Math.toRadians(arcStart))); setTouchInSide(mTouchInside); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: onStartTrackingTouch(); updateOnTouch(event); break; case MotionEvent.ACTION_MOVE: updateOnTouch(event); break; case MotionEvent.ACTION_UP: onStopTrackingTouch(); setPressed(false); break; case MotionEvent.ACTION_CANCEL: onStopTrackingTouch(); setPressed(false); break; } return true; } @Override protected void drawableStateChanged() { super.drawableStateChanged(); if (mThumb != null && mThumb.isStateful()) { int[] state = getDrawableState(); mThumb.setState(state); } invalidate(); } private void onStartTrackingTouch() { if (mOnSeekArcChangeListener != null) { mOnSeekArcChangeListener.onStartTrackingTouch(this); } } private void onStopTrackingTouch() { if (mOnSeekArcChangeListener != null) { mOnSeekArcChangeListener.onStopTrackingTouch(this); } } private void updateOnTouch(MotionEvent event) { boolean ignoreTouch = ignoreTouch(event.getX(), event.getY()); if (ignoreTouch) { return; } setPressed(true); mTouchAngle = getTouchDegrees(event.getX(), event.getY()); int progress = getProgressForAngle(mTouchAngle); onProgressRefresh(progress, true); } private boolean ignoreTouch(float xPos, float yPos) { boolean ignore = false; float x = xPos - mTranslateX; float y = yPos - mTranslateY; float touchRadius = (float) Math.sqrt(((x * x) + (y * y))); if (touchRadius < mTouchIgnoreRadius) { ignore = true; } return ignore; } private double getTouchDegrees(float xPos, float yPos) { float x = xPos - mTranslateX; float y = yPos - mTranslateY; //invert the x-coord if we are rotating anti-clockwise x = (mClockwise) ? x : -x; // convert to arc Angle double angle = Math.toDegrees(Math.atan2(y, x) + (Math.PI / 2) - Math.toRadians(mRotation)); if (angle < 0) { angle = 360 + angle; } angle -= mStartAngle; return angle; } private int getProgressForAngle(double angle) { int touchProgress = (int) Math.round(valuePerDegree() * angle); touchProgress = (touchProgress < 0) ? INVALID_PROGRESS_VALUE : touchProgress; touchProgress = (touchProgress > mMax) ? INVALID_PROGRESS_VALUE : touchProgress; return touchProgress; } private float valuePerDegree() { return (float) mMax / mSweepAngle; } private void onProgressRefresh(int progress, boolean fromUser) { updateProgress(progress, fromUser); } private void updateThumbPosition() { int thumbAngle = (int) (mStartAngle + mProgressSweep + mRotation + 90); mThumbXPos = (int) (mArcRadius * Math.cos(Math.toRadians(thumbAngle))); mThumbYPos = (int) (mArcRadius * Math.sin(Math.toRadians(thumbAngle))); } private void updateProgress(int progress, boolean fromUser) { if (progress == INVALID_PROGRESS_VALUE) { return; } if (mOnSeekArcChangeListener != null) { mOnSeekArcChangeListener .onProgressChanged(this, progress, fromUser); } progress = (progress > mMax) ? mMax : progress; progress = (mProgress < 0) ? 0 : progress; mProgress = progress; mProgressSweep = (float) progress / mMax * mSweepAngle; updateThumbPosition(); invalidate(); } /** * Sets a listener to receive notifications of changes to the SeekArc's * progress level. Also provides notifications of when the user starts and * stops a touch gesture within the SeekArc. * * @param l The seek bar notification listener * @see android.widget.SeekBar.OnSeekBarChangeListener */ public void setOnSeekArcChangeListener(OnSeekArcChangeListener l) { mOnSeekArcChangeListener = l; } public void setProgress(int progress) { updateProgress(progress, false); } public int getProgressWidth() { return mProgressWidth; } public void setProgressWidth(int mProgressWidth) { this.mProgressWidth = mProgressWidth; mProgressPaint.setStrokeWidth(mProgressWidth); } public int getArcWidth() { return mArcWidth; } public void setArcWidth(int mArcWidth) { this.mArcWidth = mArcWidth; mArcPaint.setStrokeWidth(mArcWidth); } public int getArcRotation() { return mRotation; } public void setArcRotation(int mRotation) { this.mRotation = mRotation; updateThumbPosition(); } public int getStartAngle() { return mStartAngle; } public void setStartAngle(int mStartAngle) { this.mStartAngle = mStartAngle; updateThumbPosition(); } public int getSweepAngle() { return mSweepAngle; } public void setSweepAngle(int mSweepAngle) { this.mSweepAngle = mSweepAngle; updateThumbPosition(); } public void setRoundedEdges(boolean isEnabled) { mRoundedEdges = isEnabled; if (mRoundedEdges) { mArcPaint.setStrokeCap(Paint.Cap.ROUND); mProgressPaint.setStrokeCap(Paint.Cap.ROUND); } else { mArcPaint.setStrokeCap(Paint.Cap.SQUARE); mProgressPaint.setStrokeCap(Paint.Cap.SQUARE); } } public void setTouchInSide(boolean isEnabled) { int thumbHalfheight = (int) mThumb.getIntrinsicHeight() / 2; int thumbHalfWidth = (int) mThumb.getIntrinsicWidth() / 2; mTouchInside = isEnabled; if (mTouchInside) { mTouchIgnoreRadius = (float) mArcRadius / 4; } else { // Don't use the exact radius makes interaction too tricky mTouchIgnoreRadius = mArcRadius - Math.min(thumbHalfWidth, thumbHalfheight); } } public void setClockwise(boolean isClockwise) { mClockwise = isClockwise; } public interface OnSeekArcChangeListener { /** * Notification that the progress level has changed. Clients can use the * fromUser parameter to distinguish user-initiated changes from those * that occurred programmatically. * * @param seekArc The SeekArc whose progress has changed * @param progress The current progress level. * @param fromUser True if the progress change was initiated by the user. */ void onProgressChanged(SeekArc seekArc, int progress, boolean fromUser); /** * Notification that the user has started a touch gesture. Clients may * want to use this to disable advancing the seekbar. * * @param seekArc The SeekArc in which the touch gesture began */ void onStartTrackingTouch(SeekArc seekArc); /** * Notification that the user has finished a touch gesture. Clients may * want to use this to re-enable advancing the seekarc. * * @param seekArc The SeekArc in which the touch gesture began */ void onStopTrackingTouch(SeekArc seekArc); } }