1. 程式人生 > >Android自定義圖表庫(一):圓形進度圖

Android自定義圖表庫(一):圓形進度圖

效果預覽

在這裡插入圖片描述

自定義View第一步:確認View的大小

無論是自定義一個View還是ViewGroup我們必須得先為其制定在不同MeasureSpecMode下的大小,我這裡就不講解什麼繪製原始碼了什麼的,我們就直接實戰。
我們在onMeasure中需要呼叫setMeasuredDimension(width,height)來為這個View確定寬高,呼叫這個方法確認完,我們就可以通過getMeasuredWidth和getMeasuredHeight來獲取設定的這兩個值了,而我們看過相關文章或者原始碼的童鞋們都知道,View的onMeasure預設實現是會處理 MeasureSpec.EXACTLY這種模式的,因為這種模式下代表寬高在XML或者程式碼中明確設定過了,或者是父View的大小。所以我們只需處理MeasureSpec.AT_MOST這種模式時,寬高是多少,因為這個模式代表了我們在XML設定了wrap_content,而包裹內容是多大,我們不知道,必須得自己明確的設定,你設定多大就是多大。一般的話,我們會根據繪製內容會繪製多大來確認這個View的大小,但是我們這種圖表類的繪製內容要繪製的時候又需要根據控制的大小去設定一些變數,所以這樣就產生了矛盾,所以我們這裡的這個自定義View必須得先設定一個合適的寬高,我在這裡就設定它為父View的2/3。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int resultWidth = widthSize;
        int resultHeight = heightSize;

        if(widthMode == MeasureSpec.AT_MOST){ //wrap_content的時候取父寬度的2/3
            resultWidth = widthSize * 2/3;
        }
        if(heightMode == MeasureSpec.AT_MOST){//wrap_content的時候取父高度的2/3
            resultHeight = heightSize * 2/3;
        }

        setMeasuredDimension(resultWidth,resultHeight);

        initAfterMeasure(); //設定完寬高之後去為繪製的時候需要的變數賦值
    }

接著去初始化繪製時需要用的一些變數

    private void initAfterMeasure() {
    	//rectContent這個Rect物件,代表了我們自定義View時要繪製的區域,我們建立的時候需要在寬高的繼承上減去padding,因為
    	//我們需要自己處理padding,父view不會幫我們處理的。padding之內才可以繪製,這樣的話padding才會看起來是生效的
        rectContent = new RectF(getPaddingLeft(),getPaddingTop(),getMeasuredWidth() - getPaddingRight(),getMeasuredHeight() - getPaddingBottom());
        //要畫的圓形的中心店
        centerPoint = new PointF(rectContent.right/2,rectContent.bottom/2);
        //完成度對應的角度,per是這個完成度,小數,由外部設定,比如說0.6f就是百分之60
        sweepAngle = per * 360f;
        //選擇寬高中較小的一邊作為圓的半徑
        chartRaduis = rectContent.right < rectContent.bottom ? rectContent.right : rectContent.bottom;
        chartRaduis = chartRaduis/2;
        //圓環的寬度為半徑的五分之一
        ringWidth = (int) (chartRaduis * 1 / 5);
		
		//圓環中間字型的高度
        allHeight = 0;
		//計算字型的高度,這樣才能讓幾行字居中顯示
        for (int i = 0; i < labelList.size(); i++) {
            ChartLabel chartLabel = labelList.get(i);
            paintText.setTextSize(chartLabel.getTextSize());
            float fontHeight = FontUtil.getFontHeight(paintText);
            allHeight += (fontHeight + labelSpace);
        }
        allHeight -= labelSpace;
    }

上述變數的圖示:
在這裡插入圖片描述

因為的文章裡,只寫實戰,不會去太多的講解什麼原理,或者API怎麼使用,所以就不過多解釋其他的了。

自定義View第二步:繪製內容

因為在上面 我們已經確認了View的大小,並且根據大小我們已經確認了中心點,圓形的半徑,文字的起始位置,所以我們就可以愉快的在onDraw方法中進行繪製了。

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //繪製背景
        drawDefualt(canvas);

        //繪製圖表
        if(!startDraw && startAnimator){ //如果是第一次onDraw就去啟動動畫,通過動畫不斷的重繪View
            startDraw = true;
            startAnimation();
        }else { //實際去繪製View
            drawChart(canvas);
        }
    }

我們看動態圖應該可以發現,在進度環逐漸變多的時候,背景有一個灰色的環是不動的,一直存在的,所以我們先來繪製這個背景環:

   private void drawDefualt(Canvas canvas) {
   		//設定圓環顏色,並且畫一個chartRaduis半徑的圓
        paintChart.setColor(colorRingDef);
        canvas.drawCircle(centerPoint.x,centerPoint.y,chartRaduis,paintChart);
        //設定為View原本的背景色(可以通過特殊方法獲取,這裡我們就使用白色),畫一個 chartRaduis - ringWidth 半徑的圓,
        //這樣子的話,中間就被蓋住,只有灰色的ringWidth這麼寬的圓環了。
        paintChart.setColor(backColor);
        canvas.drawCircle(centerPoint.x,centerPoint.y,chartRaduis - ringWidth,paintChart);
    }

去看一下啟動動畫:

    private void startAnimation(){
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f); //從0到1 意思是從沒有到原本設定的那個值
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                animPro = (float) animation.getAnimatedValue(); //原來值的完成度
                invalidate(); //重繪
            }
        });
        valueAnimator.setDuration(animDuration);
        valueAnimator.start();
    }

這是個屬性動畫,從0到1 意味著完成度,假如原來設定的完成率是0.6f,那麼動畫就會導致他的值 從0.6f* 0 ~ 0.6f * 1,就是從無到有,不理解的同學,也可以直接將動畫值設定成從0到設定的完成度。

來看一下實際的繪製內容:

   private void drawChart(Canvas canvas) {
       if(per == 0){//如果所佔進度為0,則不用繪製彩色的圓弧了,只繪製中間的文字部分。
           drawCenterText(canvas);
           return;
       }
       if(!startAnimator){ // 如果不用開啟動畫,那麼就直接使動畫完成度為1,這樣就是直接畫設定的那個值
           animPro = 1.0f;
       }
       //rectF是要畫圓弧的那個矩形,我們知道要畫一個圓形 必須現有一個矩形。
       RectF rectF = new RectF(centerPoint.x - chartRaduis, centerPoint.y - chartRaduis, centerPoint.x + chartRaduis, centerPoint.y + chartRaduis);
       paintChart.setColor(colorRing);
       //畫圓弧startAngle = -90,就是12點的位置,轉過的角度是sweepAngle * 動畫完成度。
       canvas.drawArc(rectF,startAngle,sweepAngle*animPro,true,paintChart);
       paintChart.setColor(backColor);
       //畫中心的白色圓,這樣才能蓋住所畫的圓弧,呈現出圓環的效果
       canvas.drawCircle(centerPoint.x,centerPoint.y,chartRaduis - ringWidth,paintChart);
   		//畫中心的文字
       drawCenterText(canvas);
   }

通過上述的努力,我們就把圓環畫完了,接著我們去畫中間的文字:

    private void drawCenterText(Canvas canvas) {
    	//算出文字的其實Y座標
        float top = centerPoint.y - allHeight/2;
        //迴圈畫每一行label
        for (int i = 0; i < labelList.size(); i++) {
        	//chartLabel就是封裝了每一行文字的顏色和字型大小,由外部來設定
            ChartLabel chartLabel = labelList.get(i);
            paintText.setColor(chartLabel.getTextColor());
            paintText.setTextSize(chartLabel.getTextSize());
             //將這個設定成center,我們就不需要求文字的x座標了,直接用圓形的中點當做文字的中點
            paintText.setTextAlign(Paint.Align.CENTER);
            //如何求得文字的高,也有很多文章講,不多說,只是封裝成工具類
            float textHeight = FontUtil.getFontHeight(paintText);
            top += i * (textHeight + labelSpace);
            //文字的y座標是個難點,應該是top + Math.abs(paintText.getFontMetrics().top),至於為什麼,也有文字講,我們這裡只講實戰
            canvas.drawText(chartLabel.getText(),centerPoint.x,top + Math.abs(paintText.getFontMetrics().top),paintText);
        }
    }

這樣的話,一個圓形進度條就畫完了,至於如何去自定義屬性,值的賦值等等這些無關緊要的大家自己看程式碼就可以了。

總結

總結一下畫這麼一個圓形進度環用到了什麼知識:

  • 屬性動畫改變值,不斷的進行重繪
  • onMeasure確定View的寬高。
  • 繪製API:
    1.canvas.drawCircle 畫圓形
    2.canvas.drawArc 畫圓弧
    3.canvas.drawText 畫文字

可以看到要畫一個圓環是如何的簡單,而確確實實的只用到了三個繪圖的API,畫圓形,畫圓弧,畫文字,所以我們不要再鬱悶每次看文章學習了很多自定義View的API,卻還是不知道能做什麼,自定義不出好看的View。
原始碼:https://github.com/Ade-rui/ChartCodes/tree/master/progresschart

如果大家對上述用到的API不是很瞭解,可以去看如下的兩篇大神寫的系列文章:
啟艦的自定義View系列
hencoder大神的自定義View系列
有這兩個的系列文章就可以完全掌握自定義了。