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 源码下载

源码下载

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: 可以使用Android提供的Canvas和Paint类来绘制空心扇形统计图。具体步骤如下: 1. 创建一个继承自View的自定义View,重写onDraw方法。 2. 在onDraw方法中创建一个Paint对象,设置画笔的颜色和样式,如: ``` Paint paint = new Paint(); paint.setColor(Color.BLUE); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(5); ``` 3. 使用Canvas的drawArc方法来绘制扇形,需要传入扇形的矩形区域、起始角度和扫过的角度。如: ``` RectF rectF = new RectF(100, 100, 500, 500); canvas.drawArc(rectF, 0, 90, false, paint); ``` 其中,RectF对象用于指定扇形的位置和大小,前两个参数是左上角的坐标,后两个参数是右下角的坐标。第三个参数是起始角度,第四个参数是扫过的角度,第五个参数指定是否连接中心点。 4. 如果要绘制多个扇形,可以根据数据计算每个扇形的起始角度和扫过的角度。如: ``` float[] data = {30, 40, 50, 60}; float sum = 0; for (float value : data) { sum += value; } float startAngle = 0; for (float value : data) { float sweepAngle = value / sum * 360; canvas.drawArc(rectF, startAngle, sweepAngle, false, paint); startAngle += sweepAngle; } ``` 其中,data数组保存了每个扇形的数值,sum是所有数值的和,startAngle表示当前扇形的起始角度,sweepAngle表示当前扇形扫过的角度。 5. 最后,在布局文件中将自定义View添加到布局中即可。 以上是绘制空心扇形统计图的基本步骤,具体细节可以根据需要进行调整。 ### 回答2: 要实现Android自定义空心扇形统计图,你可以使用自定义View来绘制扇形和文字。 首先,创建一个继承自View的自定义View类,命名为PieChartView。在该类的构造方法中,初始化画笔和相关属性,如颜色、宽度等。 接下来,在PieChartView的onMeasure方法中确定View的大小,这取决于父容器给出的测量要求。 在PieChartView的onDraw方法中,使用Canvas和Paint来绘制扇形和文字。根据给定的数据,计算出每个扇形所占的角度,然后根据角度和颜色依次绘制扇形。 为了实现空心效果,你可以使用Canvas的drawArc方法来绘制空心扇形设置Paint的Style为STROKE,并设置边框的宽度。这样,只会绘制扇形的边框而不会填充颜色。 接着,在绘制扇形时,你可以通过设置一个起始角度来绘制每个扇形,使它们按顺时针或逆时针排列。绘制文字时,可以根据扇形的中心角度和半径来确定文字的位置。 最后,你需要在使用PieChartView的Activity或Fragment中设置相关数据,如每个扇形的角度和颜色。然后将PieChartView添加到布局中。 通过这些步骤,你就能够实现一个简单的Android自定义空心扇形统计图。当然,你还可以进一步定制化,添加动画效果或触摸交互等功能,使统计图更加生动和实用。 ### 回答3: Android自定义空心扇形统计图可以通过继承View类,重写onDraw方法来实现。具体步骤如下: 1. 在XML布局文件中,声明一个自定义View的容器,如LinearLayout或RelativeLayout。 2. 在Java代码中,创建一个继承自View的类,重写onDraw方法。 3. 在onDraw方法中,先通过Canvas对象绘制一个圆形。 4. 再通过Path对象绘制一个扇形,可以通过Path.addArc方法来实现。 5. 设置扇形的起始角度和扇形的角度大小,可以根据所需的数据来计算。 6. 设置扇形的颜色,可以通过Paint对象的setColor方法来设置。 7. 设置扇形的样式为空心,可以通过Paint对象的setStyle方法来设置。 8. 设置扇形的边框宽度和颜色,可以通过Paint对象的setStrokeWidth和setColor方法来设置。 9. 最后调用Canvas对象的drawPath方法来绘制扇形。 10. 在主Activity中,实例化自定义View类的对象,并将其添加到容器中。 通过以上步骤,就可以实现一个自定义的空心扇形统计图。可以根据需要来设置扇形的起始角度、角度大小、颜色和边框样式等属性,以达到所需的效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值