本文是我在閱讀了網(wǎng)絡上其他作者的優(yōu)秀內(nèi)容之后做的摘錄轉(zhuǎn)載潮酒,其中有對內(nèi)容的補充秒梅。
原來地址:http://www.idtkm.com/customview/customview5/
(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); }
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_DOWN與ACTION_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),比如下面這個曲線圖温亲。如果在閱讀過程中,有任何疑問與問題杯矩,歡迎與我聯(lián)系栈虚。
GitHub:https://github.com/Idtk
博客:http://www.idtkm.com
郵箱:Idtkma@gmail.comPieChart源碼請點擊