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

效果圖

餅狀圖

需求

  1. 根據(jù)某種類型的數(shù)據(jù)大小占總數(shù)據(jù)大小的百分比來決定其在餅狀圖中圓環(huán)弧的大小。(百分比系數(shù)乘以360就是圓環(huán)弧的度數(shù))
  2. 不同類型數(shù)據(jù)可以設(shè)置不同圓環(huán)弧顏色。
  3. 點擊圓環(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ù)。


點擊事件坐標圖

上圖是點擊事件坐標圖,坐標原點就是餅狀圖的左上頂點。如果設(shè)外圓半徑為R,內(nèi)容半徑是0.72*R,那么圓心坐標是(R,R)。
假設(shè)點擊事件的坐標為(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();
    }

    /**
     * 初始化繪制弧形所在矩形的四點坐標
     **/
    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);
        //判斷點擊的坐標是否在環(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;
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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