/* * Copyright (C) 2015 Basil Miller * * 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. */ package devlight.io.library; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.SweepGradient; import android.graphics.Typeface; import android.os.Build; import android.support.annotation.FloatRange; import android.support.v4.view.ViewCompat; import android.text.TextPaint; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * Created by GIGAMOLE on 04.03.2016. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class ArcProgressStackView extends View { // Default values private final static float DEFAULT_START_ANGLE = 270.0F; private final static float DEFAULT_SWEEP_ANGLE = 360.0F; private final static float DEFAULT_DRAW_WIDTH_FRACTION = 0.7F; private final static float DEFAULT_MODEL_OFFSET = 5.0F; private final static float DEFAULT_SHADOW_RADIUS = 30.0F; private final static float DEFAULT_SHADOW_DISTANCE = 15.0F; private final static float DEFAULT_SHADOW_ANGLE = 90.0F; private final static int DEFAULT_ANIMATION_DURATION = 350; private final static int DEFAULT_ACTION_MOVE_ANIMATION_DURATION = 150; // Max and min progress values private final static float MAX_PROGRESS = 100.0F; private final static float MIN_PROGRESS = 0.0F; // Max and min fraction values private final static float MAX_FRACTION = 1.0F; private final static float MIN_FRACTION = 0.0F; // Max and min end angle private final static float MAX_ANGLE = 360.0F; private final static float MIN_ANGLE = 0.0F; // Min shadow private final static float MIN_SHADOW = 0.0F; // Action move constants private final static float POSITIVE_ANGLE = 90.0F; private final static float NEGATIVE_ANGLE = 270.0F; private final static int POSITIVE_SLICE = 1; private final static int NEGATIVE_SLICE = -1; private final static int DEFAULT_SLICE = 0; private final static int ANIMATE_ALL_INDEX = -2; private final static int DISABLE_ANIMATE_INDEX = -1; // Default colors private final static int DEFAULT_SHADOW_COLOR = Color.parseColor("#8C000000"); // Start and end angles private float mStartAngle; private float mSweepAngle; // Progress models private List<Model> mModels = new ArrayList<>(); // Progress and text paints private final Paint mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG) { { setDither(true); setStyle(Style.STROKE); } }; private final TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG) { { setDither(true); setTextAlign(Align.LEFT); } }; private final Paint mLevelPaint = new Paint(Paint.ANTI_ALIAS_FLAG) { { setDither(true); setStyle(Paint.Style.FILL_AND_STROKE); setPathEffect(new CornerPathEffect(0.5F)); } }; // ValueAnimator and interpolator for progress animating private final ValueAnimator mProgressAnimator = new ValueAnimator(); private ValueAnimator.AnimatorListener mAnimatorListener; private ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener; private Interpolator mInterpolator; private int mAnimationDuration; private float mAnimatedFraction; // Square size of view private int mSize; // Offsets for handling and radius of progress models private float mProgressModelSize; private float mProgressModelOffset; private float mDrawWidthFraction; private float mDrawWidthDimension; // Shadow variables private float mShadowRadius; private float mShadowDistance; private float mShadowAngle; // Boolean variables private boolean mIsAnimated; private boolean mIsShadowed; private boolean mIsRounded; private boolean mIsDragged; private boolean mIsModelBgEnabled; private boolean mIsLeveled; // Colors private int mShadowColor; private int mTextColor; private int mPreviewModelBgColor; // Action move variables private int mActionMoveModelIndex = DISABLE_ANIMATE_INDEX; private int mActionMoveLastSlice = 0; private int mActionMoveSliceCounter; private boolean mIsActionMoved; // Text typeface private Typeface mTypeface; // Indicator orientation private IndicatorOrientation mIndicatorOrientation; // Is >= VERSION_CODES.HONEYCOMB private boolean mIsFeaturesAvailable; public ArcProgressStackView(final Context context) { this(context, null); } public ArcProgressStackView(final Context context, final AttributeSet attrs) { this(context, attrs, 0); } public ArcProgressStackView(final Context context, final AttributeSet attrs, final int defStyleAttr) { super(context, attrs, defStyleAttr); // Init CPSV // Always draw setWillNotDraw(false); setLayerType(LAYER_TYPE_SOFTWARE, null); ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, null); // Detect if features available mIsFeaturesAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; // Retrieve attributes from xml final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ArcProgressStackView); try { setIsAnimated( typedArray.getBoolean(R.styleable.ArcProgressStackView_apsv_animated, true) ); setIsShadowed( typedArray.getBoolean(R.styleable.ArcProgressStackView_apsv_shadowed, true) ); setIsRounded( typedArray.getBoolean(R.styleable.ArcProgressStackView_apsv_rounded, false) ); setIsDragged( typedArray.getBoolean(R.styleable.ArcProgressStackView_apsv_dragged, false) ); setIsLeveled( typedArray.getBoolean(R.styleable.ArcProgressStackView_apsv_leveled, false) ); setTypeface( typedArray.getString(R.styleable.ArcProgressStackView_apsv_typeface) ); setTextColor( typedArray.getColor( R.styleable.ArcProgressStackView_apsv_text_color, Color.WHITE ) ); setShadowRadius( typedArray.getDimension( R.styleable.ArcProgressStackView_apsv_shadow_radius, DEFAULT_SHADOW_RADIUS ) ); setShadowDistance( typedArray.getDimension( R.styleable.ArcProgressStackView_apsv_shadow_distance, DEFAULT_SHADOW_DISTANCE ) ); setShadowAngle( typedArray.getInteger( R.styleable.ArcProgressStackView_apsv_shadow_angle, (int) DEFAULT_SHADOW_ANGLE ) ); setShadowColor( typedArray.getColor( R.styleable.ArcProgressStackView_apsv_shadow_color, DEFAULT_SHADOW_COLOR ) ); setAnimationDuration( typedArray.getInteger( R.styleable.ArcProgressStackView_apsv_animation_duration, DEFAULT_ANIMATION_DURATION ) ); setStartAngle( typedArray.getInteger( R.styleable.ArcProgressStackView_apsv_start_angle, (int) DEFAULT_START_ANGLE ) ); setSweepAngle( typedArray.getInteger( R.styleable.ArcProgressStackView_apsv_sweep_angle, (int) DEFAULT_SWEEP_ANGLE ) ); setProgressModelOffset( typedArray.getDimension( R.styleable.ArcProgressStackView_apsv_model_offset, DEFAULT_MODEL_OFFSET ) ); setModelBgEnabled( typedArray.getBoolean( R.styleable.ArcProgressStackView_apsv_model_bg_enabled, false ) ); // Set orientation final int orientationOrdinal = typedArray.getInt(R.styleable.ArcProgressStackView_apsv_indicator_orientation, 0); setIndicatorOrientation( orientationOrdinal == 0 ? IndicatorOrientation.VERTICAL : IndicatorOrientation.HORIZONTAL ); // Retrieve interpolator Interpolator interpolator = null; try { final int interpolatorId = typedArray.getResourceId( R.styleable.ArcProgressStackView_apsv_interpolator, 0 ); interpolator = interpolatorId == 0 ? null : AnimationUtils.loadInterpolator(context, interpolatorId); } catch (Resources.NotFoundException exception) { interpolator = null; exception.printStackTrace(); } finally { setInterpolator(interpolator); } // Set animation info if is available if (mIsFeaturesAvailable) { mProgressAnimator.setFloatValues(MIN_FRACTION, MAX_FRACTION); mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(final ValueAnimator animation) { mAnimatedFraction = (float) animation.getAnimatedValue(); if (mAnimatorUpdateListener != null) mAnimatorUpdateListener.onAnimationUpdate(animation); postInvalidate(); } }); } // Check whether draw width dimension or fraction if (typedArray.hasValue(R.styleable.ArcProgressStackView_apsv_draw_width)) { final TypedValue drawWidth = new TypedValue(); typedArray.getValue(R.styleable.ArcProgressStackView_apsv_draw_width, drawWidth); if (drawWidth.type == TypedValue.TYPE_DIMENSION) setDrawWidthDimension( drawWidth.getDimension(context.getResources().getDisplayMetrics()) ); else setDrawWidthFraction(drawWidth.getFraction(MAX_FRACTION, MAX_FRACTION)); } else setDrawWidthFraction(DEFAULT_DRAW_WIDTH_FRACTION); // Set preview models if (isInEditMode()) { String[] preview = null; try { final int previewId = typedArray.getResourceId( R.styleable.ArcProgressStackView_apsv_preview_colors, 0 ); preview = previewId == 0 ? null : typedArray.getResources().getStringArray(previewId); } catch (Exception exception) { preview = null; exception.printStackTrace(); } finally { if (preview == null) preview = typedArray.getResources().getStringArray(R.array.default_preview); final Random random = new Random(); for (String previewColor : preview) mModels.add( new Model("", random.nextInt((int) MAX_PROGRESS), Color.parseColor(previewColor)) ); measure(mSize, mSize); } // Set preview model bg color mPreviewModelBgColor = typedArray.getColor( R.styleable.ArcProgressStackView_apsv_preview_bg, Color.LTGRAY ); } } finally { typedArray.recycle(); } } public ValueAnimator getProgressAnimator() { return mProgressAnimator; } public long getAnimationDuration() { return mAnimationDuration; } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setAnimationDuration(final long animationDuration) { mAnimationDuration = (int) animationDuration; mProgressAnimator.setDuration(animationDuration); } public ValueAnimator.AnimatorListener getAnimatorListener() { return mAnimatorListener; } public void setAnimatorListener(final ValueAnimator.AnimatorListener animatorListener) { if (mAnimatorListener != null) mProgressAnimator.removeListener(mAnimatorListener); mAnimatorListener = animatorListener; mProgressAnimator.addListener(animatorListener); } public ValueAnimator.AnimatorUpdateListener getAnimatorUpdateListener() { return mAnimatorUpdateListener; } public void setAnimatorUpdateListener(final ValueAnimator.AnimatorUpdateListener animatorUpdateListener) { mAnimatorUpdateListener = animatorUpdateListener; } public float getStartAngle() { return mStartAngle; } @SuppressLint("SupportAnnotationUsage") @FloatRange public void setStartAngle(@FloatRange(from = MIN_ANGLE, to = MAX_ANGLE) final float startAngle) { mStartAngle = Math.max(MIN_ANGLE, Math.min(startAngle, MAX_ANGLE)); postInvalidate(); } public float getSweepAngle() { return mSweepAngle; } @SuppressLint("SupportAnnotationUsage") @FloatRange public void setSweepAngle(@FloatRange(from = MIN_ANGLE, to = MAX_ANGLE) final float sweepAngle) { mSweepAngle = Math.max(MIN_ANGLE, Math.min(sweepAngle, MAX_ANGLE)); postInvalidate(); } public List<Model> getModels() { return mModels; } public void setModels(final List<Model> models) { mModels.clear(); mModels = models; requestLayout(); } public int getSize() { return mSize; } public float getProgressModelSize() { return mProgressModelSize; } public boolean isAnimated() { return mIsAnimated; } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setIsAnimated(final boolean isAnimated) { mIsAnimated = mIsFeaturesAvailable && isAnimated; } public boolean isShadowed() { return mIsShadowed; } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setIsShadowed(final boolean isShadowed) { mIsShadowed = mIsFeaturesAvailable && isShadowed; resetShadowLayer(); requestLayout(); } public boolean isModelBgEnabled() { return mIsModelBgEnabled; } public void setModelBgEnabled(final boolean modelBgEnabled) { mIsModelBgEnabled = modelBgEnabled; postInvalidate(); } public boolean isRounded() { return mIsRounded; } public void setIsRounded(final boolean isRounded) { mIsRounded = isRounded; if (mIsRounded) { mProgressPaint.setStrokeCap(Paint.Cap.ROUND); mProgressPaint.setStrokeJoin(Paint.Join.ROUND); } else { mProgressPaint.setStrokeCap(Paint.Cap.BUTT); mProgressPaint.setStrokeJoin(Paint.Join.MITER); } requestLayout(); } public boolean isDragged() { return mIsDragged; } public void setIsDragged(final boolean isDragged) { mIsDragged = isDragged; } public boolean isLeveled() { return mIsLeveled; } public void setIsLeveled(final boolean isLeveled) { mIsLeveled = mIsFeaturesAvailable && isLeveled; requestLayout(); } public Interpolator getInterpolator() { return (Interpolator) mProgressAnimator.getInterpolator(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setInterpolator(final Interpolator interpolator) { mInterpolator = interpolator == null ? new AccelerateDecelerateInterpolator() : interpolator; mProgressAnimator.setInterpolator(mInterpolator); } public float getProgressModelOffset() { return mProgressModelOffset; } public void setProgressModelOffset(final float progressModelOffset) { mProgressModelOffset = progressModelOffset; requestLayout(); } public float getDrawWidthFraction() { return mDrawWidthFraction; } @SuppressLint("SupportAnnotationUsage") @FloatRange public void setDrawWidthFraction(@FloatRange(from = MIN_FRACTION, to = MAX_FRACTION) final float drawWidthFraction) { // Divide by half for radius and reset mDrawWidthFraction = Math.max(MIN_FRACTION, Math.min(drawWidthFraction, MAX_FRACTION)) * 0.5F; mDrawWidthDimension = MIN_FRACTION; requestLayout(); } public float getDrawWidthDimension() { return mDrawWidthDimension; } public void setDrawWidthDimension(final float drawWidthDimension) { mDrawWidthFraction = MIN_FRACTION; mDrawWidthDimension = drawWidthDimension; requestLayout(); } public float getShadowDistance() { return mShadowDistance; } public void setShadowDistance(final float shadowDistance) { mShadowDistance = shadowDistance; resetShadowLayer(); requestLayout(); } public float getShadowAngle() { return mShadowAngle; } @SuppressLint("SupportAnnotationUsage") @FloatRange public void setShadowAngle(@FloatRange(from = MIN_ANGLE, to = MAX_ANGLE) final float shadowAngle) { mShadowAngle = Math.max(MIN_ANGLE, Math.min(shadowAngle, MAX_ANGLE)); resetShadowLayer(); requestLayout(); } public float getShadowRadius() { return mShadowRadius; } public void setShadowRadius(final float shadowRadius) { mShadowRadius = shadowRadius > MIN_SHADOW ? shadowRadius : MIN_SHADOW; resetShadowLayer(); requestLayout(); } public int getShadowColor() { return mShadowColor; } public void setShadowColor(final int shadowColor) { mShadowColor = shadowColor; resetShadowLayer(); postInvalidate(); } public int getTextColor() { return mTextColor; } public void setTextColor(final int textColor) { mTextColor = textColor; mTextPaint.setColor(textColor); postInvalidate(); } public Typeface getTypeface() { return mTypeface; } public void setTypeface(final String typeface) { Typeface tempTypeface; try { if (isInEditMode()) return; tempTypeface = Typeface.createFromAsset(getContext().getAssets(), typeface); } catch (Exception e) { tempTypeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL); } setTypeface(tempTypeface); } public void setTypeface(final Typeface typeface) { mTypeface = typeface; mTextPaint.setTypeface(typeface); postInvalidate(); } public IndicatorOrientation getIndicatorOrientation() { return mIndicatorOrientation; } public void setIndicatorOrientation(final IndicatorOrientation indicatorOrientation) { mIndicatorOrientation = indicatorOrientation; } // Reset shadow layer private void resetShadowLayer() { if (isInEditMode()) return; final float newDx = (float) ((mShadowDistance) * Math.cos((mShadowAngle - mStartAngle) / 180.0F * Math.PI)); final float newDy = (float) ((mShadowDistance) * Math.sin((mShadowAngle - mStartAngle) / 180.0F * Math.PI)); if (mIsShadowed) mProgressPaint.setShadowLayer(mShadowRadius, newDx, newDy, mShadowColor); else mProgressPaint.clearShadowLayer(); } // Set start elevation pin if gradient round progress private void setLevelShadowLayer() { if (isInEditMode()) return; if (mIsShadowed || mIsLeveled) { final float shadowOffset = mShadowRadius * 0.5f; mLevelPaint.setShadowLayer( shadowOffset, 0.0f, -shadowOffset, adjustColorAlpha(mShadowColor, 0.5f) ); } else mLevelPaint.clearShadowLayer(); } // Adjust color alpha(used for shadow reduce) private int adjustColorAlpha(final int color, final float factor) { return Color.argb( Math.round(Color.alpha(color) * factor), Color.red(color), Color.green(color), Color.blue(color) ); } // Animate progress public void animateProgress() { if (!mIsAnimated || mProgressAnimator == null) return; if (mProgressAnimator.isRunning()) { if (mAnimatorListener != null) mProgressAnimator.removeListener(mAnimatorListener); mProgressAnimator.cancel(); } // Set to animate all models mActionMoveModelIndex = ANIMATE_ALL_INDEX; mProgressAnimator.setDuration(mAnimationDuration); mProgressAnimator.setInterpolator(mInterpolator); if (mAnimatorListener != null) { mProgressAnimator.removeListener(mAnimatorListener); mProgressAnimator.addListener(mAnimatorListener); } mProgressAnimator.start(); } // Animate progress private void animateActionMoveProgress() { if (!mIsAnimated || mProgressAnimator == null) return; if (mProgressAnimator.isRunning()) return; mProgressAnimator.setDuration(DEFAULT_ACTION_MOVE_ANIMATION_DURATION); mProgressAnimator.setInterpolator(null); if (mAnimatorListener != null) mProgressAnimator.removeListener(mAnimatorListener); mProgressAnimator.start(); } // Get the angle of action move model private float getActionMoveAngle(final float x, final float y) { //Get radius final float radius = mSize * 0.5F; // Get degrees without offset float degrees = (float) ((Math.toDegrees(Math.atan2(y - radius, x - radius)) + 360.0F) % 360.0F); if (degrees < 0) degrees += 2.0F * Math.PI; // Get point with offset relative to start angle final float newActionMoveX = (float) (radius * Math.cos((degrees - mStartAngle) / 180.0F * Math.PI)); final float newActionMoveY = (float) (radius * Math.sin((degrees - mStartAngle) / 180.0F * Math.PI)); // Set new angle with offset degrees = (float) ((Math.toDegrees(Math.atan2(newActionMoveY, newActionMoveX)) + 360.0F) % 360.0F); if (degrees < 0) degrees += 2.0F * Math.PI; return degrees; } private void handleActionMoveModel(final MotionEvent event) { if (mActionMoveModelIndex == DISABLE_ANIMATE_INDEX) return; // Get current move angle float currentAngle = getActionMoveAngle(event.getX(), event.getY()); // Check if angle in slice zones final int actionMoveCurrentSlice; if (currentAngle > MIN_ANGLE && currentAngle < POSITIVE_ANGLE) actionMoveCurrentSlice = POSITIVE_SLICE; else if (currentAngle > NEGATIVE_ANGLE && currentAngle < MAX_ANGLE) actionMoveCurrentSlice = NEGATIVE_SLICE; else actionMoveCurrentSlice = DEFAULT_SLICE; // Check for handling counter if (actionMoveCurrentSlice != 0 && ((mActionMoveLastSlice == NEGATIVE_SLICE && actionMoveCurrentSlice == POSITIVE_SLICE) || (actionMoveCurrentSlice == NEGATIVE_SLICE && mActionMoveLastSlice == POSITIVE_SLICE))) { if (mActionMoveLastSlice == NEGATIVE_SLICE) mActionMoveSliceCounter++; else mActionMoveSliceCounter--; // Limit counter for 1 and -1, we don`t need take the race if (mActionMoveSliceCounter > 1) mActionMoveSliceCounter = 1; else if (mActionMoveSliceCounter < -1) mActionMoveSliceCounter = -1; } mActionMoveLastSlice = actionMoveCurrentSlice; // Set total traveled angle float actionMoveTotalAngle = currentAngle + (MAX_ANGLE * mActionMoveSliceCounter); final Model model = mModels.get(mActionMoveModelIndex); // Check whether traveled angle out of limit if (actionMoveTotalAngle < MIN_ANGLE || actionMoveTotalAngle > MAX_ANGLE) { actionMoveTotalAngle = actionMoveTotalAngle > MAX_ANGLE ? MAX_ANGLE + 1.0F : -1.0F; currentAngle = actionMoveTotalAngle; } // Set model progress and invalidate float touchProgress = Math.round(MAX_PROGRESS / mSweepAngle * currentAngle); model.setProgress(touchProgress); } @Override public boolean onTouchEvent(final MotionEvent event) { if (!mIsDragged) return super.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mActionMoveModelIndex = DISABLE_ANIMATE_INDEX; // Get current move angle and check whether touched angle is in sweep angle zone float currentAngle = getActionMoveAngle(event.getX(), event.getY()); if (currentAngle > mSweepAngle && currentAngle < MAX_ANGLE) break; for (int i = 0; i < mModels.size(); i++) { final Model model = mModels.get(i); // Check if our model contains touch points if (model.mBounds.contains(event.getX(), event.getY())) { // Check variables for handle touch in progress model zone float modelRadius = model.mBounds.width() * 0.5F; float modelOffset = mProgressModelSize * 0.5F; float mainRadius = mSize * 0.5F; // Get distance between 2 points final float distance = (float) Math.sqrt(Math.pow(event.getX() - mainRadius, 2) + Math.pow(event.getY() - mainRadius, 2)); if (distance > modelRadius - modelOffset && distance < modelRadius + modelOffset) { mActionMoveModelIndex = i; mIsActionMoved = true; handleActionMoveModel(event); animateActionMoveProgress(); } } } break; case MotionEvent.ACTION_MOVE: if (mActionMoveModelIndex == DISABLE_ANIMATE_INDEX && !mIsActionMoved) break; if (mProgressAnimator.isRunning()) break; handleActionMoveModel(event); postInvalidate(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: default: // Reset values mActionMoveLastSlice = DEFAULT_SLICE; mActionMoveSliceCounter = 0; mIsActionMoved = false; break; } // If we have parent, so requestDisallowInterceptTouchEvent if (event.getAction() == MotionEvent.ACTION_MOVE && getParent() != null) getParent().requestDisallowInterceptTouchEvent(true); return true; } @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { // Get measured sizes final int width = MeasureSpec.getSize(widthMeasureSpec); final int height = MeasureSpec.getSize(heightMeasureSpec); // Get size for square dimension if (width > height) mSize = height; else mSize = width; // Get progress offsets final float divider = mDrawWidthFraction == 0 ? mDrawWidthDimension : mSize * mDrawWidthFraction; mProgressModelSize = divider / mModels.size(); final float paintOffset = mProgressModelSize * 0.5F; final float shadowOffset = mIsShadowed ? (mShadowRadius + mShadowDistance) : 0.0F; // Set bound with offset for models for (int i = 0; i < mModels.size(); i++) { final Model model = mModels.get(i); final float modelOffset = (mProgressModelSize * i) + (paintOffset + shadowOffset) - (mProgressModelOffset * i); // Set bounds to progress model.mBounds.set( modelOffset, modelOffset, mSize - modelOffset, mSize - modelOffset ); // Set sweep gradient shader if (model.getColors() != null) model.mSweepGradient = new SweepGradient( model.mBounds.centerX(), model.mBounds.centerY(), model.getColors(), null ); } // Set square measured dimension setMeasuredDimension(mSize, mSize); } @Override protected void onDraw(final Canvas canvas) { super.onDraw(canvas); // Save and rotate to start angle canvas.save(); final float radius = mSize * 0.5F; canvas.rotate(mStartAngle, radius, radius); // Draw all of progress for (int i = 0; i < mModels.size(); i++) { final Model model = mModels.get(i); // Get progress for current model float progressFraction = mIsAnimated && !isInEditMode() ? (model.mLastProgress + (mAnimatedFraction * (model.getProgress() - model.mLastProgress))) / MAX_PROGRESS : model.getProgress() / MAX_PROGRESS; if (i != mActionMoveModelIndex && mActionMoveModelIndex != ANIMATE_ALL_INDEX) progressFraction = model.getProgress() / MAX_PROGRESS; final float progress = progressFraction * mSweepAngle; // Check if model have gradient final boolean isGradient = model.getColors() != null; // Set width of progress mProgressPaint.setStrokeWidth(mProgressModelSize); // Set model arc progress model.mPath.reset(); model.mPath.addArc(model.mBounds, 0.0F, progress); // Draw gradient progress or solid resetShadowLayer(); mProgressPaint.setShader(null); mProgressPaint.setStyle(Paint.Style.STROKE); if (mIsModelBgEnabled) { //noinspection ResourceAsColor mProgressPaint.setColor(isInEditMode() ? mPreviewModelBgColor : model.getBgColor()); canvas.drawArc(model.mBounds, 0.0F, mSweepAngle, false, mProgressPaint); if (!isInEditMode()) mProgressPaint.clearShadowLayer(); } // Check if gradient for draw shadow at first and then gradient progress if (isGradient) { if (!mIsModelBgEnabled) { canvas.drawPath(model.mPath, mProgressPaint); if (!isInEditMode()) mProgressPaint.clearShadowLayer(); } mProgressPaint.setShader(model.mSweepGradient); } else mProgressPaint.setColor(model.getColor()); // Here we draw main progress mProgressPaint.setAlpha(255); canvas.drawPath(model.mPath, mProgressPaint); // Preview mode if (isInEditMode()) continue; // Get model title bounds mTextPaint.setTextSize(mProgressModelSize * 0.5F); mTextPaint.getTextBounds( model.getTitle(), 0, model.getTitle().length(), model.mTextBounds ); // Draw title at start with offset final float titleHorizontalOffset = model.mTextBounds.height() * 0.5F; final float progressLength = (float) (Math.PI / 180.0F) * progress * model.mBounds.width() * 0.5F; final String title = (String) TextUtils.ellipsize( model.getTitle(), mTextPaint, progressLength - titleHorizontalOffset * 2, TextUtils.TruncateAt.END ); canvas.drawTextOnPath( title, model.mPath, mIsRounded ? 0.0F : titleHorizontalOffset, titleHorizontalOffset, mTextPaint ); // Get pos and tan at final path point model.mPathMeasure.setPath(model.mPath, false); model.mPathMeasure.getPosTan(model.mPathMeasure.getLength(), model.mPos, model.mTan); // Get title width final float titleWidth = model.mTextBounds.width(); // Create model progress like : 23% final String percentProgress = String.format("%d%%", (int) model.getProgress()); // Get progress text bounds mTextPaint.setTextSize(mProgressModelSize * 0.35f); mTextPaint.getTextBounds( percentProgress, 0, percentProgress.length(), model.mTextBounds ); // Get pos tan with end point offset and check whether the rounded corners for offset final float progressHorizontalOffset = mIndicatorOrientation == IndicatorOrientation.VERTICAL ? model.mTextBounds.height() * 0.5F : model.mTextBounds.width() * 0.5F; final float indicatorProgressOffset = (mIsRounded ? progressFraction : 1.0F) * (-progressHorizontalOffset - titleHorizontalOffset - (mIsRounded ? model.mTextBounds.height() * 2.0F : 0.0F)); model.mPathMeasure.getPosTan( model.mPathMeasure.getLength() + indicatorProgressOffset, model.mPos, mIndicatorOrientation == IndicatorOrientation.VERTICAL && !mIsRounded ? new float[2] : model.mTan ); // Check if there available place for indicator if ((titleWidth + model.mTextBounds.height() + titleHorizontalOffset * 2.0F) - indicatorProgressOffset < progressLength) { // Get rotate indicator progress angle for progress value float indicatorProgressAngle = (float) (Math.atan2(model.mTan[1], model.mTan[0]) * (180.0F / Math.PI)); // Get arc angle of progress indicator final float indicatorLengthProgressAngle = ((progressLength + indicatorProgressOffset) / (model.mBounds.width() * 0.5F)) * (float) (180.0F / Math.PI); // Detect progress indicator position : left or right and then rotate if (mIndicatorOrientation == IndicatorOrientation.VERTICAL) { // Get X point of arc angle progress indicator final float x = (float) (model.mBounds.width() * 0.5F * (Math.cos((indicatorLengthProgressAngle + mStartAngle) * Math.PI / 180.0F))) + model.mBounds.centerX(); indicatorProgressAngle += (x > radius) ? -90.0F : 90.0F; } else { // Get Y point of arc angle progress indicator final float y = (float) (model.mBounds.height() * 0.5F * (Math.sin((indicatorLengthProgressAngle + mStartAngle) * Math.PI / 180.0F))) + model.mBounds.centerY(); indicatorProgressAngle += (y > radius) ? 180.0F : 0.0F; } // Draw progress value canvas.save(); canvas.rotate(indicatorProgressAngle, model.mPos[0], model.mPos[1]); canvas.drawText( percentProgress, model.mPos[0] - model.mTextBounds.exactCenterX(), model.mPos[1] - model.mTextBounds.exactCenterY(), mTextPaint ); canvas.restore(); } // Check if gradient and have rounded corners, because we must to create elevation effect // for start progress corner if ((isGradient || mIsLeveled) && mIsRounded && progress != 0) { model.mPathMeasure.getPosTan(0.0F, model.mPos, model.mTan); // Set paint for overlay rounded gradient with shadow setLevelShadowLayer(); //noinspection ResourceAsColor mLevelPaint.setColor(isGradient ? model.getColors()[0] : model.getColor()); // Get bounds of start pump final float halfSize = mProgressModelSize * 0.5F; final RectF arcRect = new RectF( model.mPos[0] - halfSize, model.mPos[1] - halfSize, model.mPos[0] + halfSize, model.mPos[1] + halfSize + 2.0F ); canvas.drawArc(arcRect, 0.0F, -180.0F, true, mLevelPaint); } } // Restore after drawing canvas.restore(); } public static class Model { private String mTitle; private float mLastProgress; private float mProgress; private int mColor; private int mBgColor; private int[] mColors; private final RectF mBounds = new RectF(); private final Rect mTextBounds = new Rect(); private final Path mPath = new Path(); private SweepGradient mSweepGradient; private final PathMeasure mPathMeasure = new PathMeasure(); private final float[] mPos = new float[2]; private final float[] mTan = new float[2]; public Model(final String title, final float progress, final int color) { setTitle(title); setProgress(progress); setColor(color); } public Model(final String title, final float progress, final int[] colors) { setTitle(title); setProgress(progress); setColors(colors); } public Model(final String title, final float progress, final int bgColor, final int color) { setTitle(title); setProgress(progress); setColor(color); setBgColor(bgColor); } public Model(final String title, final float progress, final int bgColor, final int[] colors) { setTitle(title); setProgress(progress); setColors(colors); setBgColor(bgColor); } public String getTitle() { return mTitle; } public void setTitle(final String title) { mTitle = title; } public float getProgress() { return mProgress; } @FloatRange public void setProgress(@FloatRange(from = MIN_PROGRESS, to = MAX_PROGRESS) final float progress) { mLastProgress = mProgress; mProgress = (int) Math.max(MIN_PROGRESS, Math.min(progress, MAX_PROGRESS)); } public int getColor() { return mColor; } public void setColor(final int color) { mColor = color; } public int getBgColor() { return mBgColor; } public void setBgColor(final int bgColor) { mBgColor = bgColor; } public int[] getColors() { return mColors; } public void setColors(final int[] colors) { if (colors != null && colors.length >= 2) mColors = colors; else mColors = null; } } public enum IndicatorOrientation { HORIZONTAL, VERTICAL } }