Android自定義View——環形進度條

自定義View

讓我們先從一個簡單的例子入手:
環形進度條
一個簡單的環形進度條,在進度條前端位置加一個白色小圓點,然後還有一個進度加載動畫。

首先我們分析一下這個自定義View包含了以下幾個部分:

  • 圓環背景
  • 圓環進度
  • 白色小圓點
  • 加載動畫

大概就只有這幾個部分組成。

第一步:首先我們要新建一個自定義的類繼承系統的View類:

public class CircleProgress extends View {
    private static final String TAG = "CircleProgress";

    public CircleProgress(Context context) {
        super(context);
        init();
    }

    public CircleProgress(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CircleProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
 
    private void init(){
        //初始化操作
    }
}

需要注意的是:

1、其中還有一個構造方法

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

因爲這個構造方法需要至少API21才能支持,所以我們不使用。

2、在每個構造方法中我們都需要調用init()方法來初始化我們的view。

3、除了上面代碼中所展示的構造方法的寫法,還有另一種方式:

public class CircleProgress extends View {
    public CircleProgress(Context context) {
        this(context,null);
    }

    public CircleProgress(Context context, @Nullable AttributeSet attrs) {
        this(context,attrs,0);
    }

    public CircleProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        
    }

}

按照此種方式最終會調用到第三個構造方法,所以我們只需要在第三個構造方法裏調用init()方法就可以了。

那麼問題來了,現在有兩種實現構造方法的方式,我們應該使用哪種?答案是:使用第一種方式更加穩妥。

使用第二種方式如果我們的自定義View(繼承TextView、ListView等自定義View)擁有默認的defStyleAttr時候,就會被我們0所覆蓋,這時候就會導致一系列問題。所以只有當我們能夠完全確認自定義View沒有默認的defStyleAttr的時候纔可以使用第二種方式。

第二步:初始化自定義View的數據

 private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
    }

對於我們這個需求來說,我們沒有自定義參數,也沒有其他需要初始化的東西。

如果我們需要自定義的參數:

  • 創建自定義View之後,values文件夾下創建attrs.xml文件,在該文件中編寫styleableitem等標籤元素完成自定義屬性的定義;
  • 在佈局文件中使用自定義屬性;
  • 在自定義View的構造方法中通過TypedArray獲取。

如果想要深入的瞭解自定義參數,請參考鴻洋的這篇博客

1.在attrs文件中創建自定義屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!--CircleProgress的自定義屬性-->
    <declare-styleable name="CircleProgress">
        <attr name="progress" format="float" />
    </declare-styleable>

</resources>

2.在佈局文件中使用自定義屬性

    <com.kanlulu.customview.widget.CircleProgress
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_centerInParent="true"
        app:progress="75" />

3.在構造方法中獲取屬性值

    private void init(Context context, @Nullable AttributeSet attrs) {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);

        if (attrs != null) {
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgress);
            creditProgress = typedArray.getFloat(R.styleable.CircleProgress_progress, 0);
            typedArray.recycle();
        }

    }

第三步:重寫onDraw()方法繪製不同部分

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //TODO getHeight()方法只有在onDraw的時候和之後的方法纔有作用
        radius = (getHeight() - backCircleWith) / 2;//半徑
        //繪製背景圓環
        paintBackCircle(canvas);
        //繪製進度
        paintProgress(canvas);
        //繪製圓點
        paintProgressPoint(canvas);
    }

這裏有一個需要注意的地方getHeight()方法獲取的是我們控件的高度,它只有在onDraw()和之後的onMeasure()onlayout()纔有作用,如果我們在init()方法中獲取getHeight()的結果是0。

如上代碼所示,我們分別新建三個方法完成這三部分的繪製:

1.繪製背景圓環

這是一個純粹的圓環沒有其他東西,我們只需要知道圓點和半徑就可以了。

    /**
     * 背景圓環
     *
     * @param canvas
     */
    private void paintBackCircle(Canvas canvas) {
        mPaint.setColor(getResources().getColor(R.color.circleBackColor));
        mPaint.setStrokeWidth(backCircleWith);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint);
    }

2.繪製進度圓度圓環

其實就是繪製一個扇形圓環,對於繪製扇形而言,我們需要一個RectF對象來確定繪製的範圍,然後我們需要一個扇形的起始位置的角度和扇形的角度,這樣我們就可以以繪製出一個扇形了。

    /**
     * 畫進度扇形圓環
     *
     * @param canvas
     */
    private void paintProgress(Canvas canvas) {
        mPaint.setColor(getResources().getColor(R.color.circleProgressColor));
        mPaint.setStrokeWidth(backCircleWith);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);

        RectF rectF = new RectF(backCircleWith / 2, backCircleWith / 2, 2 * radius + backCircleWith / 2, 2 * radius + backCircleWith / 2);
        currentAngle += 5;
        if (currentAngle >= sweepAngle) currentAngle = sweepAngle;
        canvas.drawArc(rectF, 180, currentAngle, false, mPaint);
    }

RectF的構造方法有四個參數,分別是left、top、right、bottom,這四個位置限定了我們扇形繪製的範圍,它在Android座標體系中的位置如圖所示:
位置座標體系
Android座標體系中關於角度位置的定義如圖所示:
角度座標體系
3.繪製進度前端的圓點

這個就是繪製一個點,關鍵要準確的確定點的座標位置,這時候我們需要稍微用到一些簡單的三角函數的知識,我們在園環中現在已知半徑、角度和起始位置,我們就可以確定要繪製的點在圓環上的位置。

    /**
     * 畫進度前端的圓點
     *
     * @param canvas
     */
    private void paintProgressPoint(Canvas canvas) {
        mPaint.setColor(getResources().getColor(R.color.circlePoint));
        mPaint.setStrokeWidth(backCircleWith - 8);
        mPaint.setStyle(Paint.Style.STROKE);
        if (sweepAngle > 359 || sweepAngle <= 0) return;
	    //每次繪製遞增5個數
        currentPointAngle += 5;
        if (currentPointAngle >= sweepAngle) currentPointAngle = sweepAngle;
        //繪製點的x座標
        double x = getWidth() / 2 - radius * Math.cos(currentPointAngle * Math.PI / 180);
        //繪製點的y座標
        double y = getHeight() / 2 - radius * Math.sin(currentPointAngle * Math.PI / 180);
        canvas.drawPoint((float) x, (float) y, mPaint);
    }

這些步驟完成後我們就可以看到靜態的進度環了;現在我們需要加上漸進的動畫。

在自定義View中沒有直接的動畫,要想實現動畫效果我們只有通過重新繪製的方法來達到動畫效果invalidate();

我們可以每輪比上一次多繪製一點,

//每次繪製遞增5個數  
  currentAngle += 5;
  if (currentAngle >= sweepAngle) currentAngle = sweepAngle;
  canvas.drawArc(rectF, 180, currentAngle, false, mPaint);

//在onDraw()裏重新繪製
 if (currentAngle != sweepAngle || currentPointAngle != sweepAngle) invalidate();//重新繪製

我們可以不延遲發起重新繪製,因爲繪製一次本身就會耗費數十毫秒不等的時間。我們可以根據實際情況判斷是否要設置延時的時間。
項目地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章