效果圖
需求
- 根據(jù)某種類型的數(shù)據(jù)大小占總數(shù)據(jù)大小的百分比來決定其在餅狀圖中圓環(huán)弧的大小赴背。(百分比系數(shù)乘以360就是圓環(huán)弧的度數(shù))
- 不同類型數(shù)據(jù)可以設(shè)置不同圓環(huán)弧顏色毛雇。
- 點擊圓環(huán)上任意一點烹困,可以判斷其點擊的是何種類型數(shù)據(jù)玄妈。
實現(xiàn)步驟
定義數(shù)據(jù)結(jié)構(gòu)
因為需要根據(jù)所傳數(shù)據(jù)繪制不同餅狀圖,所以首先定義其數(shù)據(jù)類型:
//數(shù)據(jù)的類型
private String type;
//數(shù)據(jù)的大小
private float value;
//數(shù)據(jù)類型所對應(yīng)圓環(huán)弧的顏色資源Id
private int colorId;
繪制步驟
上圖中主要繪制的地方有三處:
1.外圓白色的邊框髓梅。
2.不同顏色的弧拟蜻。
3.白色的內(nèi)圓。
所以對應(yīng)的我們需要三個畫筆(Paint)枯饿。
第一個畫筆用來繪制外圓白色的邊框瞭郑。
第二個畫筆用來繪制不同顏色的弧。
第三個畫筆用來繪制白色的內(nèi)圓鸭你。
測量餅狀圖的大小
因為餅狀圖最終的呈現(xiàn)的是一個圓屈张,所以其width和height是相等的擒权,也就是外圓的直徑。因此我們需要重寫onMeasure方法阁谆。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//mRadius是外圓半徑
int length = (int)(2 * mRadius);
setMeasuredDimension(length, length);
}
繪制動畫
因為要求繪制圓環(huán)的時候帶一個逐步繪制的動畫碳抄,所以我們需要繼承Animation類來完成動畫效果。
這里只需重寫Animation類的applyTransformation方法场绿。
處理點擊事件
處理點擊事件相對于繪制而言剖效,邏輯上要稍微復(fù)雜點。如上圖中焰盗,黃色圓環(huán)弧璧尸、紅色圓環(huán)弧、橙色圓環(huán)弧的百分比系數(shù)分別是0.5熬拒、0.25爷光、0.25,那么對應(yīng)的圓環(huán)弧度數(shù)為180°澎粟、90°蛀序、90°。
(1)響應(yīng)點擊的事件只能發(fā)生在圓環(huán)中間活烙。
(2)在條件(1)的基礎(chǔ)上徐裸,計算出圓頂點順時針到事件點擊處的角度。
如果點擊是紅色圓環(huán)弧中間位置啸盏,那么從圓定點順時針到點擊處走過的度數(shù)是(180°+45° = )225°重贺,225°是小于黃色圓環(huán)弧度數(shù)加上紅色圓環(huán)弧的度數(shù)。
上圖是點擊事件坐標(biāo)圖回懦,坐標(biāo)原點就是餅狀圖的左上頂點檬姥。如果設(shè)外圓半徑為R,內(nèi)容半徑是0.72*R粉怕,那么圓心坐標(biāo)是(R,R)健民。
假設(shè)點擊事件的坐標(biāo)為(X,Y)。那么只要其滿足:
(0.72R)2≤(X-R)2 + (Y-R)2≤R2
就可以判斷點擊的事件發(fā)生在圓環(huán)弧上贫贝。
接著我們需要計算出圓頂點順時針到事件點擊處的角度θ秉犹。如下圖,將圓分為1稚晚、2崇堵、3、4四個相等部分客燕。
1.若點擊事件發(fā)生在第一部分鸳劳。
則θ = Math.atan2(X - R, R - X) * 180 / PI。
2.若點擊事件發(fā)生在第二部分也搓。
則θ = Math.atan2(Y - R, X - R) * 180 / PI + 90°赏廓。
3.若點擊事件發(fā)生在第三部分涵紊。
則θ = Math.atan2(R - X, Y - R) * 180 / PI + 180°。
4.若點擊事件發(fā)生在第四部分幔摸。
則θ = Math.atan2(R - Y, R - X) * 180 / PI + 180°摸柄。
注:PI = 3.1415
計算出角度θ后,我們可以確定點擊事件發(fā)生在哪段圓環(huán)弧上了既忆。
項目代碼
最后直接上干貨驱负。
public class PieChartView extends View {
//餅圖白色輪廓畫筆
private Paint mOuterLinePaint;
//餅狀圖畫筆
private Paint mPiePaint;
//內(nèi)圓畫筆
private Paint mInnerPaint;
//餅狀圖外圓半徑
private float mRadius = DensityUtil.dip2px(getContext(), 60) + OUTER_LINE_WIDTH;
//構(gòu)成餅狀圖的數(shù)據(jù)集合
private List<PieData> mPieDataList;
//繪制弧形的sweep數(shù)組
private float[] mPieSweep;
//餅狀圖動畫效果
private PieChartAnimation mAnimation;
//初始畫弧所在的角度
private static final int START_DEGREE = -90;
private static final int PIE_ANIMATION_VALUE = 100;
//外圓邊框的寬度
private static int OUTER_LINE_WIDTH = 3;
//動畫時間
private static final int ANIMATION_DURATION = 800;
private RectF mRectF = new RectF();
//圓周率
private static final float PI = 3.1415f;
private static final int PART_ONE = 1;
private static final int PART_TWO = 2;
private static final int PART_THREE = 3;
private static final int PART_FOUR = 4;
public void setOnSpecialTypeClickListener(OnSpecialTypeClickListener listener) {
this.mListener = listener;
}
private OnSpecialTypeClickListener mListener;
public PieChartView(Context context) {
super(context);
init();
}
public PieChartView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PieChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//初始化畫筆和效果動畫
private void init() {
mOuterLinePaint = new Paint();
mOuterLinePaint.setAntiAlias(true);
mOuterLinePaint.setStyle(Style.STROKE);
mOuterLinePaint.setStrokeWidth(OUTER_LINE_WIDTH);
mOuterLinePaint.setColor(Color.WHITE);
mPiePaint = new Paint();
mPiePaint.setAntiAlias(true);
mPiePaint.setStyle(Style.FILL);
//設(shè)置動畫
mAnimation = new PieChartAnimation();
mAnimation.setDuration(ANIMATION_DURATION);
mInnerPaint = new Paint();
mInnerPaint.setColor(Color.WHITE);
mInnerPaint.setStyle(Style.FILL);
mInnerPaint.setAntiAlias(true);
initRectF();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mPieDataList != null && !mPieDataList.isEmpty()) {
//起始是從-90°位置開始畫
float pieStart = START_DEGREE;
if (mPieSweep == null) {
mPieSweep = new float[mPieDataList.size()];
}
for (int i = 0; i < mPieDataList.size(); i++) {
//設(shè)置弧形顏色
mPiePaint.setColor(getResources().getColor(mPieDataList.get(i).getColorId()));
//繪制弧形區(qū)域,以構(gòu)成餅狀圖
float pieSweep = mPieDataList.get(i).getValue() * 360;
canvas.drawArc(mRectF, pieStart, mPieSweep[i], true, mPiePaint);
canvas.drawArc(mRectF, pieStart, mPieSweep[i], true, mOuterLinePaint);
//獲取下一個弧形的起點
pieStart += pieSweep;
}
} else {
//無數(shù)據(jù)時患雇,顯示灰色圓環(huán)
mPiePaint.setColor(Color.parseColor("#dadada"));//灰色
canvas.drawCircle(mRadius, mRadius, mRadius, mPiePaint);
}
drawInnerCircle(canvas);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int length = (int) (2 * mRadius);
setMeasuredDimension(length, length);
}
/**
* 設(shè)置需要繪制的數(shù)據(jù)集合
*/
public void setPieDataList(List<PieData> pieDataList) {
this.mPieDataList = pieDataList;
if (mPieSweep == null) {
mPieSweep = new float[mPieDataList.size()];
}
startAnimation(mAnimation);
}
/**
* 設(shè)置外圓半徑
*
* @param radius 外圓半徑 dp為單位
**/
public void setOuterRadius(float radius) {
this.mRadius = DensityUtil.dip2px(getContext(), radius) + OUTER_LINE_WIDTH ;
initRectF();
}
/**
* 初始化繪制弧形所在矩形的四點坐標(biāo)
**/
private void initRectF() {
mRectF.left = 0;
mRectF.top = 0;
mRectF.right = 2 * mRadius;
mRectF.bottom = 2 * mRadius;
}
private class PieChartAnimation extends Animation {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
mPieSweep = new float[mPieDataList.size()];
if (interpolatedTime < 1.0f) {
for (int i = 0; i < mPieDataList.size(); i++) {
mPieSweep[i] = (mPieDataList.get(i).getValue() * PIE_ANIMATION_VALUE) * interpolatedTime / PIE_ANIMATION_VALUE * 360;
}
} else {
for (int i = 0; i < mPieDataList.size(); i++) {
mPieSweep[i] = mPieDataList.get(i).getValue() * 360;
}
}
invalidate();
}
}
protected void drawInnerCircle(Canvas canvas) {
canvas.drawCircle(mRadius, mRadius, (float) (mRadius * 0.72), mInnerPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
doOnSpecialTypeClick(event);
break;
}
return super.onTouchEvent(event);
}
private void doOnSpecialTypeClick(MotionEvent event) {
if (mPieDataList == null || mPieDataList.isEmpty()) return;
float eventX = event.getX();
float eventY = event.getY();
double alfa = 0;
float startArc = 0;
//點擊的位置到圓心距離的平方
double distance = Math.pow(eventX - mRadius, 2) + Math.pow(eventY - mRadius, 2);
//判斷點擊的坐標(biāo)是否在環(huán)內(nèi)
if (distance < Math.pow(mRadius, 2) && distance > Math.pow(0.72 * mRadius, 2)) {
int which = touchOnWhichPart(event);
switch (which) {
case PART_ONE:
alfa = Math.atan2(eventX - mRadius, mRadius - eventY) * 180 / PI;
break;
case PART_TWO:
alfa = Math.atan2(eventY - mRadius, eventX - mRadius) * 180 / PI + 90;
break;
case PART_THREE:
alfa = Math.atan2(mRadius - eventX, eventY - mRadius) * 180 / PI + 180;
break;
case PART_FOUR:
alfa = Math.atan2(mRadius - eventY, mRadius - eventX) * 180 / PI + 270;
break;
}
for (PieData data : mPieDataList) {
startArc = startArc + data.getValue() * 360;
if (alfa != 0 && alfa < startArc) {
if (mListener != null) mListener.onSpecialTypeClick(data.getType());
break;
}
}
}
}
/**
* 4 | 1
* -----|-----
* 3 | 2
* 圓被分成四等份跃脊,判斷點擊在園的哪一部分
*/
private int touchOnWhichPart(MotionEvent event) {
if (event.getX() > mRadius) {
if (event.getY() > mRadius) return PART_TWO;
else return PART_ONE;
} else {
if (event.getY() > mRadius) return PART_THREE;
else return PART_FOUR;
}
}
public interface OnSpecialTypeClickListener {
void onSpecialTypeClick(String type);
}
public static class PieData {
private String type;
private float value;
private int colorId;
public PieData(String type, float value, int colorId) {
this.type = type;
this.value = value;
this.colorId = colorId;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public float getValue() {
return value;
}
public void setValue(float value) {
this.value = value;
}
public int getColorId() {
return colorId;
}
public void setColorId(int colorId) {
this.colorId = colorId;
}
}
}