/* * * Copyright 2013 Matt Joseph * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * * * This custom view/widget was inspired and guided by: * * HoloCircleSeekBar - Copyright 2012 Jes�s Manzano * HoloColorPicker - Copyright 2012 Lars Werkman (Designed by Marie Schweiz) * * Although I did not used the code from either project directly, they were both used as * reference material, and as a result, were extremely helpful. */ package com.devadvance.circularseekbar; import android.content.Context; import android.content.res.TypedArray; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.RectF; import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; public class CircularSeekBar extends View { /** * Used to scale the dp units to pixels */ private final float DPTOPX_SCALE = getResources().getDisplayMetrics().density; /** * Minimum touch target size in DP. 48dp is the Android design recommendation */ private final float MIN_TOUCH_TARGET_DP = 48; // Default values private static final float DEFAULT_CIRCLE_X_RADIUS = 30f; private static final float DEFAULT_CIRCLE_Y_RADIUS = 30f; private static final float DEFAULT_POINTER_RADIUS = 7f; private static final float DEFAULT_POINTER_HALO_WIDTH = 6f; private static final float DEFAULT_POINTER_HALO_BORDER_WIDTH = 2f; private static final float DEFAULT_CIRCLE_STROKE_WIDTH = 5f; private static final float DEFAULT_START_ANGLE = 270f; // Geometric (clockwise, relative to 3 o'clock) private static final float DEFAULT_END_ANGLE = 270f; // Geometric (clockwise, relative to 3 o'clock) private static final int DEFAULT_MAX = 100; private static final int DEFAULT_PROGRESS = 0; private static final int DEFAULT_CIRCLE_COLOR = Color.DKGRAY; private static final int DEFAULT_CIRCLE_PROGRESS_COLOR = Color.argb(235, 74, 138, 255); private static final int DEFAULT_POINTER_COLOR = Color.argb(235, 74, 138, 255); private static final int DEFAULT_POINTER_HALO_COLOR = Color.argb(135, 74, 138, 255); private static final int DEFAULT_POINTER_HALO_COLOR_ONTOUCH = Color.argb(135, 74, 138, 255); private static final int DEFAULT_CIRCLE_FILL_COLOR = Color.TRANSPARENT; private static final int DEFAULT_POINTER_ALPHA = 135; private static final int DEFAULT_POINTER_ALPHA_ONTOUCH = 100; private static final boolean DEFAULT_USE_CUSTOM_RADII = false; private static final boolean DEFAULT_MAINTAIN_EQUAL_CIRCLE = true; private static final boolean DEFAULT_MOVE_OUTSIDE_CIRCLE = false; private static final boolean DEFAULT_LOCK_ENABLED = true; /** * {@code Paint} instance used to draw the inactive circle. */ private Paint mCirclePaint; /** * {@code Paint} instance used to draw the circle fill. */ private Paint mCircleFillPaint; /** * {@code Paint} instance used to draw the active circle (represents progress). */ private Paint mCircleProgressPaint; /** * {@code Paint} instance used to draw the glow from the active circle. */ private Paint mCircleProgressGlowPaint; /** * {@code Paint} instance used to draw the center of the pointer. * Note: This is broken on 4.0+, as BlurMasks do not work with hardware acceleration. */ private Paint mPointerPaint; /** * {@code Paint} instance used to draw the halo of the pointer. * Note: The halo is the part that changes transparency. */ private Paint mPointerHaloPaint; /** * {@code Paint} instance used to draw the border of the pointer, outside of the halo. */ private Paint mPointerHaloBorderPaint; /** * The width of the circle (in pixels). */ private float mCircleStrokeWidth; /** * The X radius of the circle (in pixels). */ private float mCircleXRadius; /** * The Y radius of the circle (in pixels). */ private float mCircleYRadius; /** * The radius of the pointer (in pixels). */ private float mPointerRadius; /** * The width of the pointer halo (in pixels). */ private float mPointerHaloWidth; /** * The width of the pointer halo border (in pixels). */ private float mPointerHaloBorderWidth; /** * Start angle of the CircularSeekBar. * Note: If mStartAngle and mEndAngle are set to the same angle, 0.1 is subtracted * from the mEndAngle to make the circle function properly. */ private float mStartAngle; /** * End angle of the CircularSeekBar. * Note: If mStartAngle and mEndAngle are set to the same angle, 0.1 is subtracted * from the mEndAngle to make the circle function properly. */ private float mEndAngle; /** * {@code RectF} that represents the circle (or ellipse) of the seekbar. */ private RectF mCircleRectF = new RectF(); /** * Holds the color value for {@code mPointerPaint} before the {@code Paint} instance is created. */ private int mPointerColor = DEFAULT_POINTER_COLOR; /** * Holds the color value for {@code mPointerHaloPaint} before the {@code Paint} instance is created. */ private int mPointerHaloColor = DEFAULT_POINTER_HALO_COLOR; /** * Holds the color value for {@code mPointerHaloPaint} before the {@code Paint} instance is created. */ private int mPointerHaloColorOnTouch = DEFAULT_POINTER_HALO_COLOR_ONTOUCH; /** * Holds the color value for {@code mCirclePaint} before the {@code Paint} instance is created. */ private int mCircleColor = DEFAULT_CIRCLE_COLOR; /** * Holds the color value for {@code mCircleFillPaint} before the {@code Paint} instance is created. */ private int mCircleFillColor = DEFAULT_CIRCLE_FILL_COLOR; /** * Holds the color value for {@code mCircleProgressPaint} before the {@code Paint} instance is created. */ private int mCircleProgressColor = DEFAULT_CIRCLE_PROGRESS_COLOR; /** * Holds the alpha value for {@code mPointerHaloPaint}. */ private int mPointerAlpha = DEFAULT_POINTER_ALPHA; /** * Holds the OnTouch alpha value for {@code mPointerHaloPaint}. */ private int mPointerAlphaOnTouch = DEFAULT_POINTER_ALPHA_ONTOUCH; /** * Distance (in degrees) that the the circle/semi-circle makes up. * This amount represents the max of the circle in degrees. */ private float mTotalCircleDegrees; /** * Distance (in degrees) that the current progress makes up in the circle. */ private float mProgressDegrees; /** * {@code Path} used to draw the circle/semi-circle. */ private Path mCirclePath; /** * {@code Path} used to draw the progress on the circle. */ private Path mCircleProgressPath; /** * Max value that this CircularSeekBar is representing. */ private int mMax; /** * Progress value that this CircularSeekBar is representing. */ private int mProgress; /** * If true, then the user can specify the X and Y radii. * If false, then the View itself determines the size of the CircularSeekBar. */ private boolean mCustomRadii; /** * Maintain a perfect circle (equal x and y radius), regardless of view or custom attributes. * The smaller of the two radii will always be used in this case. * The default is to be a circle and not an ellipse, due to the behavior of the ellipse. */ private boolean mMaintainEqualCircle; /** * Once a user has touched the circle, this determines if moving outside the circle is able * to change the position of the pointer (and in turn, the progress). */ private boolean mMoveOutsideCircle; /** * Used for enabling/disabling the lock option for easier hitting of the 0 progress mark. * */ private boolean lockEnabled = true; /** * Used for when the user moves beyond the start of the circle when moving counter clockwise. * Makes it easier to hit the 0 progress mark. */ private boolean lockAtStart = true; /** * Used for when the user moves beyond the end of the circle when moving clockwise. * Makes it easier to hit the 100% (max) progress mark. */ private boolean lockAtEnd = false; /** * When the user is touching the circle on ACTION_DOWN, this is set to true. * Used when touching the CircularSeekBar. */ private boolean mUserIsMovingPointer = false; /** * Represents the clockwise distance from {@code mStartAngle} to the touch angle. * Used when touching the CircularSeekBar. */ private float cwDistanceFromStart; /** * Represents the counter-clockwise distance from {@code mStartAngle} to the touch angle. * Used when touching the CircularSeekBar. */ private float ccwDistanceFromStart; /** * Represents the clockwise distance from {@code mEndAngle} to the touch angle. * Used when touching the CircularSeekBar. */ private float cwDistanceFromEnd; /** * Represents the counter-clockwise distance from {@code mEndAngle} to the touch angle. * Used when touching the CircularSeekBar. * Currently unused, but kept just in case. */ @SuppressWarnings("unused") private float ccwDistanceFromEnd; /** * The previous touch action value for {@code cwDistanceFromStart}. * Used when touching the CircularSeekBar. */ private float lastCWDistanceFromStart; /** * Represents the clockwise distance from {@code mPointerPosition} to the touch angle. * Used when touching the CircularSeekBar. */ private float cwDistanceFromPointer; /** * Represents the counter-clockwise distance from {@code mPointerPosition} to the touch angle. * Used when touching the CircularSeekBar. */ private float ccwDistanceFromPointer; /** * True if the user is moving clockwise around the circle, false if moving counter-clockwise. * Used when touching the CircularSeekBar. */ private boolean mIsMovingCW; /** * The width of the circle used in the {@code RectF} that is used to draw it. * Based on either the View width or the custom X radius. */ private float mCircleWidth; /** * The height of the circle used in the {@code RectF} that is used to draw it. * Based on either the View width or the custom Y radius. */ private float mCircleHeight; /** * Represents the progress mark on the circle, in geometric degrees. * This is not provided by the user; it is calculated; */ private float mPointerPosition; /** * Pointer position in terms of X and Y coordinates. */ private float[] mPointerPositionXY = new float[2]; /** * Listener. */ private OnCircularSeekBarChangeListener mOnCircularSeekBarChangeListener; /** * Initialize the CircularSeekBar with the attributes from the XML style. * Uses the defaults defined at the top of this file when an attribute is not specified by the user. * @param attrArray TypedArray containing the attributes. */ private void initAttributes(TypedArray attrArray) { mCircleXRadius = (float) (attrArray.getFloat(R.styleable.CircularSeekBar_circle_x_radius, DEFAULT_CIRCLE_X_RADIUS) * DPTOPX_SCALE); mCircleYRadius = (float) (attrArray.getFloat(R.styleable.CircularSeekBar_circle_y_radius, DEFAULT_CIRCLE_Y_RADIUS) * DPTOPX_SCALE); mPointerRadius = (float) (attrArray.getFloat(R.styleable.CircularSeekBar_pointer_radius, DEFAULT_POINTER_RADIUS) * DPTOPX_SCALE); mPointerHaloWidth = (float) (attrArray.getFloat(R.styleable.CircularSeekBar_pointer_halo_width, DEFAULT_POINTER_HALO_WIDTH) * DPTOPX_SCALE); mPointerHaloBorderWidth = (float) (attrArray.getFloat(R.styleable.CircularSeekBar_pointer_halo_border_width, DEFAULT_POINTER_HALO_BORDER_WIDTH) * DPTOPX_SCALE); mCircleStrokeWidth = (float) (attrArray.getFloat(R.styleable.CircularSeekBar_circle_stroke_width, DEFAULT_CIRCLE_STROKE_WIDTH) * DPTOPX_SCALE); String tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_color); if (tempColor != null) { try { mPointerColor = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { mPointerColor = DEFAULT_POINTER_COLOR; } } tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_halo_color); if (tempColor != null) { try { mPointerHaloColor = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { mPointerHaloColor = DEFAULT_POINTER_HALO_COLOR; } } tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_halo_color_ontouch); if (tempColor != null) { try { mPointerHaloColorOnTouch = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { mPointerHaloColorOnTouch = DEFAULT_POINTER_HALO_COLOR_ONTOUCH; } } tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_color); if (tempColor != null) { try { mCircleColor = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { mCircleColor = DEFAULT_CIRCLE_COLOR; } } tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_progress_color); if (tempColor != null) { try { mCircleProgressColor = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { mCircleProgressColor = DEFAULT_CIRCLE_PROGRESS_COLOR; } } tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_fill); if (tempColor != null) { try { mCircleFillColor = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { mCircleFillColor = DEFAULT_CIRCLE_FILL_COLOR; } } mPointerAlpha = Color.alpha(mPointerHaloColor); mPointerAlphaOnTouch = attrArray.getInt(R.styleable.CircularSeekBar_pointer_alpha_ontouch, DEFAULT_POINTER_ALPHA_ONTOUCH); if (mPointerAlphaOnTouch > 255 || mPointerAlphaOnTouch < 0) { mPointerAlphaOnTouch = DEFAULT_POINTER_ALPHA_ONTOUCH; } mMax = attrArray.getInt(R.styleable.CircularSeekBar_max, DEFAULT_MAX); mProgress = attrArray.getInt(R.styleable.CircularSeekBar_progress, DEFAULT_PROGRESS); mCustomRadii = attrArray.getBoolean(R.styleable.CircularSeekBar_use_custom_radii, DEFAULT_USE_CUSTOM_RADII); mMaintainEqualCircle = attrArray.getBoolean(R.styleable.CircularSeekBar_maintain_equal_circle, DEFAULT_MAINTAIN_EQUAL_CIRCLE); mMoveOutsideCircle = attrArray.getBoolean(R.styleable.CircularSeekBar_move_outside_circle, DEFAULT_MOVE_OUTSIDE_CIRCLE); lockEnabled = attrArray.getBoolean(R.styleable.CircularSeekBar_lock_enabled, DEFAULT_LOCK_ENABLED); // Modulo 360 right now to avoid constant conversion mStartAngle = ((360f + (attrArray.getFloat((R.styleable.CircularSeekBar_start_angle), DEFAULT_START_ANGLE) % 360f)) % 360f); mEndAngle = ((360f + (attrArray.getFloat((R.styleable.CircularSeekBar_end_angle), DEFAULT_END_ANGLE) % 360f)) % 360f); if (mStartAngle == mEndAngle) { //mStartAngle = mStartAngle + 1f; mEndAngle = mEndAngle - .1f; } } /** * Initializes the {@code Paint} objects with the appropriate styles. */ private void initPaints() { mCirclePaint = new Paint(); mCirclePaint.setAntiAlias(true); mCirclePaint.setDither(true); mCirclePaint.setColor(mCircleColor); mCirclePaint.setStrokeWidth(mCircleStrokeWidth); mCirclePaint.setStyle(Paint.Style.STROKE); mCirclePaint.setStrokeJoin(Paint.Join.ROUND); mCirclePaint.setStrokeCap(Paint.Cap.ROUND); mCircleFillPaint = new Paint(); mCircleFillPaint.setAntiAlias(true); mCircleFillPaint.setDither(true); mCircleFillPaint.setColor(mCircleFillColor); mCircleFillPaint.setStyle(Paint.Style.FILL); mCircleProgressPaint = new Paint(); mCircleProgressPaint.setAntiAlias(true); mCircleProgressPaint.setDither(true); mCircleProgressPaint.setColor(mCircleProgressColor); mCircleProgressPaint.setStrokeWidth(mCircleStrokeWidth); mCircleProgressPaint.setStyle(Paint.Style.STROKE); mCircleProgressPaint.setStrokeJoin(Paint.Join.ROUND); mCircleProgressPaint.setStrokeCap(Paint.Cap.ROUND); mCircleProgressGlowPaint = new Paint(); mCircleProgressGlowPaint.set(mCircleProgressPaint); mCircleProgressGlowPaint.setMaskFilter(new BlurMaskFilter((5f * DPTOPX_SCALE), BlurMaskFilter.Blur.NORMAL)); mPointerPaint = new Paint(); mPointerPaint.setAntiAlias(true); mPointerPaint.setDither(true); mPointerPaint.setStyle(Paint.Style.FILL); mPointerPaint.setColor(mPointerColor); mPointerPaint.setStrokeWidth(mPointerRadius); mPointerHaloPaint = new Paint(); mPointerHaloPaint.set(mPointerPaint); mPointerHaloPaint.setColor(mPointerHaloColor); mPointerHaloPaint.setAlpha(mPointerAlpha); mPointerHaloPaint.setStrokeWidth(mPointerRadius + mPointerHaloWidth); mPointerHaloBorderPaint = new Paint(); mPointerHaloBorderPaint.set(mPointerPaint); mPointerHaloBorderPaint.setStrokeWidth(mPointerHaloBorderWidth); mPointerHaloBorderPaint.setStyle(Paint.Style.STROKE); } /** * Calculates the total degrees between mStartAngle and mEndAngle, and sets mTotalCircleDegrees * to this value. */ private void calculateTotalDegrees() { mTotalCircleDegrees = (360f - (mStartAngle - mEndAngle)) % 360f; // Length of the entire circle/arc if (mTotalCircleDegrees <= 0f) { mTotalCircleDegrees = 360f; } } /** * Calculate the degrees that the progress represents. Also called the sweep angle. * Sets mProgressDegrees to that value. */ private void calculateProgressDegrees() { mProgressDegrees = mPointerPosition - mStartAngle; // Verified mProgressDegrees = (mProgressDegrees < 0 ? 360f + mProgressDegrees : mProgressDegrees); // Verified } /** * Calculate the pointer position (and the end of the progress arc) in degrees. * Sets mPointerPosition to that value. */ private void calculatePointerAngle() { float progressPercent = ((float)mProgress / (float)mMax); mPointerPosition = (progressPercent * mTotalCircleDegrees) + mStartAngle; mPointerPosition = mPointerPosition % 360f; } private void calculatePointerXYPosition() { PathMeasure pm = new PathMeasure(mCircleProgressPath, false); boolean returnValue = pm.getPosTan(pm.getLength(), mPointerPositionXY, null); if (!returnValue) { pm = new PathMeasure(mCirclePath, false); returnValue = pm.getPosTan(0, mPointerPositionXY, null); } } /** * Initialize the {@code Path} objects with the appropriate values. */ private void initPaths() { mCirclePath = new Path(); mCirclePath.addArc(mCircleRectF, mStartAngle, mTotalCircleDegrees); mCircleProgressPath = new Path(); mCircleProgressPath.addArc(mCircleRectF, mStartAngle, mProgressDegrees); } /** * Initialize the {@code RectF} objects with the appropriate values. */ private void initRects() { mCircleRectF.set(-mCircleWidth, -mCircleHeight, mCircleWidth, mCircleHeight); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.translate(this.getWidth() / 2, this.getHeight() / 2); canvas.drawPath(mCirclePath, mCirclePaint); canvas.drawPath(mCircleProgressPath, mCircleProgressGlowPaint); canvas.drawPath(mCircleProgressPath, mCircleProgressPaint); canvas.drawPath(mCirclePath, mCircleFillPaint); canvas.drawCircle(mPointerPositionXY[0], mPointerPositionXY[1], mPointerRadius + mPointerHaloWidth, mPointerHaloPaint); canvas.drawCircle(mPointerPositionXY[0], mPointerPositionXY[1], mPointerRadius, mPointerPaint); if (mUserIsMovingPointer) { canvas.drawCircle(mPointerPositionXY[0], mPointerPositionXY[1], mPointerRadius + mPointerHaloWidth + (mPointerHaloBorderWidth / 2f), mPointerHaloBorderPaint); } } /** * Get the progress of the CircularSeekBar. * @return The progress of the CircularSeekBar. */ public int getProgress() { int progress = Math.round((float)mMax * mProgressDegrees / mTotalCircleDegrees); return progress; } /** * Set the progress of the CircularSeekBar. * If the progress is the same, then any listener will not receive a onProgressChanged event. * @param progress The progress to set the CircularSeekBar to. */ public void setProgress(int progress) { if (mProgress != progress) { mProgress = progress; if (mOnCircularSeekBarChangeListener != null) { mOnCircularSeekBarChangeListener.onProgressChanged(this, progress, false); } recalculateAll(); invalidate(); } } private void setProgressBasedOnAngle(float angle) { mPointerPosition = angle; calculateProgressDegrees(); mProgress = Math.round((float)mMax * mProgressDegrees / mTotalCircleDegrees); } private void recalculateAll() { calculateTotalDegrees(); calculatePointerAngle(); calculateProgressDegrees(); initRects(); initPaths(); calculatePointerXYPosition(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); if (mMaintainEqualCircle) { int min = Math.min(width, height); setMeasuredDimension(min, min); } else { setMeasuredDimension(width, height); } // Set the circle width and height based on the view for the moment mCircleHeight = (float)height / 2f - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f); mCircleWidth = (float)width / 2f - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f); // If it is not set to use custom if (mCustomRadii) { // Check to make sure the custom radii are not out of the view. If they are, just use the view values if ((mCircleYRadius - mCircleStrokeWidth - mPointerRadius - mPointerHaloBorderWidth) < mCircleHeight) { mCircleHeight = mCircleYRadius - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f); } if ((mCircleXRadius - mCircleStrokeWidth - mPointerRadius - mPointerHaloBorderWidth) < mCircleWidth) { mCircleWidth = mCircleXRadius - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f); } } if (mMaintainEqualCircle) { // Applies regardless of how the values were determined float min = Math.min(mCircleHeight, mCircleWidth); mCircleHeight = min; mCircleWidth = min; } recalculateAll(); } public boolean isLockEnabled() { return lockEnabled; } public void setLockEnabled(boolean lockEnabled) { this.lockEnabled = lockEnabled; } @Override public boolean onTouchEvent(MotionEvent event) { // Convert coordinates to our internal coordinate system float x = event.getX() - getWidth() / 2; float y = event.getY() - getHeight() / 2; // Get the distance from the center of the circle in terms of x and y float distanceX = mCircleRectF.centerX() - x; float distanceY = mCircleRectF.centerY() - y; // Get the distance from the center of the circle in terms of a radius float touchEventRadius = (float) Math.sqrt((Math.pow(distanceX, 2) + Math.pow(distanceY, 2))); float minimumTouchTarget = MIN_TOUCH_TARGET_DP * DPTOPX_SCALE; // Convert minimum touch target into px float additionalRadius; // Either uses the minimumTouchTarget size or larger if the ring/pointer is larger if (mCircleStrokeWidth < minimumTouchTarget) { // If the width is less than the minimumTouchTarget, use the minimumTouchTarget additionalRadius = minimumTouchTarget / 2; } else { additionalRadius = mCircleStrokeWidth / 2; // Otherwise use the width } float outerRadius = Math.max(mCircleHeight, mCircleWidth) + additionalRadius; // Max outer radius of the circle, including the minimumTouchTarget or wheel width float innerRadius = Math.min(mCircleHeight, mCircleWidth) - additionalRadius; // Min inner radius of the circle, including the minimumTouchTarget or wheel width if (mPointerRadius < (minimumTouchTarget / 2)) { // If the pointer radius is less than the minimumTouchTarget, use the minimumTouchTarget additionalRadius = minimumTouchTarget / 2; } else { additionalRadius = mPointerRadius; // Otherwise use the radius } float touchAngle; touchAngle = (float) ((Math.atan2(y, x) / Math.PI * 180) % 360); // Verified touchAngle = (touchAngle < 0 ? 360 + touchAngle : touchAngle); // Verified cwDistanceFromStart = touchAngle - mStartAngle; // Verified cwDistanceFromStart = (cwDistanceFromStart < 0 ? 360f + cwDistanceFromStart : cwDistanceFromStart); // Verified ccwDistanceFromStart = 360f - cwDistanceFromStart; // Verified cwDistanceFromEnd = touchAngle - mEndAngle; // Verified cwDistanceFromEnd = (cwDistanceFromEnd < 0 ? 360f + cwDistanceFromEnd : cwDistanceFromEnd); // Verified ccwDistanceFromEnd = 360f - cwDistanceFromEnd; // Verified switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // These are only used for ACTION_DOWN for handling if the pointer was the part that was touched float pointerRadiusDegrees = (float) ((mPointerRadius * 180) / (Math.PI * Math.max(mCircleHeight, mCircleWidth))); cwDistanceFromPointer = touchAngle - mPointerPosition; cwDistanceFromPointer = (cwDistanceFromPointer < 0 ? 360f + cwDistanceFromPointer : cwDistanceFromPointer); ccwDistanceFromPointer = 360f - cwDistanceFromPointer; // This is for if the first touch is on the actual pointer. if (((touchEventRadius >= innerRadius) && (touchEventRadius <= outerRadius)) && ( (cwDistanceFromPointer <= pointerRadiusDegrees) || (ccwDistanceFromPointer <= pointerRadiusDegrees)) ) { setProgressBasedOnAngle(mPointerPosition); lastCWDistanceFromStart = cwDistanceFromStart; mIsMovingCW = true; mPointerHaloPaint.setAlpha(mPointerAlphaOnTouch); mPointerHaloPaint.setColor(mPointerHaloColorOnTouch); recalculateAll(); invalidate(); if (mOnCircularSeekBarChangeListener != null) { mOnCircularSeekBarChangeListener.onStartTrackingTouch(this); } mUserIsMovingPointer = true; lockAtEnd = false; lockAtStart = false; } else if (cwDistanceFromStart > mTotalCircleDegrees) { // If the user is touching outside of the start AND end mUserIsMovingPointer = false; return false; } else if ((touchEventRadius >= innerRadius) && (touchEventRadius <= outerRadius)) { // If the user is touching near the circle setProgressBasedOnAngle(touchAngle); lastCWDistanceFromStart = cwDistanceFromStart; mIsMovingCW = true; mPointerHaloPaint.setAlpha(mPointerAlphaOnTouch); mPointerHaloPaint.setColor(mPointerHaloColorOnTouch); recalculateAll(); invalidate(); if (mOnCircularSeekBarChangeListener != null) { mOnCircularSeekBarChangeListener.onStartTrackingTouch(this); mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true); } mUserIsMovingPointer = true; lockAtEnd = false; lockAtStart = false; } else { // If the user is not touching near the circle mUserIsMovingPointer = false; return false; } break; case MotionEvent.ACTION_MOVE: if (mUserIsMovingPointer) { if (lastCWDistanceFromStart < cwDistanceFromStart) { if ((cwDistanceFromStart - lastCWDistanceFromStart) > 180f && !mIsMovingCW) { lockAtStart = true; lockAtEnd = false; } else { mIsMovingCW = true; } } else { if ((lastCWDistanceFromStart - cwDistanceFromStart) > 180f && mIsMovingCW) { lockAtEnd = true; lockAtStart = false; } else { mIsMovingCW = false; } } if (lockAtStart && mIsMovingCW) { lockAtStart = false; } if (lockAtEnd && !mIsMovingCW) { lockAtEnd = false; } if (lockAtStart && !mIsMovingCW && (ccwDistanceFromStart > 90)) { lockAtStart = false; } if (lockAtEnd && mIsMovingCW && (cwDistanceFromEnd > 90)) { lockAtEnd = false; } // Fix for passing the end of a semi-circle quickly if (!lockAtEnd && cwDistanceFromStart > mTotalCircleDegrees && mIsMovingCW && lastCWDistanceFromStart < mTotalCircleDegrees) { lockAtEnd = true; } if (lockAtStart && lockEnabled) { // TODO: Add a check if mProgress is already 0, in which case don't call the listener mProgress = 0; recalculateAll(); invalidate(); if (mOnCircularSeekBarChangeListener != null) { mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true); } } else if (lockAtEnd && lockEnabled) { mProgress = mMax; recalculateAll(); invalidate(); if (mOnCircularSeekBarChangeListener != null) { mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true); } } else if ((mMoveOutsideCircle) || (touchEventRadius <= outerRadius)) { if (!(cwDistanceFromStart > mTotalCircleDegrees)) { setProgressBasedOnAngle(touchAngle); } recalculateAll(); invalidate(); if (mOnCircularSeekBarChangeListener != null) { mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true); } } else { break; } lastCWDistanceFromStart = cwDistanceFromStart; } else { return false; } break; case MotionEvent.ACTION_UP: mPointerHaloPaint.setAlpha(mPointerAlpha); mPointerHaloPaint.setColor(mPointerHaloColor); if (mUserIsMovingPointer) { mUserIsMovingPointer = false; invalidate(); if (mOnCircularSeekBarChangeListener != null) { mOnCircularSeekBarChangeListener.onStopTrackingTouch(this); } } else { return false; } break; case MotionEvent.ACTION_CANCEL: // Used when the parent view intercepts touches for things like scrolling mPointerHaloPaint.setAlpha(mPointerAlpha); mPointerHaloPaint.setColor(mPointerHaloColor); mUserIsMovingPointer = false; invalidate(); break; } if (event.getAction() == MotionEvent.ACTION_MOVE && getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } return true; } private void init(AttributeSet attrs, int defStyle) { final TypedArray attrArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircularSeekBar, defStyle, 0); initAttributes(attrArray); attrArray.recycle(); initPaints(); } public CircularSeekBar(Context context) { super(context); init(null, 0); } public CircularSeekBar(Context context, AttributeSet attrs) { super(context, attrs); init(attrs, 0); } public CircularSeekBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs, defStyle); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); Bundle state = new Bundle(); state.putParcelable("PARENT", superState); state.putInt("MAX", mMax); state.putInt("PROGRESS", mProgress); state.putInt("mCircleColor", mCircleColor); state.putInt("mCircleProgressColor", mCircleProgressColor); state.putInt("mPointerColor", mPointerColor); state.putInt("mPointerHaloColor", mPointerHaloColor); state.putInt("mPointerHaloColorOnTouch", mPointerHaloColorOnTouch); state.putInt("mPointerAlpha", mPointerAlpha); state.putInt("mPointerAlphaOnTouch", mPointerAlphaOnTouch); state.putBoolean("lockEnabled", lockEnabled); return state; } @Override protected void onRestoreInstanceState(Parcelable state) { Bundle savedState = (Bundle) state; Parcelable superState = savedState.getParcelable("PARENT"); super.onRestoreInstanceState(superState); mMax = savedState.getInt("MAX"); mProgress = savedState.getInt("PROGRESS"); mCircleColor = savedState.getInt("mCircleColor"); mCircleProgressColor = savedState.getInt("mCircleProgressColor"); mPointerColor = savedState.getInt("mPointerColor"); mPointerHaloColor = savedState.getInt("mPointerHaloColor"); mPointerHaloColorOnTouch = savedState.getInt("mPointerHaloColorOnTouch"); mPointerAlpha = savedState.getInt("mPointerAlpha"); mPointerAlphaOnTouch = savedState.getInt("mPointerAlphaOnTouch"); lockEnabled = savedState.getBoolean("lockEnabled"); initPaints(); recalculateAll(); } public void setOnSeekBarChangeListener(OnCircularSeekBarChangeListener l) { mOnCircularSeekBarChangeListener = l; } /** * Listener for the CircularSeekBar. Implements the same methods as the normal OnSeekBarChangeListener. */ public interface OnCircularSeekBarChangeListener { public abstract void onProgressChanged(CircularSeekBar circularSeekBar, int progress, boolean fromUser); public abstract void onStopTrackingTouch(CircularSeekBar seekBar); public abstract void onStartTrackingTouch(CircularSeekBar seekBar); } /** * Sets the circle color. * @param color the color of the circle */ public void setCircleColor(int color) { mCircleColor = color; mCirclePaint.setColor(mCircleColor); invalidate(); } /** * Gets the circle color. * @return An integer color value for the circle */ public int getCircleColor() { return mCircleColor; } /** * Sets the circle progress color. * @param color the color of the circle progress */ public void setCircleProgressColor(int color) { mCircleProgressColor = color; mCircleProgressPaint.setColor(mCircleProgressColor); invalidate(); } /** * Gets the circle progress color. * @return An integer color value for the circle progress */ public int getCircleProgressColor() { return mCircleProgressColor; } /** * Sets the pointer color. * @param color the color of the pointer */ public void setPointerColor(int color) { mPointerColor = color; mPointerPaint.setColor(mPointerColor); invalidate(); } /** * Gets the pointer color. * @return An integer color value for the pointer */ public int getPointerColor() { return mPointerColor; } /** * Sets the pointer halo color. * @param color the color of the pointer halo */ public void setPointerHaloColor(int color) { mPointerHaloColor = color; mPointerHaloPaint.setColor(mPointerHaloColor); invalidate(); } /** * Gets the pointer halo color. * @return An integer color value for the pointer halo */ public int getPointerHaloColor() { return mPointerHaloColor; } /** * Sets the pointer alpha. * @param alpha the alpha of the pointer */ public void setPointerAlpha(int alpha) { if (alpha >=0 && alpha <= 255) { mPointerAlpha = alpha; mPointerHaloPaint.setAlpha(mPointerAlpha); invalidate(); } } /** * Gets the pointer alpha value. * @return An integer alpha value for the pointer (0..255) */ public int getPointerAlpha() { return mPointerAlpha; } /** * Sets the pointer alpha when touched. * @param alpha the alpha of the pointer (0..255) when touched */ public void setPointerAlphaOnTouch(int alpha) { if (alpha >=0 && alpha <= 255) { mPointerAlphaOnTouch = alpha; } } /** * Gets the pointer alpha value when touched. * @return An integer alpha value for the pointer (0..255) when touched */ public int getPointerAlphaOnTouch() { return mPointerAlphaOnTouch; } /** * Sets the circle fill color. * @param color the color of the circle fill */ public void setCircleFillColor(int color) { mCircleFillColor = color; mCircleFillPaint.setColor(mCircleFillColor); invalidate(); } /** * Gets the circle fill color. * @return An integer color value for the circle fill */ public int getCircleFillColor() { return mCircleFillColor; } /** * Set the max of the CircularSeekBar. * If the new max is less than the current progress, then the progress will be set to zero. * If the progress is changed as a result, then any listener will receive a onProgressChanged event. * @param max The new max for the CircularSeekBar. */ public void setMax(int max) { if (!(max <= 0)) { // Check to make sure it's greater than zero if (max <= mProgress) { mProgress = 0; // If the new max is less than current progress, set progress to zero if (mOnCircularSeekBarChangeListener != null) { mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, false); } } mMax = max; recalculateAll(); invalidate(); } } /** * Get the current max of the CircularSeekBar. * @return Synchronized integer value of the max. */ public synchronized int getMax() { return mMax; } }