自定義View——PieChart

本文是我在閱讀了網(wǎng)絡上其他作者的優(yōu)秀內(nèi)容之后做的摘錄轉(zhuǎn)載潮酒,其中有對內(nèi)容的補充秒梅。
原來地址:http://www.idtkm.com/customview/customview5/

PieChart

(PS: 經(jīng)過之前4篇博客的基礎知識學習,終于可以開始編寫PieChart了 (≧▽≦)/啦啦啦)

一置森、數(shù)據(jù)需求

來分析下庞瘸,用戶需要提供怎樣的數(shù)據(jù),首先要有數(shù)據(jù)的值拿诸,然后還需要對應的數(shù)據(jù)名稱,以及顏色塞茅。繪制PieChart需要什么呢亩码,由圖可以看出,需要百分比值野瘦,扇形角度描沟,色塊顏色。所以總共屬性有:

public class PieData { 
  private String name;
  private float value;
  private float percentage; 
  private int color = 0; 
  private float angle = 0;
}

各屬性的set與get請自行增加鞭光。

二吏廉、構造函數(shù)

構造函數(shù)中,增加一些xml設置惰许,創(chuàng)建一個attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources> 
    <declare-styleable name="PieChart">
       <attr name="name" format="string"/> 
       <attr name="percentDecimal" format="integer"/> 
       <attr name="textSize" format="dimension"/> 
    </declare-styleable>
</resources>

這是只設置了一部分屬性席覆,如果你有強迫癥希望全部設置的話,可以自行增加汹买。在PieChart中使用TypedArray進行屬性的獲取佩伤。建議使用如下的寫法,可以避免在沒有設置屬性時卦睹,也運行getXXX方法畦戒。

TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PieChart, defStyleAttr,defStyleRes);
int n = array.getIndexCount();
for (int i=0; i<n; i++){ 
  int attr = array.getIndex(i); 
  switch (attr){ 
      case R.styleable.PieChart_name: 
        name = array.getString(attr);
        break; 
      case R.styleable.PieChart_percentDecimal:
        percentDecimal = array.getInt(attr,percentDecimal); 
        break; 
      case R.styleable.PieChart_textSize: 
        percentTextSize = array.getDimensionPixelSize(attr,percentTextSize); 
        break; 
      }
  }
array.recycle();

三、動畫函數(shù)

繪制一個完整的圓结序,旋轉(zhuǎn)的角度為360障斋,動畫時間為可set參數(shù),默認5秒,監(jiān)聽animatedValue參數(shù)垃环,用于與繪制時進行計算邀层。ValueAnimator類涉及到的參數(shù)的意義請查看自定義View——Canvas與ValueAnimator文章。

private void initAnimator(long duration){ 
  if (animator !=null &&animator.isRunning()){ 
    animator.cancel(); animator.start();
  }else { 
    animator=ValueAnimator.ofFloat(0,360).setDuration(duration); 
    animator.setInterpolator(timeInterpolator); 
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
       @Override 
       public void onAnimationUpdate(ValueAnimator animation) { 
            animatedValue = (float) animation.getAnimatedValue(); 
            invalidate(); 
        }
     });
   animator.start(); 
  }
}

四遂庄、onMeasure

View默認的onMeasure方法中艇挨,并沒有根據(jù)測量模式区岗,對布局寬高進行調(diào)整,所以為了適應wrap_content的布局設置,需要對onMeasure方法進行重寫舅锄。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
  int width = measureDimension(widthMeasureSpec); 
  int height = measureDimension(heightMeasureSpec); 
  setMeasuredDimension(width,height);
}

重寫的onMeasure方法展氓,調(diào)用了自定義的measureDimension方法處理數(shù)據(jù)申钩,完成后交給系統(tǒng)的setMeasuredDimension方法颂碧。接下來看下自定義的measureDimension方法。

private int measureDimension(int measureSpec){ 
  int size = measureWrap(mPaint); 
  int specMode = MeasureSpec.getMode(measureSpec); 
  int specSize = MeasureSpec.getSize(measureSpec); 
  switch (specMode){ 
    case MeasureSpec.UNSPECIFIED: 
      size = measureWrap(mPaint); 
      break; 
    case MeasureSpec.EXACTLY: 
      size = specSize; 
      break; 
    case MeasureSpec.AT_MOST: 
      //合適尺寸不得大于View的尺寸 
      size = Math.min(specSize,measureWrap(mPaint)); 
      break; 
   }
return size;}

measureDimension根據(jù)測量的類型沫换,分別計算尺寸的長度臭蚁,每個類型的含義在第一篇中已經(jīng)進行了說明,在這里不再贅述讯赏。EXACTLY是在xml中定義match_parent以及具體的數(shù)值是使用垮兑,而AT_MOST則是在wrap_content時使用,measureWrap方法用于計算當前PieChart的最小合適長度漱挎,接下來看看這個方法系枪。

 private int measureWrap(Paint paint){ 
  float wrapSize; 
  if (mPieData!=null&&mPieData.size()>1){ 
     NumberFormat numberFormat =NumberFormat.getPercentInstance(); 
     numberFormat.setMinimumFractionDigits(percentDecimal); 
     paint.setTextSize(percentTextSize); 
     float percentWidth = paint.measureText(numberFormat.format(mPieData.get(stringId).getPercentage())+""); 
     paint.setTextSize(centerTextSize); 
     float nameWidth = paint.measureText(name+""); 
     wrapSize = (percentWidth*4+nameWidth*1.0f)*(float) offsetScaleRadius; 
  }else { 
      wrapSize = 0; 
  } 
return (int) wrapSize;}

測量寬高的方式類似于TextView,根據(jù)PieChart中的圖名與百分比文本的寬度進行計算的磕谅。其中stringId是在處理數(shù)據(jù)的過程中嗤无,計算出的擁有最長字符的區(qū)域Id。
從代碼中可以看出怜庸,wrap_content情況下的,PieChart的寬高就等于百分比字符長度的4倍垢村,加上圖名的長度割疾。

五、onSizeChanged

在此函數(shù)中嘉栓,獲取當前View的寬高以及根據(jù)padding值計算出的實際繪制區(qū)域的寬高宏榕,同時進行PieChart繪制所需的半徑以及布局位置設置。

protected void onSizeChanged(int w, int h, int oldw, int oldh) { 
  super.onSizeChanged(w, h, oldw, oldh); 
  mWidth = w-getPaddingLeft()-getPaddingRight();
 //適應padding設置 
 mHeight = h-getPaddingTop()-getPaddingBottom();
//適應padding設置 
mViewWidth = w; mViewHeight = h; 

//標準圓環(huán) 
//圓弧 
r = (float) (Math.min(mWidth,mHeight)/2*widthScaleRadius);

// 餅狀圖半徑 
// 餅狀圖繪制區(qū)域 
rectF.left = -r; 
rectF.top = -r; 
rectF.right =r; 
rectF.bottom = r; 

//白色圓弧 
//透明圓弧 
rTra = (float) (r*radiusScaleTransparent); 
rectFTra.left = -rTra; 
rectFTra.top = -rTra; 
rectFTra.right = rTra; 
rectFTra.bottom = rTra; 

//白色圓 
rWhite = (float) (r*radiusScaleInside); 

//浮出圓環(huán) 
//圓弧 
// 餅狀圖半徑 
rF = (float) (Math.min(mWidth,mHeight)/2*widthScaleRadius*offsetScaleRadius); 
// 餅狀圖繪制區(qū)域 
rectFF.left = -rF; 
rectFF.top = -rF; 
rectFF.right = rF; 
rectFF.bottom = rF; 
...
}

六侵佃、onDraw

onDraw分為繪制扇形麻昼,繪制文本,繪制圖名三個部分馋辈。繪制扇形和文本時需要與Valueanimator的監(jiān)聽值進行計算抚芦,完成動畫;另外還要在Touch時進行交互,完成浮出動畫叉抡。在進行具體的繪制之前尔崔,需要坐標原點平移至中心位置,并且判斷數(shù)據(jù)是否為空褥民。

1季春、繪制扇形

  float currentStartAngle = 0;
  // 當前起始角度
  canvas.save();
  canvas.rotate(mStartAngle);
  float drawAngle;
  for (int i=0; i<mPieData.size(); i++){ 
    PieData pie = mPieData.get(i); 
    if (Math.min(pie.getAngle()-1,animatedValue-currentStartAngle)>=0){ 
      drawAngle = Math.min(pie.getAngle()-1,animatedValue-currentStartAngle); 
    }else { 
      drawAngle = 0; 
    } 
    if (i==angleId){ 
      drawArc(canvas,currentStartAngle,drawAngle,pie,rectFF,rectFTraF,reatFWhite,mPaint); 
    }else { 
      drawArc(canvas,currentStartAngle,drawAngle,pie,rectF,rectFTra,rectFIn,mPaint); 
    } 
    currentStartAngle += pie.getAngle();}canvas.restore();

根據(jù)當前的初始角度旋轉(zhuǎn)畫布。初始化扇形的起始角度消返,通過累加計算出下一次的起始角度载弄。
drawArc用于繪制扇形,和上一篇最后的環(huán)形圖片一樣撵颊,通過一大一小兩個扇形進行補集運算宇攻,獲得可知半徑的及寬度的圓環(huán),只不過這里多了一個為了立體效果而增加的半透明圓弧秦驯。


繪制扇形時尺碰,使用當前的動畫值減去起始角度與當前的扇形經(jīng)過的角度對比取小,作為當前扇形的需要繪制的經(jīng)過角度译隘。減1是為了生存扇形區(qū)域之間的間隔亲桥。

angleId用于Touch時顯示點擊是哪一塊扇形,具體判斷會在TouchEvent中進行固耘。

2题篷、繪制文本

//扇形百分比文字
currentStartAngle = mStartAngle;
for (int i=0; i<mPieData.size(); i++){ 
  PieData pie = mPieData.get(i); 
  mPaint.setColor(percentTextColor); 
  mPaint.setTextSize(percentTextSize); 
  mPaint.setTextAlign(Paint.Align.CENTER); 
  NumberFormat numberFormat =NumberFormat.getPercentInstance(); 
  numberFormat.setMinimumFractionDigits(percentDecimal); 

  //根據(jù)Paint的TextSize計算Y軸的值 
  if (animatedValue>pieAngles[i]-pie.getAngle()/2&&percentFlag) { 
    if (i == angleId) { 
        drawText(canvas,pie,currentStartAngle,numberFormat,true); 
    } else { 
        if (pie.getAngle() > minAngle) { 
            drawText(canvas,pie,currentStartAngle,numberFormat,false); 
         } 
    }
   currentStartAngle += pie.getAngle(); }}
  • 文本是有方向的,無法在畫布旋轉(zhuǎn)后繪制厅目,所以初始化當前扇形的起始角度為PieChart的起始角度番枚。* 然后循環(huán)繪制文本,當扇形繪制到當前區(qū)域的1/2時损敷,開始繪制當前區(qū)域的文字葫笼。為了防止文本遮擋視線,在繪制前需要判斷此扇形經(jīng)過的角度是否大于最小顯示角度拗馒。* angleId用于Touch時顯示點擊是哪一塊扇形路星,具體判斷會在TouchEvent中進行。

    private void drawText(Canvas canvas, PieData pie ,float currentStartAngle, NumberFormat numberFormat,boolean flag){ 
        int textPathX = (int) (Math.cos(Math.toRadians(currentStartAngle + (pie.getAngle() / 2))) * (r + rTra) / 2); 
        int textPathY = (int) (Math.sin(Math.toRadians(currentStartAngle + (pie.getAngle() / 2))) * (r + rTra) / 2); 
        mPoint.x = textPathX; 
        mPoint.y = textPathY; 
        String[] strings; 
        if (flag){ 
            strings = new String[]{pie.getName() + "", numberFormat.format(pie.getPercentage()) + ""}; 
        }else { 
            strings = new String[]{numberFormat.format(pie.getPercentage()) + ""}; 
        }
         textCenter(strings, mPaint, canvas, mPoint, Paint.Align.CENTER);
      }
    

drawText函數(shù)的主要作用就是根據(jù)傳入的Pie诱桂,獲取大小扇形的半徑合除以2洋丐,角度取一半,計算出扇形中心點挥等,然后使用之前介紹的textCenter多行文本居中函數(shù)進行文本繪制友绝。最后累加當前扇形的起始角度,用于下一個扇形使用肝劲。

3迁客、繪制圖名

//餅圖名
mPaint.setColor(centerTextColor);
mPaint.setTextSize(centerTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);

//根據(jù)Paint的TextSize計算Y軸的值
mPoint.x=0;
mPoint.y=0;
String[] strings = new String[]{name+""};
textCenter(strings,mPaint,canvas,mPoint, Paint.Align.CENTER);

繪制圖名的部分就比較簡單了郭宝,和之前繪制單個Pie時類似,獲取x哲泊,y坐標為(0,0),然后使用textCenter多行文本繪制函數(shù)進行文本繪制剩蟀。

七、onTouchEvent

onTouchEvent用于處理當前的點擊事件切威,具體內(nèi)容在第一篇文章中已經(jīng)進行了說明育特,這里使用其中的ACTION_DOWNACTION_UP事件。

public boolean onTouchEvent(MotionEvent event) { 
  if (touchFlag&&mPieData.size()>0){ 
    switch (event.getAction()){ 
      case MotionEvent.ACTION_DOWN: 
        float x = event.getX()-(mWidth/2); 
        float y = event.getY()-(mHeight/2); 
        float touchAngle = 0; 
        if (x<0&&y<0){ 
            touchAngle += 180; 
        }else if (y<0&&x>0){ 
            touchAngle += 360; 
        }else if (y>0&&x<0){ 
            touchAngle += 180; 
        } 
        touchAngle +=Math.toDegrees(Math.atan(y/x)); 
        touchAngle = touchAngle-mStartAngle; 
        if (touchAngle<0){ 
            touchAngle = touchAngle+360; 
        } 
        float touchRadius = (float) Math.sqrt(y*y+x*x); 
        if (rTra< touchRadius && touchRadius< r){ 
          angleId = -Arrays.binarySearch(pieAngles,(touchAngle))-1; 
          invalidate(); 
        } 
        return true; 
    case MotionEvent.ACTION_UP: 
        angleId = -1; 
        invalidate(); 
        return true; 
      } 
    }
 return super.onTouchEvent(event);}

運行之前需要判斷PieChart是否開啟了點擊效果先朦,同時需要判斷數(shù)據(jù)不為空缰冤。

在用戶點擊下的時候,獲取當前的坐標喳魏,計算出這個點與原點的距離以及角度棉浸。通過距離可以判斷出是否點擊在了扇形區(qū)域上,而通過角度可以判斷出點擊了哪一個區(qū)域刺彩。將判斷出的區(qū)域Id傳遞給angleId值迷郑,就像我們之前在onDraw中說的那樣,重新繪制创倔,根據(jù)angleId浮出指定的扇形區(qū)域嗡害。
用戶手指離開屏幕時,重置angleId為默認值畦攘,并使用invalidate()函數(shù)霸妹,重新繪制onDraw中變化的部分。

八知押、小結(jié)

經(jīng)過之前4篇的知識準備叹螟,終于迎來了本章的PieChart的具體實現(xiàn)。在本文中重溫了之前的繪制流程的各個函數(shù)台盯,VlaueAnimator函數(shù)罢绽,以及Canvas、Path的使用方法静盅,并使用這些方法完成了一個自定義餅圖的繪制有缆。在之后的文章中還會進行幾個圖表的實戰(zhàn),比如下面這個曲線圖温亲。
cubic

如果在閱讀過程中,有任何疑問與問題杯矩,歡迎與我聯(lián)系栈虚。

GitHub:https://github.com/Idtk
博客:http://www.idtkm.com
郵箱:Idtkma@gmail.comPieChart源碼請點擊

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市史隆,隨后出現(xiàn)的幾起案子魂务,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粘姜,死亡現(xiàn)場離奇詭異鬓照,居然都是意外死亡,警方通過查閱死者的電腦和手機孤紧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門豺裆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人号显,你說我怎么就攤上這事臭猜。” “怎么了押蚤?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵蔑歌,是天一觀的道長。 經(jīng)常有香客問我揽碘,道長次屠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任雳刺,我火速辦了婚禮劫灶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘煞烫。我一直安慰自己浑此,他們只是感情好,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布滞详。 她就那樣靜靜地躺著凛俱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪料饥。 梳的紋絲不亂的頭發(fā)上蒲犬,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音岸啡,去河邊找鬼原叮。 笑死,一個胖子當著我的面吹牛巡蘸,可吹牛的內(nèi)容都是我干的奋隶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼悦荒,長吁一口氣:“原來是場噩夢啊……” “哼唯欣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起搬味,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤境氢,失蹤者是張志新(化名)和其女友劉穎蟀拷,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萍聊,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡问芬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了寿桨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片此衅。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖牛隅,靈堂內(nèi)的尸體忽然破棺而出炕柔,到底是詐尸還是另有隱情,我是刑警寧澤媒佣,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布匕累,位于F島的核電站,受9級特大地震影響默伍,放射性物質(zhì)發(fā)生泄漏欢嘿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一也糊、第九天 我趴在偏房一處隱蔽的房頂上張望炼蹦。 院中可真熱鬧,春花似錦狸剃、人聲如沸掐隐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虑省。三九已至,卻和暖如春僧凰,著一層夾襖步出監(jiān)牢的瞬間探颈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工训措, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伪节,地道東北人。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓绩鸣,卻偏偏與公主長得像怀大,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子呀闻,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容