Android自定義餅狀圖,且能區分點擊的區域

cubina 8年前發布 | 19K 次閱讀 Android Android開發 移動開發

效果圖

Android自定義餅狀圖,且能區分點擊的區域

餅狀圖

需求

  1. 根據某種類型的數據大小占總數據大小的百分比來決定其在餅狀圖中圓環弧的大小。(百分比系數乘以360就是圓環弧的度數)
  2. 不同類型數據可以設置不同圓環弧顏色。
  3. 點擊圓環上任意一點,可以判斷其點擊的是何種類型數據。

實現步驟

定義數據結構

因為需要根據所傳數據繪制不同餅狀圖,所以首先定義其數據類型:

    //數據的類型
    private String type;
    //數據的大小
    private float value;
    //數據類型所對應圓環弧的顏色資源Id
    private int colorId;

繪制步驟

上圖中主要繪制的地方有三處:
1.外圓白色的邊框。
2.不同顏色的弧。
3.白色的內圓。
所以對應的我們需要三個畫筆(Paint)。
第一個畫筆用來繪制外圓白色的邊框。
第二個畫筆用來繪制不同顏色的弧。
第三個畫筆用來繪制白色的內圓。

測量餅狀圖的大小

因為餅狀圖最終的呈現的是一個圓,所以其width和height是相等的,也就是外圓的直徑。因此我們需要重寫onMeasure方法。

@Override 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   //mRadius是外圓半徑
    int length = (int)(2 * mRadius);
    setMeasuredDimension(length, length);
}

繪制動畫

因為要求繪制圓環的時候帶一個逐步繪制的動畫,所以我們需要繼承Animation類來完成動畫效果。
這里只需重寫Animation類的applyTransformation方法。

處理點擊事件

處理點擊事件相對于繪制而言,邏輯上要稍微復雜點。如上圖中,黃色圓環弧、紅色圓環弧、橙色圓環弧的百分比系數分別是0.5、0.25、0.25,那么對應的圓環弧度數為180°、90°、90°。
(1)響應點擊的事件只能發生在圓環中間。
(2)在條件(1)的基礎上,計算出圓頂點順時針到事件點擊處的角度。
如果點擊是紅色圓環弧中間位置,那么從圓定點順時針到點擊處走過的度數是(180°+45° = )225°,225°是小于黃色圓環弧度數加上紅色圓環弧的度數。

Android自定義餅狀圖,且能區分點擊的區域

點擊事件坐標圖


上圖是點擊事件坐標圖,坐標原點就是餅狀圖的左上頂點。如果設外圓半徑為R,內容半徑是0.72*R,那么圓心坐標是(R,R)。
假設點擊事件的坐標為(X,Y)。那么只要其滿足:

(0.72R)2≤(X-R)2 + (Y-R)2≤R2

就可以判斷點擊的事件發生在圓環弧上。
接著我們需要計算出圓頂點順時針到事件點擊處的角度θ。如下圖,將圓分為1、2、3、4四個相等部分。

Android自定義餅狀圖,且能區分點擊的區域

四等分圓


1.若點擊事件發生在第一部分。
則θ = Math.atan2(X - R, R - X) 180 / PI。
2.若點擊事件發生在第二部分。
則θ = Math.atan2(Y - R, X - R)
180 / PI + 90°。
3.若點擊事件發生在第三部分。
則θ = Math.atan2(R - X, Y - R) 180 / PI + 180°。
4.若點擊事件發生在第四部分。
則θ = Math.atan2(R - Y, R - X)
180 / PI + 180°。

注:PI = 3.1415

計算出角度θ后,我們可以確定點擊事件發生在哪段圓環弧上了。

項目代碼

最后直接上干貨。

public class PieChartView extends View {
    //餅圖白色輪廓畫筆
    private Paint mOuterLinePaint;
    //餅狀圖畫筆
    private Paint mPiePaint;
    //內圓畫筆
    private Paint mInnerPaint;
    //餅狀圖外圓半徑
    private float mRadius = DensityUtil.dip2px(getContext(), 60) + OUTER_LINE_WIDTH;
    //構成餅狀圖的數據集合
    private List<PieData> mPieDataList;
    //繪制弧形的sweep數組
    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);
    //設置動畫
    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++) {
            //設置弧形顏色
            mPiePaint.setColor(getResources().getColor(mPieDataList.get(i).getColorId()));
            //繪制弧形區域,以構成餅狀圖
            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 {
        //無數據時,顯示灰色圓環
        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);
}

/**
 * 設置需要繪制的數據集合
 */
public void setPieDataList(List<PieData> pieDataList) {
    this.mPieDataList = pieDataList;
    if (mPieSweep == null) {
        mPieSweep = new float[mPieDataList.size()];
    }
    startAnimation(mAnimation);
}

/**
 * 設置外圓半徑
 *
 * @param radius 外圓半徑 dp為單位
 **/
public void setOuterRadius(float radius) {
    this.mRadius = DensityUtil.dip2px(getContext(), radius) + OUTER_LINE_WIDTH ;
    initRectF();
}

/**
 * 初始化繪制弧形所在矩形的四點坐標
 **/
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);
    //判斷點擊的坐標是否在環內
    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;
    }
}

}</code></pre>

via:http://www.jianshu.com/p/f50dbae3a07f
 

 本文由用戶 cubina 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!