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ù)。


點擊事件坐標(biāo)圖

上圖是點擊事件坐標(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;
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市苛吱,隨后出現(xiàn)的幾起案子酪术,更是在濱河造成了極大的恐慌,老刑警劉巖又谋,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拼缝,死亡現(xiàn)場離奇詭異娱局,居然都是意外死亡彰亥,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門衰齐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來任斋,“玉大人,你說我怎么就攤上這事耻涛》峡幔” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵抹缕,是天一觀的道長澈蟆。 經(jīng)常有香客問我,道長卓研,這世上最難降的妖魔是什么趴俘? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮奏赘,結(jié)果婚禮上寥闪,老公的妹妹穿的比我還像新娘。我一直安慰自己磨淌,他們只是感情好疲憋,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著梁只,像睡著了一般缚柳。 火紅的嫁衣襯著肌膚如雪埃脏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天喂击,我揣著相機(jī)與錄音剂癌,去河邊找鬼。 笑死翰绊,一個胖子當(dāng)著我的面吹牛佩谷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播监嗜,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼谐檀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了裁奇?” 一聲冷哼從身側(cè)響起桐猬,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎刽肠,沒想到半個月后溃肪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡音五,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年惫撰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躺涝。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡厨钻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出坚嗜,到底是詐尸還是另有隱情夯膀,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布苍蔬,位于F島的核電站诱建,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏碟绑。R本人自食惡果不足惜俺猿,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蜈敢。 院中可真熱鬧辜荠,春花似錦、人聲如沸抓狭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至午笛,卻和暖如春惭蟋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背药磺。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工告组, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人癌佩。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓木缝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親围辙。 傳聞我的和親對象是個殘疾皇子我碟,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

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