Android自定義餅狀圖,支持點擊彈出扇形

上一篇主要是記錄瞭如何實現簡單的折線圖,支持點擊彈出提示;這篇主要是實現另外一種圖表–餅狀圖。

1 先上效果圖

圖片1

2 分析

第一看看到這個圖,有過畫扇形經驗的同學會不屑,這個不簡單嗎?主要就是將所有的值相加,然後用每個值去除於總值,得到對於的一個扇形的角度,逐個畫上去就好。這個說法大體是對的,但等到真正實施,還是有些小細節需要注意的。

for (int i = 0; i < numbers.size(); i++) {
    if (i == numbers.size() - 1) {
        sweepAngle = 360 - startAngle;
    } else {
        sweepAngle = (int) (numbers.get(i) * 1.0f / total * 360);
    }

看上邊的代碼,爲什麼最後一個我沒有用最後的item值去除於總值,而是通過用360減去其他所有角度的總和呢?因爲,我們使用除法進行計算,會很有可能得到帶小數的結果,但畫扇形接受的角度都是int型,這就會產生誤差,導致最後不能剛好畫滿整個圓。於是需要採用這種方式來避免

3 支持點擊彈出扇形

好了,一個簡單的餅狀圖我們就完成了。但我們肯定不能僅限於此啊,現在開源的圖表庫框架都是帶有很多功能的。我們也可以按照他們的思路,來添加一些功能上去。

圖片2
圖片3

3.1 看上邊兩張圖,很多圖表庫都支持該種操作,但點擊某個扇形時,扇形會向外彈出,感覺是被切出來一樣。

3.2 看了效果,我們可以先整理下思路:

3.2.1 首先,需要判斷手指是點擊了哪個扇形(區域),怎麼判斷呢?
我這裏有一種方式,可以保存每個扇形對於的角度的範圍(把畫扇形的起始角度當作零度);當發生點擊事件時,可以通過點擊點與圓點連線,計算出該線處在的角度,通過比較每個扇形的角度範圍,判斷點擊發生在哪個扇形上。
3.2.2 點擊區域問題解決了,還有另外一個重點:如何讓扇形彈出,並且保證彈出的兩邊是對稱的?下邊用個圖來說,這裏需要用到數學的知識,不記得的需要上網搜搜了

這裏寫圖片描述

3.2.3 關鍵代碼
@Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            float x = event.getX();
            float y = event.getY();
            int radius = 0;
            // 第一象限
            if (x >= getMeasuredWidth() / 2 && y >= getMeasuredHeight() / 2) {
                radius = (int) (Math.atan((y - getMeasuredHeight() / 2) * 1.0f
                        / (x - getMeasuredWidth() / 2)) * 180 / Math.PI);
            }
            // 第二象限
            if (x <= getMeasuredWidth() / 2 && y >= getMeasuredHeight() / 2) {
                radius = (int) (Math.atan((getMeasuredWidth() / 2 - x)
                        / (y - getMeasuredHeight() / 2))
                        * 180 / Math.PI + 90);
            }
            // 第三象限
            if (x <= getMeasuredWidth() / 2 && y <= getMeasuredHeight() / 2) {
                radius = (int) (Math.atan((getMeasuredHeight() / 2 - y)
                        / (getMeasuredWidth() / 2 - x))
                        * 180 / Math.PI + 180);
            }
            // 第四象限
            if (x >= getMeasuredWidth() / 2 && y <= getMeasuredHeight() / 2) {
                radius = (int) (Math.atan((x - getMeasuredWidth() / 2)
                        / (getMeasuredHeight() / 2 - y))
                        * 180 / Math.PI + 270);
            }
            for (int i = 0; i < points.size(); i++) {
                Point point = points.get(i);
                if (point.x <= radius && point.y >= radius) {
                    select = i;
                    // Toast.makeText(context, "點擊了" + point,
                    // Toast.LENGTH_SHORT)
                    // .show();
                    invalidate();
                    return true;
                }
            }
            return true;
        }
        return super.onTouchEvent(event);
    }
看到,爲了角度計算好理解,我是通過劃分四個象限來進行的;其中,採用point保存了每個扇形的起始和結束的角度,並添加到points中

4 整個餅狀圖的源碼送上

public class CircleChartView extends View {

    private List<Double> numbers;
    private List<Point> points;
    private double total;
    private RectF normalOval;
    private RectF selectOval;

    private Paint paint;
    private Context context;

    public static final int[] colors = { android.R.color.holo_blue_light,
            android.R.color.holo_green_light, android.R.color.holo_red_light,
            android.R.color.holo_orange_light, android.R.color.holo_purple };

    public CircleChartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        numbers = new ArrayList<Double>();

        normalOval = new RectF();
        selectOval = new RectF();

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Style.FILL);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST
                || heightMode == MeasureSpec.AT_MOST) {
            width = 400;
            height = 400;
        }
        setMeasuredDimension(width, height);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        normalOval.left = (float) (getMeasuredWidth() * 0.1);
        normalOval.top = (float) (getMeasuredHeight() * 0.1);
        normalOval.right = (float) (getMeasuredWidth() * 0.9);
        normalOval.bottom = (float) (getMeasuredHeight() * 0.9);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!numbers.isEmpty()) {
            int startAngle = 0;
            int sweepAngle = 0;
            for (int i = 0; i < numbers.size(); i++) {
                if (i == numbers.size() - 1) {
                    sweepAngle = 360 - startAngle;
                } else {
                    sweepAngle = (int) (numbers.get(i) * 1.0f / total * 360);
                }
                if (select >= 0 && i == select) {
                    selectOval.left = (float) (getMeasuredWidth() * 0.1);
                    selectOval.top = (float) (getMeasuredHeight() * 0.1);
                    selectOval.right = (float) (getMeasuredWidth() * 0.9);
                    selectOval.bottom = (float) (getMeasuredHeight() * 0.9);
                    Point point = points.get(select);
                    int middle = (point.x + point.y) / 2;
                    if (middle <= 90) {
                        int top = (int) (Math.sin(Math.toRadians(middle)) * 15);
                        int left = (int) (Math.cos(Math.toRadians(middle)) * 15);
                        selectOval.left += left;
                        selectOval.right += left;
                        selectOval.top += top;
                        selectOval.bottom += top;
                    }
                    if (middle > 90 && middle <= 180) {
                        middle = 180 - middle;
                        int top = (int) (Math.sin(Math.toRadians(middle)) * 15);
                        int left = (int) (Math.cos(Math.toRadians(middle)) * 15);
                        selectOval.left -= left;
                        selectOval.right -= left;
                        selectOval.top += top;
                        selectOval.bottom += top;
                    }
                    if (middle > 180 && middle <= 270) {
                        middle = 270 - middle;
                        int left = (int) (Math.sin(Math.toRadians(middle)) * 15);
                        int top = (int) (Math.cos(Math.toRadians(middle)) * 15);
                        selectOval.left -= left;
                        selectOval.right -= left;
                        selectOval.top -= top;
                        selectOval.bottom -= top;
                    }
                    if (middle > 270 && middle <= 360) {
                        middle = 360 - middle;
                        int top = (int) (Math.sin(Math.toRadians(middle)) * 15);
                        int left = (int) (Math.cos(Math.toRadians(middle)) * 15);
                        selectOval.left += left;
                        selectOval.right += left;
                        selectOval.top -= top;
                        selectOval.bottom -= top;
                    }
                    paint.setColor(getResources().getColor(colors[i]));
                    canvas.drawArc(selectOval, startAngle, sweepAngle, true,
                            paint);
                } else {
                    paint.setColor(getResources().getColor(colors[i]));
                    canvas.drawArc(normalOval, startAngle, sweepAngle, true,
                            paint);
                }
                points.get(i).x = startAngle;
                points.get(i).y = startAngle + sweepAngle;
                startAngle += sweepAngle;
            }
        }
    }

    public void setNumbers(List<Double> numbers) {
        this.numbers.clear();
        this.numbers.addAll(numbers);
        points = new ArrayList<Point>();
        for (Double item : numbers) {
            total += item;
            Point point = new Point();
            points.add(point);
        }
        invalidate();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            float x = event.getX();
            float y = event.getY();
            int radius = 0;
            // 第一象限
            if (x >= getMeasuredWidth() / 2 && y >= getMeasuredHeight() / 2) {
                radius = (int) (Math.atan((y - getMeasuredHeight() / 2) * 1.0f
                        / (x - getMeasuredWidth() / 2)) * 180 / Math.PI);
            }
            // 第二象限
            if (x <= getMeasuredWidth() / 2 && y >= getMeasuredHeight() / 2) {
                radius = (int) (Math.atan((getMeasuredWidth() / 2 - x)
                        / (y - getMeasuredHeight() / 2))
                        * 180 / Math.PI + 90);
            }
            // 第三象限
            if (x <= getMeasuredWidth() / 2 && y <= getMeasuredHeight() / 2) {
                radius = (int) (Math.atan((getMeasuredHeight() / 2 - y)
                        / (getMeasuredWidth() / 2 - x))
                        * 180 / Math.PI + 180);
            }
            // 第四象限
            if (x >= getMeasuredWidth() / 2 && y <= getMeasuredHeight() / 2) {
                radius = (int) (Math.atan((x - getMeasuredWidth() / 2)
                        / (getMeasuredHeight() / 2 - y))
                        * 180 / Math.PI + 270);
            }
            for (int i = 0; i < points.size(); i++) {
                Point point = points.get(i);
                if (point.x <= radius && point.y >= radius) {
                    select = i;
                    // Toast.makeText(context, "點擊了" + point,
                    // Toast.LENGTH_SHORT)
                    // .show();
                    invalidate();
                    return true;
                }
            }
            return true;
        }
        return super.onTouchEvent(event);
    }

    private int select = -1;
}

5 總結

可以看到,包括餅狀圖描繪、點擊效果等,更多的是需要一些數學知識進行計算。所以,要想搞好開發,還是要全面發展,哈哈哈!

6 源碼下載

源碼下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章