1. 程式人生 > >Android 自定義View例項之進度圓環

Android 自定義View例項之進度圓環

自定義View的相關文章:

看下效果:
這裡寫圖片描述

實際效果呢要比這個gif好,大家執行看看吧。

我們可以把這個View分為幾個部分:

  1. 底部灰色的圓環
  2. 漸變、動態的圓弧,
  3. 圓弧上的箭頭
  4. 中間的文字

我們就一個一個的來實現,我本來想直接把最終的程式碼給大家的,可是我做的過程中遇到了好多細節問題,所以我詳細的把我實現的步驟也分享給大家。

底部灰色的圓環

底部灰色的圓環寬度和顏色可以通過屬性設定,所以在res/values下新建attrs.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RoundView">
        <!--圓環半徑-->
<attr name="radius" format="dimension"/> <!--圓環寬度--> <attr name="ring_width" format="dimension"/> <!--圓環顏色--> <attr name="ring_color" format="color"/> </declare-styleable> </resources>

後面還會有其他屬性,我們直接新增這裡,不在具體說明。

RoundView(此自定義View)的程式碼:

/**
 * 自定義進度圓環
 */
public class RoundView extends View{
    /**
     * 圓環寬度,預設半徑的1/5
     */
    private float mRingWidth = 0;
    /**
     * 圓環顏色,預設 #CBCBCB
     */
    private int mRingColor = 0;

    /**
     * 圓環半徑,預設:Math.min(getHeight()/2,getWidth()/2)
     */
    private float mRadius = 0;
    /**
     * 底環畫筆
     */
    private Paint mRingPaint;

    public RoundView(Context context) {
        this(context, null);
    }

    public RoundView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public RoundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public RoundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundView);
        mRingWidth = typedArray.getDimension(R.styleable.RoundView_ring_width, 0);
        mRingColor = typedArray.getColor(R.styleable.RoundView_ring_color, Color.parseColor("#CBCBCB"));
        mRadius = typedArray.getDimension(R.styleable.RoundView_android_radius, 0);
        init();
    }
    /**
     * 初始化
     */
    private void init() {
        mRingPaint = new Paint();
        mRingPaint.setAntiAlias(true);// 抗鋸齒效果
        mRingPaint.setStyle(Paint.Style.STROKE);
        mRingPaint.setColor(mRingColor);// 背景
    }

    @Override
    public void onDraw(Canvas canvas) {
        // 圓心座標,當前View的中心
        float x = getWidth() / 2;
        float y = getHeight() / 2;

        //如果未設定半徑,則半徑的值為view的寬、高一半的較小值
        mRadius = mRadius == 0 ? Math.min(getWidth() / 2, getHeight() / 2) : mRadius;
        //圓環的寬度預設為半徑的1/5
        mRingWidth = mRingWidth == 0 ? mRadius / 5 : mRingWidth;
        // 底環
        canvas.drawCircle(x, y, mRadius, mRingPaint);
    }

效果:
這裡寫圖片描述
我們發現這個環不全啊,因為圓的寬度是在當前半徑向2邊展開的。
這裡寫圖片描述
紅線就是我們的半徑,一部分超出的view的邊界了。解決的辦法就是將我們的半徑減去圓環寬度的一半:

這裡寫圖片描述
加入圖中方框內部分。在執行就會出現我們想要的效果了,不過這裡還有一個問題,就是onDraw方法會一直重繪,所以這段程式碼(就是我們解決上面問題的程式碼)

 //由於圓環本身有寬度,所以半徑要減去圓環寬度的一半,不然一部分圓會在view外面
        mRadius = mRadius - mRingWidth / 2;

會越來越小,所以修改為:

@Override
    public void onDraw(Canvas canvas) {

        // 圓心座標,當前View的中心
        float x = getWidth() / 2;
        float y = getHeight() / 2;

        //如果未設定半徑,則半徑的值為view的寬、高一半的較小值
        float radius = mRadius == 0 ? Math.min(getWidth() / 2, getHeight() / 2) : mRadius;
        //圓環的寬度預設為半徑的1/5
        float ringWidth = mRingWidth == 0 ? radius / 5 : mRingWidth;
        //由於圓環本身有寬度,所以半徑要減去圓環寬度的一半,不然一部分圓會在view外面
        radius = radius - ringWidth / 2;
        mRingPaint.setStrokeWidth(ringWidth);

        // 底環
        canvas.drawCircle(x, y, radius, mRingPaint);
        }

沒有設定任何屬性的效果:
這裡寫圖片描述

設定屬性:

<qiwei.com.frameworknote.ui.RoundView
        android:layout_width="300dp"
        android:layout_height="300dp"
        app:ring_width="10dp"
        app:ring_color="#ff0000"
        android:radius="100"/>

效果:
這裡寫圖片描述

漸變、動態的圓弧

我們先在圓環上畫一個靜態進度圓環,屬性有寬度,由於進度圓環有漸變效果,所以設定屬性開始顏色、結束顏色,還有一點要說明這個進度圓環和底部圓環的半徑是一樣的。
新增如下屬性:

        <!--進度圓環寬度-->
        <attr name="progress_ring_width" format="dimension"/>
        <!--進度圓環開始顏色-->
        <attr name="progress_ring_start_color" format="color"/>
        <!--進度圓環結束顏色-->
        <attr name="progress_ring_end_color" format="color"/>

在RoundView.java中新增屬性:

 /**
     * 進度條圓環寬度
     */
    private float mProgressRingWidth = 0;
    /**
     * 進度條圓環開始顏色,進度條圓環是漸變的,預設
     */
    private int mProgressRingStartColor = 0;
    /**
     * 進度條圓環結束顏色,進度條圓環是漸變的,預設
     */
    private int mProgressRingEndColor = 0;
    /**
     * 進度條圓環Paint
     */
    private Paint mProgressRingPaint;
    /**
     * 進度條圓環漸變shader
     */
    private Shader mProgressRingShader;

TypeArray解析:

mProgressRingWidth = typedArray.getDimension(R.styleable.RoundView_progress_ring_width, 0);
        mProgressRingStartColor = typedArray.getColor(R.styleable.RoundView_progress_ring_start_color, Color.parseColor("#f90aa9"));
        mProgressRingEndColor = typedArray.getColor(R.styleable.RoundView_progress_ring_end_color, Color.parseColor("#931c80"));

在init方法中初始化進度圓滑的Paint和漸變陣列

 // 圓環紫色漸變色
        arcColors = new int[]{mProgressRingStartColor, mProgressRingEndColor};
        mProgressRingPaint = new Paint();
        mProgressRingPaint.setAntiAlias(true);// 抗鋸齒效果
        mProgressRingPaint.setStyle(Paint.Style.STROKE);
        mProgressRingPaint.setStrokeCap(Cap.ROUND);// 圓形筆頭
        mProgressRingPaint.setStrokeWidth(mProgressRingWidth);

注意進度圓環應該小於等於底部圓環的寬度,如果小於,那麼進度圓環應該在底部圓環的中間。
在onDraw方法中繪製完底部圓環後繪製靜態的進度圓環:

// 設定漸變色
        if (mProgressRingShader == null) {
            mProgressRingShader = new SweepGradient(x, y, arcColors,null);
            mProgressRingPaint.setShader(mProgressRingShader);
        }
        canvas.drawArc(new RectF(0,0,getWidth(),getHeight()), 0, 180, false, mProgressRingPaint);

看下效果:
這裡寫圖片描述

出現幾個問題:

  1. 一部分超出view邊界
  2. 進度圓環沒有在底部圓環裡
  3. 我們想要的效果是從頂部開始的

其實1、2都是由半徑引發的問題,3問題是程式碼中我們設定的是從0處開始,解決的方法就是設定為-90。

修改後:

 //----繪製進度圓環------
        // 設定漸變色
        if (mProgressRingShader == null) {
            mProgressRingShader = new SweepGradient(x, y, arcColors, null);
            mProgressRingPaint.setShader(mProgressRingShader);
        }
        //計算進度圓環寬度,預設和底部圓滑一樣大
        float progressRingWidth = mProgressRingWidth == 0 ? ringWidth : mProgressRingWidth;
        mProgressRingPaint.setStrokeWidth(progressRingWidth);
        canvas.drawArc(new RectF(progressRingWidth/2,progressRingWidth/2,getWidth()-progressRingWidth/2,getHeight()-progressRingWidth/2), -90, 180, false, mProgressRingPaint);

我們設定底部圓環的寬度和進度條的寬度:

<qiwei.com.frameworknote.ui.RoundView
        android:layout_width="300dp"
        android:layout_height="300dp"
        app:ring_width="28dp"
        app:progress_ring_width="14dp"/>

效果:
這裡寫圖片描述
問題又出現了:

  1. 進度圓環應該在底部圓環的中間
  2. 漸變色的效果不對

    第一個問題是我們的RectF(矩形區域)設定的不對,下面這張圖告訴我們怎麼算
    這裡寫圖片描述
    修改後:

 // 設定漸變色
        if (mProgressRingShader == null) {
            mProgressRingShader = new SweepGradient(x, y, arcColors, null);
            mProgressRingPaint.setShader(mProgressRingShader);
        }
        //計算進度圓環寬度,預設和底部圓滑一樣大
        float progressRingWidth = mProgressRingWidth == 0 ? ringWidth : mProgressRingWidth;
        mProgressRingPaint.setStrokeWidth(progressRingWidth);
        float left = x - radius;
        float top = y - radius;
        float right = x + radius;
        float bottom = y + radius;
        canvas.drawArc(new RectF(left, top, right, bottom), -90, 180, false, mProgressRingPaint);

效果:
這裡寫圖片描述

我們看下漸變的問題,我們使用的是SweepGradient,也是所說的雷達漸變,它也是從水平方向開始順時針漸變,所以就出現了上面的情況,怎麼解決呢?我想到的辦法是旋轉畫布:

 //計算進度圓環寬度,預設和底部圓滑一樣大
        float progressRingWidth = mProgressRingWidth == 0 ? ringWidth : mProgressRingWidth;
        mProgressRingPaint.setStrokeWidth(progressRingWidth);
        float left = x - radius;
        float top = y - radius;
        float right = x + radius;
        float bottom = y + radius;
        //儲存當前的狀態
        canvas.save();
        //逆時針旋轉90度
        canvas.rotate(-90, x, y);
        canvas.drawArc(new RectF(left, top, right, bottom), 0, 180, false, mProgressRingPaint);

效果:
這裡寫圖片描述

無限接近中啊,可是最上面的圓頭有問題啊,看下圖:
這裡寫圖片描述
我們旋轉90度只旋轉到了藍線處,而實際我們應該旋轉到紅線處,所以我們要計算出紅藍線間的夾角然後加上90度就是我們要旋轉的角度。
修改如下:

/**
     * 已知圓半徑和切線長求弧長公式
     */
    private double radianToAngle(float radios) {
        double aa = mProgressRingWidth / 2 / radios;
        double asin = Math.asin(aa);
        double radian = Math.toDegrees(asin);
        return radian;
    }
    @Override
    public void onDraw(Canvas canvas) {

        // 圓心座標,當前View的中心
        float x = getWidth() / 2;
        float y = getHeight() / 2;

        //如果未設定半徑,則半徑的值為view的寬、高一半的較小值
        float radius = mRadius == 0 ? Math.min(getWidth() / 2, getHeight() / 2) : mRadius;
        //圓環的寬度預設為半徑的1/5
        float ringWidth = mRingWidth == 0 ? radius / 5 : mRingWidth;
        //由於圓環本身有寬度,所以半徑要減去圓環寬度的一半,不然一部分圓會在view外面
        radius = radius - ringWidth / 2;
        mRingPaint.setStrokeWidth(ringWidth);

        // 底環
        canvas.drawCircle(x, y, radius, mRingPaint);
        //----繪製圓環(最底部)結束------

        //----繪製進度圓環------
        // 設定漸變色
        if (mProgressRingShader == null) {
            mProgressRingShader = new SweepGradient(x, y, arcColors, null);
            mProgressRingPaint.setShader(mProgressRingShader);
        }
        //計算進度圓環寬度,預設和底部圓滑一樣大
        float progressRingWidth = mProgressRingWidth == 0 ? ringWidth : mProgressRingWidth;
        mProgressRingPaint.setStrokeWidth(progressRingWidth);
        float left = x - radius;
        float top = y - radius;
        float right = x + radius;
        float bottom = y + radius;
        //儲存當前的狀態
        canvas.save();
        // 旋轉畫布90度+筆頭半徑轉過的角度
        double radian = radianToAngle(radius);
        double degrees = Math.toDegrees(-2 * Math.PI / 360 * (90 + radian));// 90度+
        //逆時針旋轉90度
        canvas.rotate((float)-degrees, x, y);
        canvas.drawArc(new RectF(left, top, right, bottom), (float)radian, 180, false, mProgressRingPaint);

}

注意繪製弧的開始角度為 radian
效果:
這裡寫圖片描述

如何讓我們的圓環動起來呢?就是每次繪製的時候修改圓弧的角度,另外圓弧旋轉的速度我們如何控制呢?我們可以使用動畫和差值器,動畫和差值器我們就不詳細講了。

/**
 * 自定義進度圓環
 */
public class RoundView extends View implements AnimationListener {

    /**
     * 圓環轉過的角度
     */
    private float mSweepAngle = 1;
    /**
     * 之前的角度
     */
    private float mOldAngle = 0;
    /**
     * 新的角度
     */
    private float mNewAngle = 0;
    /**
     * 圓環寬度,預設半徑的1/5
     */
    private float mRingWidth = 0;
    /**
     * 圓環顏色,預設 #CBCBCB
     */
    private int mRingColor = 0;
    /**
     * 進度條圓環寬度
     */
    private float mProgressRingWidth = 0;
    /**
     * 進度條圓環開始顏色,進度條圓環是漸變的,預設
     */
    private int mProgressRingStartColor = 0;
    /**
     * 進度條圓環結束顏色,進度條圓環是漸變的,預設
     */
    private int mProgressRingEndColor = 0;
    /**
     * 圓環半徑,預設:Math.min(getHeight()/2,getWidth()/2)
     */
    private float mRadius = 0;
    /**
     * 進度條圓環Paint
     */
    private Paint mProgressRingPaint;
    /**
     * 進度條圓環漸變shader
     */
    private Shader mProgressRingShader;

    /**
     * 底環畫筆
     */
    private Paint mRingPaint;

    private int[] arcColors = {};// 漸變色
    /**
     * 進度動畫
     */
    private BarAnimation barAnimation;
    /**
     * 抖動(縮放)動畫
     */
    private ScaleAnimation scaleAnimation;
    /**
     * 是否正在改變
     */
    private boolean isAnimation = false;

    public RoundView(Context context) {
        this(context, null);
    }

    public RoundView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public RoundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public RoundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundView);
        mRingWidth = typedArray.getDimension(R.styleable.RoundView_ring_width, 0);
        mRingColor = typedArray.getColor(R.styleable.RoundView_ring_color, Color.parseColor("#CBCBCB"));
        mProgressRingWidth = typedArray.getDimension(R.styleable.RoundView_progress_ring_width, 0);
        mProgressRingStartColor = typedArray.getColor(R.styleable.RoundView_progress_ring_start_color, Color.parseColor("#f90aa9"));
        mProgressRingEndColor = typedArray.getColor(R.styleable.RoundView_progress_ring_end_color, Color.parseColor("#931c80"));
        mRadius = typedArray.getDimension(R.styleable.RoundView_android_radius, 0);

        typedArray.recycle();
        init();
    }

    /**
     * 初始化
     */
    private void init() {

        mRingPaint = new Paint();
        mRingPaint.setAntiAlias(true);// 抗鋸齒效果
        mRingPaint.setStyle(Paint.Style.STROKE);
        mRingPaint.setColor(mRingColor);// 背景灰

        // 圓環紫色漸變色
        arcColors = new int[]{mProgressRingStartColor, mProgressRingEndColor};
        mProgressRingPaint = new Paint();
        mProgressRingPaint.setAntiAlias(true);// 抗鋸齒效果
        mProgressRingPaint.setStyle(Paint.Style.STROKE);
        mProgressRingPaint.setStrokeCap(Cap.ROUND);// 圓形筆頭
        mProgressRingPaint.setStrokeWidth(mProgressRingWidth);

        barAnimation = new BarAnimation();
        barAnimation.setAnimationListener(this);

        scaleAnimation = new ScaleAnimation();
        scaleAnimation.setDuration(100);
    }


    /**
     * 設定新的角度
     */
    public void setAngle(final float newAngle,boolean isScale) {
        if (!isAnimation) {
            if(isScale){
                scaleAnimation.setAnimationListener(new AnimationListener() {
                    @Override
                    public void onAnimationStart(Animation animation) {

                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        changeAngle(newAngle);
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {

                    }
                });
                startAnimation(scaleAnimation);
            }else{
                changeAngle(newAngle);
            }
        }
    }

    private void changeAngle(float newAngle){
        mOldAngle = mNewAngle;
        mNewAngle = newAngle;
        int duration = (int) (Math.abs(mNewAngle - mOldAngle) * 15);
        barAnimation.setDuration(duration);
        barAnimation.setInterpolator(new DecelerateInterpolator());
        startAnimation(barAnimation);
    }

    @SuppressLint("DrawAllocation")
    @Override
    public void onDraw(Canvas canvas) {

        // 圓心座標,當前View的中心
        float x = getWidth() / 2;
        float y = getHeight() / 2;

        //如果未設定半徑,則半徑的值為view的寬、高一半的較小值
        float radius = mRadius == 0 ? Math.min(getWidth() / 2, getHeight() / 2) : mRadius;
        //圓環的寬度預設為半徑的1/5
        float ringWidth = mRingWidth == 0 ? radius / 5 : mRingWidth;
        //由於圓環本身有寬度,所以半徑要減去圓環寬度的一半,不然一部分圓會在view外面
        radius = radius - ringWidth / 2;
        mRingPaint.setStrokeWidth(ringWidth);

        // 底環
        canvas.drawCircle(x, y, radius, mRingPaint);
        //----繪製圓環(最底部)結束------

        //----繪製進度圓環------
        // 設定漸變色
        if (mProgressRingShader == null) {
            mProgressRingShader = new SweepGradient(x, y, arcColors, null);
            mProgressRingPaint.setShader(mProgressRingShader);
        }
        //計算進度圓環寬度,預設和底部圓滑一樣大
        float progressRingWidth = mProgressRingWidth == 0 ? ringWidth : mProgressRingWidth;
        mProgressRingPaint.setStrokeWidth(progressRingWidth);
        float left = x - radius;
        float top = y - radius;
        float right = x + radius;
        float bottom = y + radius;
        // 旋轉畫布90度+筆頭半徑轉過的角度
        double radian = radianToAngle(radius);
        double degrees = Math.toDegrees(-2 * Math.PI / 360 * (90 + radian));// 90度+
        canvas.save();
        canvas.rotate((float) degrees, x, y);
        canvas.drawArc(new RectF(left, top, right, bottom), (float) radian, mOldAngle + mSweepAngle, false, mProgressRingPaint);

        //----繪製進度圓環結束------

        super.onDraw(canvas);
    }

    /**
     * 已知圓半徑和切線長求弧長公式
     *
     * @param radios
     * @return
     */
    private double radianToAngle(float radios) {
        double aa = mProgressRingWidth / 2 / radios;
        double asin = Math.asin(aa);
        double radian = Math.toDegrees(asin);
        return radian;
    }


    /**
     * 抖動(縮放動畫)
     */
    public class ScaleAnimation extends Animation {
        private int centerX;
        private int centerY;

        public ScaleAnimation() {
        }

        @Override
        public void initialize(int width, int height, int parentWidth,
                               int parentHeight) {
            super.initialize(width, height, parentWidth, parentHeight);
            centerX = width / 2;
            centerY = height / 2;
        }

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);

            if (interpolatedTime < 0.25f) {
                // 1-1.1
                float ss = interpolatedTime * 0.4f + 1f;
                Matrix matrix = t.getMatrix();
                matrix.setScale(ss, ss, centerX, centerY);
            } else if (interpolatedTime >= 0.25f && interpolatedTime < 0.5f) {
                // 1.1-1
                float ss = (0.5f - interpolatedTime) * 0.4f + 1f;
                Matrix matrix = t.getMatrix();
                matrix.setScale(ss, ss, centerX, centerY);
            } else if (interpolatedTime >= 0.5f && interpolatedTime < 0.75f) {
                // 1-0.9
                float ss = (0.75f - interpolatedTime) * 0.4f + 0.9f;
                Matrix matrix = t.getMatrix();
                matrix.setScale(ss, ss, centerX, centerY);
            } else if (interpolatedTime >= 0.75f && interpolatedTime <= 1f) {
                // 0.9-1
                float ss = interpolatedTime * 0.4f + 0.6f;
                Matrix matrix = t.getMatrix();
                matrix.setScale(ss, ss, centerX, centerY);
            }
        }
    }

    /**
     * 進度條動畫
     */
    public class BarAnimation extends Animation {


        public BarAnimation() {
        }

        /**
         * 然後呼叫postInvalidate()不停的繪製view。
         */
        @Override
        protected void applyTransformation(float interpolatedTime,
                                           Transformation t) {

            super.applyTransformation(interpolatedTime, t);
            if (mNewAngle - mOldAngle >= 0) {
                // 正向
                mSweepAngle = interpolatedTime * (mNewAngle - mOldAngle);
            } else {
                // 逆向
                mSweepAngle = interpolatedTime * (mNewAngle - mOldAngle);
            }
            postInvalidate();
        }
    }

    @Override
    public void onAnimationStart(Animation animation) {
        isAnimation = true;
    }

    @Override
    public void onAnimationEnd(Animation animation) {
        isAnimation = false;

    }

    @Override
    public void onAnimationRepeat(Animation animation) {
        // TODO Auto-generated method stub

    }

}

這裡面主要涉及到了動畫和差值器,重寫了動畫的 applyTransformation 方法。
這裡寫圖片描述
抖動的效果在gif上沒有展示出來,執行看看吧。

下面我們繪製下箭頭,其實箭頭呢就是3條線,重點是計算線的座標:
這裡寫圖片描述

由圖中計算1、2、3、4點到座標,我們定義1、2點之間長度的一半為箭頭的大小(arrowSize)。r代表半徑所以:
1點座標:(x-arrowSize,y-r)
2點座標:(x+arrowSize,y-r)
3點座標:(x, y - r - arrowSize)
4點座標:(x, y - r + arrowSize)

所以繪製箭頭的程式碼:

         //----繪製箭頭開始------
        //畫布恢復到上一次save到狀態,也就是旋轉的畫布

        //將我們要畫箭頭的位置旋轉到頂部,方便我們計算箭頭的座標,繪製完箭頭在將畫布恢復
        canvas.save();
        double hudu = 2 * Math.PI / 360 * (mOldAngle + mSweepAngle);
        double radians = Math.toDegrees(hudu);
        // 旋轉畫布
        canvas.rotate((float) radians, x, y);
        //繪製箭頭
        canvas.drawLine(x - arrowSize, y - radius, x + arrowSize, y - radius, mArrowPaint);
        canvas.drawLine(x + arrowSize, y - radius, x, y - radius - arrowSize, mArrowPaint);
        canvas.drawLine(x + arrowSize, y - radius, x, y - radius + arrowSize, mArrowPaint);
        canvas.restore();
        //----繪製箭頭結束------

完整程式碼:


/**
 * 自定義進度圓環
 */
public class RoundView extends View implements AnimationListener {

    /**
     * 圓環轉過的角度
     */
    private float mSweepAngle = 1;
    /**
     * 之前的角度
     */
    private float mOldAngle = 0;
    /**
     * 新的角度
     */
    private float mNewAngle = 0;
    /**
     * 圓環寬度,預設半徑的1/5
     */
    private float mRingWidth = 0;
    /**
     * 圓環顏色,預設 #CBCBCB
     */
    private int mRingColor = 0;
    /**
     * 進度條圓環寬度
     */
    private float mProgressRingWidth = 0;
    /**
     * 進度條圓環開始顏色,進度條圓環是漸變的,預設
     */
    private int mProgressRingStartColor = 0;
    /**
     * 進度條圓環結束顏色,進度條圓環是漸變的,預設
     */
    private int mProgressRingEndColor = 0;
    /**
     * 圓環半徑,預設:Math.min(getHeight()/2,getWidth()/2)
     */
    private float mRadius = 0;
    /**
     * 進度條圓環Paint
     */
    private Paint mProgressRingPaint;
    /**
     * 進度條圓環漸變shader
     */
    private Shader mProgressRingShader;

    /**
     * 底環畫筆
     */
    private Paint mRingPaint;
    /**
     * 箭頭畫筆
     */
    private Paint mArrowPaint;
    /**
     * 箭頭大小
     */
    private int arrowSize = 0;
    /**
     * 文字畫筆
     */
    private Paint mTextPaint;
    /**
     * 文字顏色
     */
    private int mTextColor;
    /**
     * 文字大小
     */
    private float mTextSize;

    private int[] arcColors = {};// 漸變色
    /**
     * 進度動畫
     */
    private BarAnimation barAnimation;
    /**
     * 抖動(縮放)動畫
     */
    private ScaleAnimation scaleAnimation;
    /**
     * 是否正在改變
     */
    private boolean isAnimation = false;

    public RoundView(Context context) {
        this(context, null);
    }

    public RoundView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public RoundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public RoundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundView);
        mRingWidth = typedArray.getDimension(R.styleable.RoundView_ring_width, 0);
        mRingColor = typedArray.getColor(R.styleable.RoundView_ring_color, Color.parseColor("#CBCBCB"));
        mProgressRingWidth = typedArray.getDimension(R.styleable.RoundView_progress_ring_width, 0);
        mProgressRingStartColor = typedArray.getColor(R.styleable.RoundView_progress_ring_start_color, Color.parseColor("#f90aa9"));
        mProgressRingEndColor = typedArray.getColor(R.styleable.RoundView_progress_ring_end_color, Color.parseColor("#931c80"));
        mRadius = typedArray.getDimension(R.styleable.RoundView_android_radius, 0);
        mTextColor = typedArray.getColor(R.styleable.RoundView_text_color, Color.parseColor("#f90aa9"));
        mTextSize = typedArray.getDimension(R.styleable.RoundView_text_size, dp2px(context, 40));
        arrowSize = dp2px(context, 7);
        typedArray.recycle();
        init();
    }

    /**
     * 初始化
     */
    private void init() {

        mRingPaint = new Paint();
        mRingPaint.setAntiAlias(true);// 抗鋸齒效果
        mRingPaint.setStyle(Paint.Style.STROKE);
        mRingPaint.setColor(mRingColor);// 背景灰

        // 圓環紫色漸變色
        arcColors = new int[]{mProgressRingStartColor, mProgressRingEndColor};
        mProgressRingPaint = new Paint();
        mProgressRingPaint.setAntiAlias(true);// 抗鋸齒效果
        mProgressRingPaint.setStyle(Paint.Style.STROKE);
        mProgressRingPaint.setStrokeCap(Cap.ROUND);// 圓形筆頭
        mProgressRingPaint.setStrokeWidth(mProgressRingWidth);

        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);// 抗鋸齒效果
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setTextAlign(Align.CENTER);
        mTextPaint.setColor(mTextColor);// 字型顏色
        mTextPaint.setTextSize(mTextSize);

        mArrowPaint = new Paint();
        mArrowPaint.setAntiAlias(true);// 抗鋸齒效果
        mArrowPaint.setStyle(Paint.Style.FILL);
        mArrowPaint.setColor(Color.parseColor("#000000"));
        mArrowPaint.setStrokeCap(Cap.ROUND);
        mArrowPaint.setStrokeWidth(5);


        barAnimation = new BarAnimation();
        barAnimation.setAnimationListener(this);

        scaleAnimation = new ScaleAnimation();
        scaleAnimation.setDuration(100);
    }

    /**
     * 設定新的角度
     * newAngle:角度
     */
    public void setAngle(final float newAngle) {
        setAngle(newAngle, true);
    }

    /**
     * 設定新的角度
     * newAngle:角度
     * isScale:是否抖動
     */
    public void setAngle(final float newAngle, boolean isScale) {
        if (!isAnimation) {
            if (isScale) {
                scaleAnimation.setAnimationListener(new AnimationListener() {
                    @Override
                    public void onAnimationStart(Animation animation) {

                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        changeAngle(newAngle);
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {

                    }
                });
                startAnimation(scaleAnimation);
            } else {
                changeAngle(newAngle);
            }
        }
    }


    private void changeAngle(float newAngle) {
        mOldAngle = mNewAngle;
        mNewAngle = newAngle;
        //計算動畫時間
        int duration = (int) (Math.abs(mNewAngle - mOldAngle) * 15);
        barAnimation.setDuration(duration);
        //動畫差值器
        barAnimation.setInterpolator(new DecelerateInterpolator());
        startAnimation(barAnimation);
    }

    @SuppressLint("DrawAllocation")
    @Override
    public void onDraw(Canvas canvas) {

        // 圓心座標,當前View的中心
        float x = getWidth() / 2;
        float y = getHeight() / 2;

        //如果未設定半徑,則半徑的值為view的寬、高一半的較小值
        float radius = mRadius == 0 ? Math.min(getWidth() /