Java tutorial
/** * Wire * Copyright (C) 2016 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * This part of the Wire software uses source code from the CircularSeekBar library. * (https://github.com/devadvance/circularseekbar) * * 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 Jesus 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.waz.zclient.views.images; 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.PorterDuff; import android.graphics.RectF; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.v4.view.GestureDetectorCompat; import android.util.AttributeSet; import android.util.Property; import android.view.GestureDetector; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import com.makeramen.roundedimageview.RoundedImageView; import com.waz.zclient.R; import com.waz.zclient.ui.utils.ColorUtils; import com.waz.zclient.ui.utils.MathUtils; import com.waz.zclient.views.OnDoubleClickListener; public class CircularSeekBar extends RoundedImageView { // 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; public static final Property<CircularSeekBar, Float> DARKEN_LEVEL = new Property<CircularSeekBar, Float>( Float.class, "darkenLevel") { @Override public Float get(CircularSeekBar object) { return object.getDarkenLevel(); } @Override public void set(CircularSeekBar object, Float value) { object.setDarkenLevel(value); } }; /** * Holds the color value for {@code circlePaint} before the {@code Paint} instance is created. */ private int circleColor = DEFAULT_CIRCLE_COLOR; private static final int DEFAULT_CIRCLE_PROGRESS_COLOR = Color.argb(235, 74, 138, 255); /** * Holds the color value for {@code circleProgressPaint} before the {@code Paint} instance is created. */ private int circleProgressColor = DEFAULT_CIRCLE_PROGRESS_COLOR; private static final int DEFAULT_POINTER_COLOR = Color.argb(235, 74, 138, 255); /** * Holds the color value for {@code pointerPaint} before the {@code Paint} instance is created. */ private int pointerColor = DEFAULT_POINTER_COLOR; private static final int DEFAULT_POINTER_HALO_COLOR = Color.argb(135, 74, 138, 255); /** * Holds the color value for {@code pointerHaloPaint} before the {@code Paint} instance is created. */ private int pointerHaloColor = DEFAULT_POINTER_HALO_COLOR; private static final int DEFAULT_POINTER_HALO_COLOR_ONTOUCH = Color.argb(135, 74, 138, 255); /** * Holds the color value for {@code pointerHaloPaint} before the {@code Paint} instance is created. */ private int pointerHaloColorOnTouch = DEFAULT_POINTER_HALO_COLOR_ONTOUCH; private static final int DEFAULT_CIRCLE_FILL_COLOR = Color.TRANSPARENT; /** * Holds the color value for {@code circleFillPaint} before the {@code Paint} instance is created. */ private int circleFillColor = DEFAULT_CIRCLE_FILL_COLOR; private static final int DEFAULT_POINTER_ALPHA = 135; /** * Holds the alpha value for {@code pointerHaloPaint}. */ private int pointerAlpha = DEFAULT_POINTER_ALPHA; private static final int DEFAULT_POINTER_ALPHA_ONTOUCH = 100; /** * Holds the OnTouch alpha value for {@code pointerHaloPaint}. */ private int pointerAlphaOnTouch = DEFAULT_POINTER_ALPHA_ONTOUCH; 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; /** * Used to scale the dp units to pixels */ private final float dpToPxScale = getResources().getDisplayMetrics().density; /** * Minimum touch target size in DP. 48dp is the Android design recommendation */ private static final float MIN_TOUCH_TARGET_DP = 48; /** * {@code Paint} instance used to draw the inactive circle. */ private Paint circlePaint; /** * {@code Paint} instance used to draw the circle fill. */ private Paint circleFillPaint; /** * {@code Paint} instance used to draw the active circle (represents progress). */ private Paint circleProgressPaint; /** * {@code Paint} instance used to draw the glow from the active circle. */ private Paint circleProgressGlowPaint; /** * {@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 pointerPaint; /** * {@code Paint} instance used to draw the halo of the pointer. * Note: The halo is the part that changes transparency. */ private Paint pointerHaloPaint; /** * {@code Paint} instance used to draw the border of the pointer, outside of the halo. */ private Paint pointerHaloBorderPaint; /** * The width of the circle (in pixels). */ private float circleStrokeWidth; /** * The X radius of the circle (in pixels). */ private float circleXRadius; /** * The Y radius of the circle (in pixels). */ private float circleYRadius; /** * The radius of the pointer (in pixels). */ protected float pointerRadius; /** * The width of the pointer halo (in pixels). */ private float pointerHaloWidth; /** * The width of the pointer halo border (in pixels). */ private float pointerHaloBorderWidth; /** * Start angle of the CircularSeekBar. * Note: If startAngle and endAngle are set to the same angle, 0.1 is subtracted * from the endAngle to make the circle function properly. */ private float startAngle; /** * End angle of the CircularSeekBar. * Note: If startAngle and endAngle are set to the same angle, 0.1 is subtracted * from the endAngle to make the circle function properly. */ private float endAngle; /** * {@code RectF} that represents the circle (or ellipse) of the seekbar. */ private RectF circleRectF = new RectF(); /** * Distance (in degrees) that the the circle/semi-circle makes up. * This amount represents the max of the circle in degrees. */ private float totalCircleDegrees; /** * Distance (in degrees) that the current progress makes up in the circle. */ private float progressDegrees; /** * {@code Path} used to draw the circle/semi-circle. */ protected Path circlePath; /** * {@code Path} used to draw the progress on the circle. */ private Path circleProgressPath; /** * Max value that this CircularSeekBar is representing. */ private int max; /** * Progress value that this CircularSeekBar is representing. */ private int progress; /** * 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 customRadii; /** * 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 maintainEqualCircle; /** * 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 moveOutsideCircle; /** * 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 userIsMovingPointer = false; /** * Represents the clockwise distance from {@code startAngle} to the touch angle. * Used when touching the CircularSeekBar. */ private float cwDistanceFromStart; /** * Represents the counter-clockwise distance from {@code startAngle} to the touch angle. * Used when touching the CircularSeekBar. */ private float ccwDistanceFromStart; /** * Represents the clockwise distance from {@code endAngle} to the touch angle. * Used when touching the CircularSeekBar. */ private float cwDistanceFromEnd; /** * Represents the counter-clockwise distance from {@code endAngle} 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 pointerPosition} to the touch angle. * Used when touching the CircularSeekBar. */ private float cwDistanceFromPointer; /** * Represents the counter-clockwise distance from {@code pointerPosition} 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 isMovingCW; /** * 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 circleWidth; /** * 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 circleHeight; /** * Represents the progress mark on the circle, in geometric degrees. * This is not provided by the user; it is calculated; */ private float pointerPosition; /** * Pointer position in terms of X and Y coordinates. */ protected float[] pointerPositionXY = new float[2]; private GestureDetectorCompat gestureDetector; private OnDoubleClickListener onArtClickListener; /** * Listener. */ private OnCircularSeekBarChangeListener onCircularSeekBarChangeListener; private boolean showPointer = false; private boolean enabled = true; private float darkenLevel; private OnLongClickListener onArtLongClickListener; 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); } /** * 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. */ protected void initAttributes(TypedArray attrArray) { circleXRadius = attrArray.getDimension(R.styleable.CircularSeekBar_circle_x_radius, DEFAULT_CIRCLE_X_RADIUS * dpToPxScale); circleYRadius = attrArray.getDimension(R.styleable.CircularSeekBar_circle_y_radius, DEFAULT_CIRCLE_Y_RADIUS * dpToPxScale); pointerRadius = attrArray.getDimension(R.styleable.CircularSeekBar_pointer_radius, DEFAULT_POINTER_RADIUS * dpToPxScale); pointerHaloWidth = attrArray.getDimension(R.styleable.CircularSeekBar_pointer_halo_width, DEFAULT_POINTER_HALO_WIDTH * dpToPxScale); pointerHaloBorderWidth = attrArray.getDimension(R.styleable.CircularSeekBar_pointer_halo_border_width, DEFAULT_POINTER_HALO_BORDER_WIDTH * dpToPxScale); circleStrokeWidth = attrArray.getDimension(R.styleable.CircularSeekBar_circle_stroke_width, DEFAULT_CIRCLE_STROKE_WIDTH * dpToPxScale); String tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_color); if (tempColor != null) { try { pointerColor = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { pointerColor = DEFAULT_POINTER_COLOR; } } tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_halo_color); if (tempColor != null) { try { pointerHaloColor = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { pointerHaloColor = DEFAULT_POINTER_HALO_COLOR; } } tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_halo_color_ontouch); if (tempColor != null) { try { pointerHaloColorOnTouch = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { pointerHaloColorOnTouch = DEFAULT_POINTER_HALO_COLOR_ONTOUCH; } } tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_color); if (tempColor != null) { try { circleColor = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { circleColor = DEFAULT_CIRCLE_COLOR; } } tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_progress_color); if (tempColor != null) { try { circleProgressColor = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { circleProgressColor = DEFAULT_CIRCLE_PROGRESS_COLOR; } } tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_fill); if (tempColor != null) { try { circleFillColor = Color.parseColor(tempColor); } catch (IllegalArgumentException e) { circleFillColor = DEFAULT_CIRCLE_FILL_COLOR; } } pointerAlpha = Color.alpha(pointerHaloColor); pointerAlphaOnTouch = attrArray.getInt(R.styleable.CircularSeekBar_pointer_alpha_ontouch, DEFAULT_POINTER_ALPHA_ONTOUCH); if (pointerAlphaOnTouch > 255 || pointerAlphaOnTouch < 0) { pointerAlphaOnTouch = DEFAULT_POINTER_ALPHA_ONTOUCH; } max = attrArray.getInt(R.styleable.CircularSeekBar_max, DEFAULT_MAX); progress = attrArray.getInt(R.styleable.CircularSeekBar_progress, DEFAULT_PROGRESS); customRadii = attrArray.getBoolean(R.styleable.CircularSeekBar_use_custom_radii, DEFAULT_USE_CUSTOM_RADII); maintainEqualCircle = attrArray.getBoolean(R.styleable.CircularSeekBar_maintain_equal_circle, DEFAULT_MAINTAIN_EQUAL_CIRCLE); moveOutsideCircle = attrArray.getBoolean(R.styleable.CircularSeekBar_move_outside_circle, DEFAULT_MOVE_OUTSIDE_CIRCLE); // Modulo 360 right now to avoid constant conversion startAngle = ((360f + (attrArray.getFloat((R.styleable.CircularSeekBar_start_angle), DEFAULT_START_ANGLE) % 360f)) % 360f); endAngle = ((360f + (attrArray.getFloat((R.styleable.CircularSeekBar_end_angle), DEFAULT_END_ANGLE) % 360f)) % 360f); if (MathUtils.floatEqual(startAngle, endAngle)) { //startAngle = startAngle + 1f; endAngle = endAngle - .1f; } final GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onSingleTapConfirmed(MotionEvent e) { if (onArtClickListener != null) { onArtClickListener.onSingleClick(); return true; } return false; } @Override public boolean onDoubleTap(MotionEvent e) { if (onArtClickListener != null) { onArtClickListener.onDoubleClick(); } return super.onDoubleTap(e); } @Override public void onLongPress(MotionEvent e) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); if (onArtLongClickListener != null) { onArtLongClickListener.onLongClick(CircularSeekBar.this); } } }; gestureDetector = new GestureDetectorCompat(getContext(), gestureListener); gestureDetector.setOnDoubleTapListener(gestureListener); } public void setOnArtClickListener(OnDoubleClickListener onArtClickListener) { this.onArtClickListener = onArtClickListener; } public void setOnArtLongClickListener(View.OnLongClickListener onArtLongClickListener) { this.onArtLongClickListener = onArtLongClickListener; } public boolean isShowPointer() { return showPointer; } public void setShowPointer(boolean showPointer) { this.showPointer = showPointer; requestLayout(); invalidate(); } /** * Initializes the {@code Paint} objects with the appropriate styles. */ protected void initPaints() { circlePaint = new Paint(); circlePaint.setAntiAlias(true); circlePaint.setDither(true); circlePaint.setColor(circleColor); circlePaint.setStrokeWidth(circleStrokeWidth); circlePaint.setStyle(Paint.Style.STROKE); circlePaint.setStrokeJoin(Paint.Join.ROUND); circlePaint.setStrokeCap(Paint.Cap.ROUND); circleFillPaint = new Paint(); circleFillPaint.setAntiAlias(true); circleFillPaint.setDither(true); circleFillPaint.setColor(circleFillColor); circleFillPaint.setStyle(Paint.Style.FILL); circleProgressPaint = new Paint(); circleProgressPaint.setAntiAlias(true); circleProgressPaint.setDither(true); circleProgressPaint.setColor(circleProgressColor); circleProgressPaint.setStrokeWidth(circleStrokeWidth); circleProgressPaint.setStyle(Paint.Style.STROKE); circleProgressGlowPaint = new Paint(); circleProgressGlowPaint.set(circleProgressPaint); if (!isInEditMode()) { circleProgressGlowPaint .setMaskFilter(new BlurMaskFilter((5f * dpToPxScale), BlurMaskFilter.Blur.NORMAL)); } pointerPaint = new Paint(); pointerPaint.setAntiAlias(true); pointerPaint.setDither(true); pointerPaint.setStyle(Paint.Style.FILL); pointerPaint.setColor(pointerColor); pointerPaint.setStrokeWidth(pointerRadius); pointerHaloPaint = new Paint(); pointerHaloPaint.set(pointerPaint); pointerHaloPaint.setColor(pointerHaloColor); pointerHaloPaint.setAlpha(pointerAlpha); pointerHaloPaint.setStrokeWidth(pointerRadius + pointerHaloWidth); pointerHaloBorderPaint = new Paint(); pointerHaloBorderPaint.set(pointerPaint); pointerHaloBorderPaint.setStrokeWidth(pointerHaloBorderWidth); pointerHaloBorderPaint.setStyle(Paint.Style.STROKE); } /** * Calculates the total degrees between startAngle and endAngle, and sets totalCircleDegrees * to this value. */ private void calculateTotalDegrees() { totalCircleDegrees = (360f - (startAngle - endAngle)) % 360f; // Length of the entire circle/arc if (totalCircleDegrees <= 0f) { totalCircleDegrees = 360f; } } /** * Calculate the degrees that the progress represents. Also called the sweep angle. * Sets progressDegrees to that value. */ private void calculateProgressDegrees() { progressDegrees = pointerPosition - startAngle; // Verified progressDegrees = (progressDegrees < 0 ? 360f + progressDegrees : progressDegrees); // Verified } /** * Calculate the pointer position (and the end of the progress arc) in degrees. * Sets pointerPosition to that value. */ private void calculatePointerAngle() { float progressPercent = ((float) progress / (float) max); pointerPosition = (progressPercent * totalCircleDegrees) + startAngle; pointerPosition = pointerPosition % 360f; } private void calculatePointerXYPosition() { PathMeasure pm = new PathMeasure(circleProgressPath, false); boolean returnValue = pm.getPosTan(pm.getLength(), pointerPositionXY, null); if (!returnValue) { pm = new PathMeasure(circlePath, false); returnValue = pm.getPosTan(0, pointerPositionXY, null); } } /** * Initialize the {@code Path} objects with the appropriate values. */ private void initPaths() { circlePath = new Path(); circlePath.addArc(circleRectF, startAngle, totalCircleDegrees); circleProgressPath = new Path(); circleProgressPath.addArc(circleRectF, startAngle, progressDegrees); } /** * Initialize the {@code RectF} objects with the appropriate values. */ private void initRects() { circleRectF.set(-circleWidth, -circleHeight, circleWidth, circleHeight); } @Override public void onDraw(@NonNull Canvas canvas) { super.onDraw(canvas); canvas.translate(this.getWidth() / 2f, this.getHeight() / 2f); canvas.drawPath(circlePath, circlePaint); canvas.drawPath(circleProgressPath, circleProgressGlowPaint); canvas.drawPath(circleProgressPath, circleProgressPaint); canvas.drawPath(circlePath, circleFillPaint); if (showPointer) { canvas.drawCircle(pointerPositionXY[0], pointerPositionXY[1], pointerRadius + pointerHaloWidth, pointerHaloPaint); canvas.drawCircle(pointerPositionXY[0], pointerPositionXY[1], pointerRadius, pointerPaint); if (userIsMovingPointer) { canvas.drawCircle(pointerPositionXY[0], pointerPositionXY[1], pointerRadius + pointerHaloWidth + (pointerHaloBorderWidth / 2f), pointerHaloBorderPaint); } } } /** * Get the progress of the CircularSeekBar. * * @return The progress of the CircularSeekBar. */ public int getProgress() { return Math.round((float) max * progressDegrees / totalCircleDegrees); } /** * 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 (this.progress != progress) { this.progress = progress; if (onCircularSeekBarChangeListener != null) { onCircularSeekBarChangeListener.onProgressChanged(this, progress, false); } recalculateAll(); invalidate(); } } private void setProgressBasedOnAngle(float angle) { pointerPosition = angle; calculateProgressDegrees(); progress = Math.round((float) max * progressDegrees / totalCircleDegrees); } private void recalculateAll() { calculateTotalDegrees(); calculatePointerAngle(); calculateProgressDegrees(); initRects(); initPaths(); calculatePointerXYPosition(); setCornerRadius(circleRectF.width() / 2f + circleStrokeWidth); } public void setProgressEnabled(boolean enabled) { this.enabled = enabled; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); if (maintainEqualCircle) { int min = Math.min(width, height); if (min == 0) { min = Math.max(width, height); } height = min; width = min; setMeasuredDimension(min, min); super.setMeasuredDimension(min, min); } else { setMeasuredDimension(width, height); super.setMeasuredDimension(width, height); } // Set the circle width and height based on the view for the moment circleHeight = (float) height / 2f - circleStrokeWidth / 2f; if (showPointer) { circleHeight -= pointerRadius - (pointerHaloBorderWidth * 1.5f); } circleWidth = (float) width / 2f - circleStrokeWidth / 2f; if (showPointer) { circleWidth -= pointerRadius - (pointerHaloBorderWidth * 1.5f); } // If it is not set to use custom if (customRadii) { // Check to make sure the custom radii are not out of the view. If they are, just use the view values if ((circleYRadius - circleStrokeWidth - pointerRadius - pointerHaloBorderWidth) < circleHeight) { circleHeight = circleYRadius - circleStrokeWidth - pointerRadius - (pointerHaloBorderWidth * 1.5f); } if ((circleXRadius - circleStrokeWidth - pointerRadius - pointerHaloBorderWidth) < circleWidth) { circleWidth = circleXRadius - circleStrokeWidth - pointerRadius - (pointerHaloBorderWidth * 1.5f); } } if (maintainEqualCircle) { // Applies regardless of how the values were determined float min = Math.min(circleHeight, circleWidth); circleHeight = min; circleWidth = 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 final float x = event.getX() - getWidth() / 2; final float y = event.getY() - getHeight() / 2; // Get the distance from the center of the circle in terms of x and y final float distanceX = circleRectF.centerX() - x; final float distanceY = circleRectF.centerY() - y; // Get the distance from the center of the circle in terms of a radius final float touchEventRadius = (float) Math.sqrt((Math.pow(distanceX, 2) + Math.pow(distanceY, 2))); final float minimumTouchTarget = MIN_TOUCH_TARGET_DP * dpToPxScale; // Convert minimum touch target into px final float additionalRadius; // Either uses the minimumTouchTarget size or larger if the ring/pointer is larger if (circleStrokeWidth < minimumTouchTarget) { // If the width is less than the minimumTouchTarget, use the minimumTouchTarget additionalRadius = minimumTouchTarget / 2; } else { additionalRadius = circleStrokeWidth / 2; // Otherwise use the width } final float innerRadius = Math.min(circleHeight, circleWidth) - additionalRadius; // Min inner radius of the circle, including the minimumTouchTarget or wheel width if (touchEventRadius <= innerRadius) { gestureDetector.onTouchEvent(event); userIsMovingPointer = false; return true; } if (!isEnabled() || !enabled) { return super.onTouchEvent(event); } final float outerRadius = Math.max(circleHeight, circleWidth) + additionalRadius; // Max outer radius of the circle, including the minimumTouchTarget or wheel width float touchAngle; touchAngle = (float) ((java.lang.Math.atan2(y, x) / Math.PI * 180) % 360); // Verified touchAngle = (touchAngle < 0 ? 360 + touchAngle : touchAngle); // Verified cwDistanceFromStart = touchAngle - startAngle; // Verified cwDistanceFromStart = (cwDistanceFromStart < 0 ? 360f + cwDistanceFromStart : cwDistanceFromStart); // Verified ccwDistanceFromStart = 360f - cwDistanceFromStart; // Verified cwDistanceFromEnd = touchAngle - endAngle; // 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) ((pointerRadius * 180) / (Math.PI * Math.max(circleHeight, circleWidth))); cwDistanceFromPointer = touchAngle - pointerPosition; 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(pointerPosition); lastCWDistanceFromStart = cwDistanceFromStart; isMovingCW = true; pointerHaloPaint.setAlpha(pointerAlphaOnTouch); pointerHaloPaint.setColor(pointerHaloColorOnTouch); recalculateAll(); invalidate(); if (onCircularSeekBarChangeListener != null) { onCircularSeekBarChangeListener.onStartTrackingTouch(this); } userIsMovingPointer = true; lockAtEnd = false; lockAtStart = false; } else if (cwDistanceFromStart > totalCircleDegrees) { // If the user is touching outside of the start AND end userIsMovingPointer = false; return false; } else if ((touchEventRadius >= innerRadius) && (touchEventRadius <= outerRadius)) { // If the user is touching near the circle setProgressBasedOnAngle(touchAngle); lastCWDistanceFromStart = cwDistanceFromStart; isMovingCW = true; pointerHaloPaint.setAlpha(pointerAlphaOnTouch); pointerHaloPaint.setColor(pointerHaloColorOnTouch); recalculateAll(); invalidate(); if (onCircularSeekBarChangeListener != null) { onCircularSeekBarChangeListener.onStartTrackingTouch(this); onCircularSeekBarChangeListener.onProgressChanged(this, progress, true); } userIsMovingPointer = true; lockAtEnd = false; lockAtStart = false; } else { // If the user is not touching near the circle userIsMovingPointer = false; return false; } break; case MotionEvent.ACTION_MOVE: if (userIsMovingPointer) { if (lastCWDistanceFromStart < cwDistanceFromStart) { if ((cwDistanceFromStart - lastCWDistanceFromStart) > 180f && !isMovingCW) { lockAtStart = true; lockAtEnd = false; } else { isMovingCW = true; } } else { if ((lastCWDistanceFromStart - cwDistanceFromStart) > 180f && isMovingCW) { lockAtEnd = true; lockAtStart = false; } else { isMovingCW = false; } } if (lockAtStart && isMovingCW) { lockAtStart = false; } if (lockAtEnd && !isMovingCW) { lockAtEnd = false; } if (lockAtStart && !isMovingCW && (ccwDistanceFromStart > 90)) { lockAtStart = false; } if (lockAtEnd && isMovingCW && (cwDistanceFromEnd > 90)) { lockAtEnd = false; } // Fix for passing the end of a semi-circle quickly if (!lockAtEnd && cwDistanceFromStart > totalCircleDegrees && isMovingCW && lastCWDistanceFromStart < totalCircleDegrees) { lockAtEnd = true; } if (lockAtStart && lockEnabled) { // TODO: Add a check if progress is already 0, in which case don't call the listener progress = 0; recalculateAll(); invalidate(); if (onCircularSeekBarChangeListener != null) { onCircularSeekBarChangeListener.onProgressChanged(this, progress, true); } } else if (lockAtEnd && lockEnabled) { progress = max; recalculateAll(); invalidate(); if (onCircularSeekBarChangeListener != null) { onCircularSeekBarChangeListener.onProgressChanged(this, progress, true); } } else if ((moveOutsideCircle) || (touchEventRadius <= outerRadius)) { if (!(cwDistanceFromStart > totalCircleDegrees)) { setProgressBasedOnAngle(touchAngle); } recalculateAll(); invalidate(); if (onCircularSeekBarChangeListener != null) { onCircularSeekBarChangeListener.onProgressChanged(this, progress, true); } } else { break; } lastCWDistanceFromStart = cwDistanceFromStart; } else { return false; } break; case MotionEvent.ACTION_UP: pointerHaloPaint.setAlpha(pointerAlpha); pointerHaloPaint.setColor(pointerHaloColor); if (userIsMovingPointer) { userIsMovingPointer = false; invalidate(); if (onCircularSeekBarChangeListener != null) { onCircularSeekBarChangeListener.onStopTrackingTouch(this); } } else { return false; } break; case MotionEvent.ACTION_CANCEL: // Used when the parent view intercepts touches for things like scrolling pointerHaloPaint.setAlpha(pointerAlpha); pointerHaloPaint.setColor(pointerHaloColor); userIsMovingPointer = 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(); initInnerImage(); } private void initInnerImage() { } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); Bundle state = new Bundle(); state.putParcelable("PARENT", superState); state.putInt("MAX", max); state.putInt("PROGRESS", progress); state.putInt("circleColor", circleColor); state.putInt("circleProgressColor", circleProgressColor); state.putInt("pointerColor", pointerColor); state.putInt("pointerHaloColor", pointerHaloColor); state.putInt("pointerHaloColorOnTouch", pointerHaloColorOnTouch); state.putInt("pointerAlpha", pointerAlpha); state.putInt("pointerAlphaOnTouch", pointerAlphaOnTouch); return state; } @Override protected void onRestoreInstanceState(Parcelable state) { Bundle savedState = (Bundle) state; Parcelable superState = savedState.getParcelable("PARENT"); super.onRestoreInstanceState(superState); max = savedState.getInt("MAX"); progress = savedState.getInt("PROGRESS"); circleColor = savedState.getInt("circleColor"); circleProgressColor = savedState.getInt("circleProgressColor"); pointerColor = savedState.getInt("pointerColor"); pointerHaloColor = savedState.getInt("pointerHaloColor"); pointerHaloColorOnTouch = savedState.getInt("pointerHaloColorOnTouch"); pointerAlpha = savedState.getInt("pointerAlpha"); pointerAlphaOnTouch = savedState.getInt("pointerAlphaOnTouch"); initPaints(); recalculateAll(); } public void setOnSeekBarChangeListener(OnCircularSeekBarChangeListener l) { onCircularSeekBarChangeListener = l; } public void setCircleStrokeWidth(float circleStrokeWidth) { this.circleStrokeWidth = circleStrokeWidth; circlePaint.setStrokeWidth(circleStrokeWidth); circleProgressPaint.setStrokeWidth(circleStrokeWidth); requestLayout(); invalidate(); } /** * Gets the circle color. * * @return An integer color value for the circle */ public int getCircleColor() { return circleColor; } /** * Sets the circle color. * * @param color the color of the circle */ public void setCircleColor(int color) { circleColor = color; circlePaint.setColor(circleColor); invalidate(); } /** * Gets the circle progress color. * * @return An integer color value for the circle progress */ public int getCircleProgressColor() { return circleProgressColor; } /** * Sets the circle progress color. * * @param color the color of the circle progress */ public void setCircleProgressColor(int color) { circleProgressColor = color; circleProgressPaint.setColor(circleProgressColor); invalidate(); } /** * Gets the pointer color. * * @return An integer color value for the pointer */ public int getPointerColor() { return pointerColor; } /** * Sets the pointer color. * * @param color the color of the pointer */ public void setPointerColor(int color) { pointerColor = color; pointerPaint.setColor(pointerColor); invalidate(); } /** * Gets the pointer halo color. * * @return An integer color value for the pointer halo */ public int getPointerHaloColor() { return pointerHaloColor; } /** * Sets the pointer halo color. * * @param color the color of the pointer halo */ public void setPointerHaloColor(int color) { pointerHaloColor = color; pointerHaloPaint.setColor(pointerHaloColor); invalidate(); } /** * Gets the pointer alpha value. * * @return An integer alpha value for the pointer (0..255) */ public int getPointerAlpha() { return pointerAlpha; } /** * Sets the pointer alpha. * * @param alpha the alpha of the pointer */ public void setPointerAlpha(int alpha) { if (alpha >= 0 && alpha <= 255) { pointerAlpha = alpha; pointerHaloPaint.setAlpha(pointerAlpha); invalidate(); } } /** * Gets the pointer alpha value when touched. * * @return An integer alpha value for the pointer (0..255) when touched */ public int getPointerAlphaOnTouch() { return pointerAlphaOnTouch; } /** * 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) { pointerAlphaOnTouch = alpha; } } /** * Gets the circle fill color. * * @return An integer color value for the circle fill */ public int getCircleFillColor() { return circleFillColor; } /** * Sets the circle fill color. * * @param color the color of the circle fill */ public void setCircleFillColor(int color) { circleFillColor = color; circleFillPaint.setColor(circleFillColor); invalidate(); } /** * Get the current max of the CircularSeekBar. * * @return Synchronized integer value of the max. */ public synchronized int getMax() { return max; } /** * 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 <= progress) { progress = 0; // If the new max is less than current progress, set progress to zero if (onCircularSeekBarChangeListener != null) { onCircularSeekBarChangeListener.onProgressChanged(this, progress, false); } } this.max = max; recalculateAll(); invalidate(); } } public void setDarkenLevel(float darkenLevel) { this.darkenLevel = darkenLevel; if (darkenLevel < 0f || darkenLevel > 1f) { throw new IllegalArgumentException("darkenLevel must be between 0 and 1"); } super.setColorFilter(ColorUtils.injectAlpha(darkenLevel, Color.BLACK), PorterDuff.Mode.DARKEN); } public float getDarkenLevel() { return darkenLevel; } /** * Listener for the CircularSeekBar. Implements the same methods as the normal OnSeekBarChangeListener. */ public interface OnCircularSeekBarChangeListener { void onProgressChanged(CircularSeekBar circularSeekBar, int progress, boolean fromUser); void onStopTrackingTouch(CircularSeekBar seekBar); void onStartTrackingTouch(CircularSeekBar seekBar); } }