devlight.io.library.ArcProgressStackView.java Source code

Java tutorial

Introduction

Here is the source code for devlight.io.library.ArcProgressStackView.java

Source

/*
 * 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
    }
}