给之前绘制的饼状图增加点击扩大突出效果

在这篇博客【http://www.cnblogs.com/webor2006/p/7401912.html】中已经学习过如何绘制一个饼状图了,这里在其基础上进行拓展,先上最终效果:

这种效果在开源库中经常遇见,而正好有一个类似的效果出现在公司的项目中,不过公司的更加酷炫,所以先把这个研究清楚之后再来把公司的那个效果也记录一下,好了,话不多话,进入编码【注:完全是基于上次现有的实现代码去拓展滴】。

添加触摸事件:

/**
 * 带百分比的饼图效果,实现思路:
 * 1、最内测的扇形组成的的圆形区域
 * 2、中间的短线段的绘制
 * 3、最外侧的文本的绘制
 * 4、加上点击事件
 */
public class PieView extends View {

    /* 数据源,由外部传过来 */
    private List<PieEntity> pieEntities;
    /* 控件的大小 */
    private int height, width;
    /* 圆的半径 */
    private int radius;
    /* 扇形组成圆形的外接短形 */
    private RectF rectF;
    /* 绘制扇形的画笔 */
    private Paint paint;
    /* 总占比 */
    private int totalValue;
    private Path path;
    /* 由于线条的颜色需要一致所以重新新建一个Paint专用于绘制直线 */
    private Paint linePaint;

    public PieView(Context context) {
        this(context, null);
    }

    public PieView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        rectF = new RectF();

        paint = new Paint();
        paint.setAntiAlias(true);

        path = new Path();

        linePaint = new Paint();
        linePaint.setAntiAlias(true);
        linePaint.setColor(Color.BLACK);
        linePaint.setTextSize(20);
    }

    //当自定义控件的尺寸已经决定好的时候回调
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //初始化控制的宽高
        this.width = w;
        this.height = h;

        //为了防止绘制后超出屏幕区域,获取屏幕的宽高的较小值
        int min = Math.min(this.width, this.height);
        this.radius = (int) ((min * 0.7f) / 2);//圆的半径取屏幕的7成
        rectF.left = -radius;
        rectF.top = -radius;
        rectF.right = radius;
        rectF.bottom = radius;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();//由于用到了translate画片所以需要save一下
        //1、将画布的坐标系移到屏幕中间,这样方便去绘制圆
        canvas.translate(this.width / 2, this.height / 2);
        //2、绘制扇形
        drawPie(canvas);
        canvas.restore();
    }


    private void drawPie(Canvas canvas) {
        float startAngle = 0;
        for (int i = 0; i < pieEntities.size(); i++) {
            PieEntity pieEntity = pieEntities.get(i);
            paint.setColor(pieEntity.getColor());//设置扇形的颜色
            path.moveTo(0, 0);//需要将其移动到坐标系位置
            //其中减1是为了让各扇形区域之间有一个间隙
            float sweepAngle = (pieEntity.getValue() / totalValue) * 360 - 1;
            path.arcTo(rectF, startAngle, sweepAngle);
            canvas.drawPath(path, paint);

            //绘制每个扇形对应的直线
            double a = Math.toRadians(startAngle + sweepAngle / 2);//将角度转化为弧度
            float startX = (float) (radius * Math.cos(a));
            float startY = (float) (radius * Math.sin(a));
            float endX = (float) ((radius + 30) * Math.cos(a));
            float endY = (float) ((radius + 30) * Math.sin(a));
            canvas.drawLine(startX, startY, endX, endY, linePaint);

            //每一个扇形区域的起始点就是上一个扇形区域的终点
            startAngle += sweepAngle + 1;
            //在每次绘制扇形之后需要对path进行重置操作,这样就可以清除上一次绘制path使用的画笔的相关记录
            path.reset();

            //绘制文本
            //1、文本内容
            String percent = String.format("%.1f", pieEntity.getValue() / totalValue * 100);
            percent += "%";
            Log.d("cexo", "percent:" + percent + ";startAngle:" + startAngle);
            //2、文本的位置
            if ((startAngle - sweepAngle / 2) % 360.0f >= 90.0f && (startAngle - sweepAngle / 2) % 360.0f <= 270.0f) {
                //如果是90度~270度的文字,则将其x左移一个文本宽的位置来避免显示上的问题
                //计算文本的宽度
                float textWidth = linePaint.measureText(percent);
                canvas.drawText(percent, endX - textWidth, endY, linePaint);//左移文本让其显示在直线的左边
            } else {
                canvas.drawText(percent, endX, endY, linePaint);
            }
        }
    }

    /**
     * 设置饼图的源数据
     */
    public void setData(List<PieEntity> pieEntities) {
        this.pieEntities = pieEntities;
        for (PieEntity pieEntity : pieEntities) {
            this.totalValue += pieEntity.getValue();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }
}

将点击的坐标位置转化为以饼状图中心为原点的坐标:

首先获取当前点击的x、y坐标:

其中这个点是距离当前视图左边缘的距离,用示例图来表示:

下面画图来阐述一下点击触摸事件的原理:

另外还需要解决一个问题:

那如果想计算点击的点处理饼状图的哪一个范围,以哪里做为原点比较好?肯定是以饼状图的圆心做为原点比较好,如图中标红的那个,而默认自定义控件是以左上角做为原点,这样计算角度啥的就很不方便,所以关键需要处理的一步是:

那如何将点击的坐标系由以左上角为原点转换为以饼图中心点为原点呢?

所以转换代码如下:

将圆上点击坐标转化为点击的角度:

接下来就要做这个处理了,如之前总结的步骤:

那如何能求得呢?这里就得回顾一下初中的数学知识啦,呃~~早就忘得一干二净了,这里趁着这个机会脑补一下,可能现在听到数学计算就头发麻,但对于程序员而言有时候确实也逃不开这些,用到了再去现场学习就成,这个公式也有些绕,下面开始:

首先计算直线的斜率:

也就是触摸点与饼图中心点两点连线的斜率:

而斜率的计算公式是:

所以其斜率=(y-0) / (x - 0) = y / x;

然后计算弧度值:

有了斜率之后,就可以通过数学公式中的arctan反正切函数来计算角度了,而Math类中已经提供了atan()反正切函数了,但是它返回的弧度值,如下:

其中参数就可以传我们计算的斜率y / x,所以其弧度为:Math.atan(y / x);

最后弧度转换为角度:

由于我们最终是要角度值,而上一步中的atan得到的是弧度值,所以so easy啦,弧度转角度公式如下:

弧度变角度 = 弧度  * 180/π 

而Math类已经为我们准备好啦,直接可以用它的toDegrees()方法:

所以其角度= Math.toDegrees(Math.atan(y / x)),所以将其加入到咱们的触摸事件当中,看一下其角度是不是准确滴啦:

运行看一下获得的角度是否如我们所愿:

如上图所示其结果是:

1象限的d点返回的角度为-39.708208788491326

2象限的c点返回的角度为15.013464636859112

3象限的b点返回的角度为-35.626313289782644

4象限的a点返回的角度为37.05221271200418

呃,貌似跟我们的预期相差太大,因为atan的值域是从-90~90 也就是它只处理一四象限,所以可以看到其结果并未有超过90度的值,但是!!可以找到一定的规律来解决目前结果不准的问题,在分析之前需要有个细节注意,其超始角度是从0度开始,并向顺时针方向进行增大的,如下:

下面开始找规律来解决目前不准的问题:

  • 发现4象限目前的角度结果是正常的:4象限的a点返回的角度为37.05221271200418,而这象限的点的特点是:x>0、y>0。
  • 其它1、2、3象限的角度结果是不正常的,但是可以通过这个不正常的值加上一些处理就可以得到正确的结果,如下:
    ①、3象限的b点返回的角度为-35.626313289782644这个象限的特点是:x>0、y<0,其实用该值+360度就如我们所愿了:-35.626313289782644 + 360 = 324.373686710217356,也就是360是一个整圆,然后去减掉这返回的值就刚好得到我们想要的啦,所以这个值对我们是有意义的。
    ②、2象限的c点返回的角度为15.013464636859112这个象限的特点是:x<0、y<0,其实用该值+180度就如我们所愿了:15.013464636859112 + 180 = 195.013464636859112,也就是用180度+返回的值既为真正的角度。
    ③、1象限的d点返回的角度为-39.708208788491326这个象限的特点是:x<0、y>0,其实用该值+180度就如我们所愿了:-35.626313289782644 + 180 = 144.373686710217356,也就是用180度+返回的值既为真正的角度。

其中标红的文字就组成了针对不同的象限再进一步对当前返回的值做适当的处理就可以了,所以修改代码如下:

这时编译运行再看下角度有木有修正:

嗯~~角度完美获取了,但是对于这个要据当前点击的点来获取角度可以封装成一个工具方法便于未来有同样需求的时候直接拿来用,所以最后一步对它进行一下简单封装。

将根据当前在饼区点击的点获取角度的代码封装成工作方法:

计算点击角度是在哪个饼状的区域内:

首先要判断当前点击的位置是在饼状圆之内,不然点击圆外则是一个无效的点击,而判断方法就是根据圆的半径来判断,所以这里又涉及到如何计算当前点到圆心的长度,又得回顾一下数学公式:

既:

所以代码如下:

那要判断当前点击的区域是在饼状图的哪个区域,前提得要知道各个饼状区域的起始角度才行,如下:

所以这里定义一个集合来收集这些数据:

/**
 * 带百分比的饼图效果,实现思路:
 * 1、最内测的扇形组成的的圆形区域
 * 2、中间的短线段的绘制
 * 3、最外侧的文本的绘制
 * 4、加上点击事件
 */
public class PieView extends View {

    /* 数据源,由外部传过来 */
    private List<PieEntity> pieEntities;
    /* 控件的大小 */
    private int height, width;
    /* 圆的半径 */
    private int radius;
    /* 扇形组成圆形的外接短形 */
    private RectF rectF;
    /* 绘制扇形的画笔 */
    private Paint paint;
    /* 总占比 */
    private int totalValue;
    private Path path;
    /* 由于线条的颜色需要一致所以重新新建一个Paint专用于绘制直线 */
    private Paint linePaint;
    /* 收集所有饼状图的起始角度,以便到时点击用来判断到底点的是哪个扇形 */
    private float[] startAngles;

    public PieView(Context context) {
        this(context, null);
    }

    public PieView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        rectF = new RectF();

        paint = new Paint();
        paint.setAntiAlias(true);

        path = new Path();

        linePaint = new Paint();
        linePaint.setAntiAlias(true);
        linePaint.setColor(Color.BLACK);
        linePaint.setTextSize(20);
    }

    //当自定义控件的尺寸已经决定好的时候回调
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //初始化控制的宽高
        this.width = w;
        this.height = h;

        //为了防止绘制后超出屏幕区域,获取屏幕的宽高的较小值
        int min = Math.min(this.width, this.height);
        this.radius = (int) ((min * 0.7f) / 2);//圆的半径取屏幕的7成
        rectF.left = -radius;
        rectF.top = -radius;
        rectF.right = radius;
        rectF.bottom = radius;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();//由于用到了translate画片所以需要save一下
        //1、将画布的坐标系移到屏幕中间,这样方便去绘制圆
        canvas.translate(this.width / 2, this.height / 2);
        //2、绘制扇形
        drawPie(canvas);
        canvas.restore();
    }


    private void drawPie(Canvas canvas) {
        float startAngle = 0;
        for (int i = 0; i < pieEntities.size(); i++) {
            PieEntity pieEntity = pieEntities.get(i);
            paint.setColor(pieEntity.getColor());//设置扇形的颜色
            path.moveTo(0, 0);//需要将其移动到坐标系位置
            //其中减1是为了让各扇形区域之间有一个间隙
            float sweepAngle = (pieEntity.getValue() / totalValue) * 360 - 1;
            path.arcTo(rectF, startAngle, sweepAngle);
            canvas.drawPath(path, paint);

            //绘制每个扇形对应的直线
            double a = Math.toRadians(startAngle + sweepAngle / 2);//将角度转化为弧度
            float startX = (float) (radius * Math.cos(a));
            float startY = (float) (radius * Math.sin(a));
            float endX = (float) ((radius + 30) * Math.cos(a));
            float endY = (float) ((radius + 30) * Math.sin(a));
            canvas.drawLine(startX, startY, endX, endY, linePaint);
            startAngles[i] = startAngle;

            //每一个扇形区域的起始点就是上一个扇形区域的终点
            startAngle += sweepAngle + 1;
            //在每次绘制扇形之后需要对path进行重置操作,这样就可以清除上一次绘制path使用的画笔的相关记录
            path.reset();

            //绘制文本
            //1、文本内容
            String percent = String.format("%.1f", pieEntity.getValue() / totalValue * 100);
            percent += "%";
            Log.d("cexo", "percent:" + percent + ";startAngle:" + startAngle);
            //2、文本的位置
            if ((startAngle - sweepAngle / 2) % 360.0f >= 90.0f && (startAngle - sweepAngle / 2) % 360.0f <= 270.0f) {
                //如果是90度~270度的文字,则将其x左移一个文本宽的位置来避免显示上的问题
                //计算文本的宽度
                float textWidth = linePaint.measureText(percent);
                canvas.drawText(percent, endX - textWidth, endY, linePaint);//左移文本让其显示在直线的左边
            } else {
                canvas.drawText(percent, endX, endY, linePaint);
            }
        }
    }

    /**
     * 设置饼图的源数据
     */
    public void setData(List<PieEntity> pieEntities) {
        this.pieEntities = pieEntities;
        for (PieEntity pieEntity : pieEntities) {
            this.totalValue += pieEntity.getValue();
        }
        startAngles = new float[pieEntities.size()];
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //判断是扇形的哪个区域被点击了,注意:这个x、y是获取用户点击的位置距当前视图的左边缘的距离
                float x = event.getX();
                float y = event.getY();
                //将点击的x和y坐示转换以饼状图为圆心的坐标
                x = x - width / 2;
                y = y - height / 2;
                Log.d("cexo", "caculate x:" + x + ";y:" + y);
                float touchAngle = MathUtil.getTouchAngle(x, y);//获得触摸的角度
                Log.d("cexo", "touchAngle :" + touchAngle);
                float touchRadius = (float) Math.sqrt(x * x + y * y);//获得触摸点的半径
                //判断触摸的点距离饼状图圆心的距离(饼状图对应圆的圆心)
                if (touchRadius < radius) {
                    //说明是一个有效点击区域,在饼状图内
                }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }
}

接下来就是根据当前点击的角度从这些集合中查询,并获得所点击饼图的位置,那如何搞呢?这里可以采用jdk自带的二分查找方法就能满足我们的要求,下面来写个单元测试来使用下它,从使用中来得到我们的判断代码:

也就是只有返回值为负时,需要我们处理下,具体处理代码如下:

public class PieView extends View {

    /* 数据源,由外部传过来 */
    private List<PieEntity> pieEntities;
    /* 控件的大小 */
    private int height, width;
    /* 圆的半径 */
    private int radius;
    /* 扇形组成圆形的外接短形 */
    private RectF rectF;
    /* 绘制扇形的画笔 */
    private Paint paint;
    /* 总占比 */
    private int totalValue;
    private Path path;
    /* 由于线条的颜色需要一致所以重新新建一个Paint专用于绘制直线 */
    private Paint linePaint;
    /* 收集所有饼状图的起始角度,以便到时点击用来判断到底点的是哪个扇形 */
    private float[] startAngles;
    /* 点击饼图所在扇形的位置 */
    private int position;

    public PieView(Context context) {
        this(context, null);
    }

    public PieView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        rectF = new RectF();

        paint = new Paint();
        paint.setAntiAlias(true);

        path = new Path();

        linePaint = new Paint();
        linePaint.setAntiAlias(true);
        linePaint.setColor(Color.BLACK);
        linePaint.setTextSize(20);
    }

    //当自定义控件的尺寸已经决定好的时候回调
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //初始化控制的宽高
        this.width = w;
        this.height = h;

        //为了防止绘制后超出屏幕区域,获取屏幕的宽高的较小值
        int min = Math.min(this.width, this.height);
        this.radius = (int) ((min * 0.7f) / 2);//圆的半径取屏幕的7成
        rectF.left = -radius;
        rectF.top = -radius;
        rectF.right = radius;
        rectF.bottom = radius;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();//由于用到了translate画片所以需要save一下
        //1、将画布的坐标系移到屏幕中间,这样方便去绘制圆
        canvas.translate(this.width / 2, this.height / 2);
        //2、绘制扇形
        drawPie(canvas);
        canvas.restore();
    }


    private void drawPie(Canvas canvas) {
        float startAngle = 0;
        for (int i = 0; i < pieEntities.size(); i++) {
            PieEntity pieEntity = pieEntities.get(i);
            paint.setColor(pieEntity.getColor());//设置扇形的颜色
            path.moveTo(0, 0);//需要将其移动到坐标系位置
            //其中减1是为了让各扇形区域之间有一个间隙
            float sweepAngle = (pieEntity.getValue() / totalValue) * 360 - 1;
            path.arcTo(rectF, startAngle, sweepAngle);
            canvas.drawPath(path, paint);

            //绘制每个扇形对应的直线
            double a = Math.toRadians(startAngle + sweepAngle / 2);//将角度转化为弧度
            float startX = (float) (radius * Math.cos(a));
            float startY = (float) (radius * Math.sin(a));
            float endX = (float) ((radius + 30) * Math.cos(a));
            float endY = (float) ((radius + 30) * Math.sin(a));
            canvas.drawLine(startX, startY, endX, endY, linePaint);
            startAngles[i] = startAngle;

            //每一个扇形区域的起始点就是上一个扇形区域的终点
            startAngle += sweepAngle + 1;
            //在每次绘制扇形之后需要对path进行重置操作,这样就可以清除上一次绘制path使用的画笔的相关记录
            path.reset();

            //绘制文本
            //1、文本内容
            String percent = String.format("%.1f", pieEntity.getValue() / totalValue * 100);
            percent += "%";
            Log.d("cexo", "percent:" + percent + ";startAngle:" + startAngle);
            //2、文本的位置
            if ((startAngle - sweepAngle / 2) % 360.0f >= 90.0f && (startAngle - sweepAngle / 2) % 360.0f <= 270.0f) {
                //如果是90度~270度的文字,则将其x左移一个文本宽的位置来避免显示上的问题
                //计算文本的宽度
                float textWidth = linePaint.measureText(percent);
                canvas.drawText(percent, endX - textWidth, endY, linePaint);//左移文本让其显示在直线的左边
            } else {
                canvas.drawText(percent, endX, endY, linePaint);
            }
        }
    }

    /**
     * 设置饼图的源数据
     */
    public void setData(List<PieEntity> pieEntities) {
        this.pieEntities = pieEntities;
        for (PieEntity pieEntity : pieEntities) {
            this.totalValue += pieEntity.getValue();
        }
        startAngles = new float[pieEntities.size()];
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //判断是扇形的哪个区域被点击了,注意:这个x、y是获取用户点击的位置距当前视图的左边缘的距离
                float x = event.getX();
                float y = event.getY();
                //将点击的x和y坐示转换以饼状图为圆心的坐标
                x = x - width / 2;
                y = y - height / 2;
                float touchAngle = MathUtil.getTouchAngle(x, y);//获得触摸的角度
                float touchRadius = (float) Math.sqrt(x * x + y * y);//获得触摸点的半径
                //判断触摸的点距离饼状图圆心的距离(饼状图对应圆的圆心)
                if (touchRadius < radius) {
                    //说明是一个有效点击区域,在饼状图内
                    //查找触摸的角度是否位于起始角度集合中
                    //binarySearch:参数二在参数1对应的集合中的索引,未找到,则返回-(和搜索的值附近的大于搜索值的正确值对应的索引值+1)
                    // 比如{1,2,3},搜索1:返回值1在集合中对应的索引0;搜索1.2:返回值为-(1【因为2的索引是1】+1)=-2;搜索1.8:返回值为-(1+1)=-2
                    int searchResult = Arrays.binarySearch(startAngles, touchAngle);
                    if (searchResult < 0) {
                        position = -searchResult - 1 - 1;//也就是将负数变成正数之后连续减两次1既可以得到我们想要的索引值
                    } else {
                        //证明是刚好点击了起始角度的位置,也就是扇形与扇形相交的位置
                        position = searchResult;
                    }
                    Log.d("cexo", "position:" + position);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }
}

这时再编译运行看是否能精确的知道所点击的区域位置:

完美!!接着就是处理最后一步突出效果啦~

绘制当前点击的突然扇形的效果:

终于到最后一步啦,不容易,先分析一下实现思路:

所以再定义一个稍大一点的外接矩形,如下:

public class PieView extends View {

    /* 数据源,由外部传过来 */
    private List<PieEntity> pieEntities;
    /* 控件的大小 */
    private int height, width;
    /* 圆的半径 */
    private int radius;
    /* 扇形组成圆形的外接短形 */
    private RectF rectF;
    /* 被触摸的扇形组成圆形的外接短形【放大效果】 */
    private RectF touchRectF;
    /* 绘制扇形的画笔 */
    private Paint paint;
    /* 总占比 */
    private int totalValue;
    private Path path;
    /* 由于线条的颜色需要一致所以重新新建一个Paint专用于绘制直线 */
    private Paint linePaint;
    /* 收集所有饼状图的起始角度,以便到时点击用来判断到底点的是哪个扇形 */
    private float[] startAngles;
    /* 点击饼图所在扇形的位置 */
    private int position;

    public PieView(Context context) {
        this(context, null);
    }

    public PieView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        rectF = new RectF();
        touchRectF = new RectF();

        paint = new Paint();
        paint.setAntiAlias(true);

        path = new Path();

        linePaint = new Paint();
        linePaint.setAntiAlias(true);
        linePaint.setColor(Color.BLACK);
        linePaint.setTextSize(20);
    }

    //当自定义控件的尺寸已经决定好的时候回调
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //初始化控制的宽高
        this.width = w;
        this.height = h;

        //为了防止绘制后超出屏幕区域,获取屏幕的宽高的较小值
        int min = Math.min(this.width, this.height);
        this.radius = (int) ((min * 0.7f) / 2);//圆的半径取屏幕的7成
        rectF.left = -radius;
        rectF.top = -radius;
        rectF.right = radius;
        rectF.bottom = radius;

        touchRectF.left = -radius - 15;//其中长度自己定义,这里加了15
        touchRectF.top = -radius - 15;
        touchRectF.right = radius + 15;
        touchRectF.bottom = radius + 15;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();//由于用到了translate画片所以需要save一下
        //1、将画布的坐标系移到屏幕中间,这样方便去绘制圆
        canvas.translate(this.width / 2, this.height / 2);
        //2、绘制扇形
        drawPie(canvas);
        canvas.restore();
    }


    private void drawPie(Canvas canvas) {
        float startAngle = 0;
        for (int i = 0; i < pieEntities.size(); i++) {
            PieEntity pieEntity = pieEntities.get(i);
            paint.setColor(pieEntity.getColor());//设置扇形的颜色
            path.moveTo(0, 0);//需要将其移动到坐标系位置
            //其中减1是为了让各扇形区域之间有一个间隙
            float sweepAngle = (pieEntity.getValue() / totalValue) * 360 - 1;
            path.arcTo(rectF, startAngle, sweepAngle);
            canvas.drawPath(path, paint);

            //绘制每个扇形对应的直线
            double a = Math.toRadians(startAngle + sweepAngle / 2);//将角度转化为弧度
            float startX = (float) (radius * Math.cos(a));
            float startY = (float) (radius * Math.sin(a));
            float endX = (float) ((radius + 30) * Math.cos(a));
            float endY = (float) ((radius + 30) * Math.sin(a));
            canvas.drawLine(startX, startY, endX, endY, linePaint);
            startAngles[i] = startAngle;

            //每一个扇形区域的起始点就是上一个扇形区域的终点
            startAngle += sweepAngle + 1;
            //在每次绘制扇形之后需要对path进行重置操作,这样就可以清除上一次绘制path使用的画笔的相关记录
            path.reset();

            //绘制文本
            //1、文本内容
            String percent = String.format("%.1f", pieEntity.getValue() / totalValue * 100);
            percent += "%";
            Log.d("cexo", "percent:" + percent + ";startAngle:" + startAngle);
            //2、文本的位置
            if ((startAngle - sweepAngle / 2) % 360.0f >= 90.0f && (startAngle - sweepAngle / 2) % 360.0f <= 270.0f) {
                //如果是90度~270度的文字,则将其x左移一个文本宽的位置来避免显示上的问题
                //计算文本的宽度
                float textWidth = linePaint.measureText(percent);
                canvas.drawText(percent, endX - textWidth, endY, linePaint);//左移文本让其显示在直线的左边
            } else {
                canvas.drawText(percent, endX, endY, linePaint);
            }
        }
    }

    /**
     * 设置饼图的源数据
     */
    public void setData(List<PieEntity> pieEntities) {
        this.pieEntities = pieEntities;
        for (PieEntity pieEntity : pieEntities) {
            this.totalValue += pieEntity.getValue();
        }
        startAngles = new float[pieEntities.size()];
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //判断是扇形的哪个区域被点击了,注意:这个x、y是获取用户点击的位置距当前视图的左边缘的距离
                float x = event.getX();
                float y = event.getY();
                //将点击的x和y坐示转换以饼状图为圆心的坐标
                x = x - width / 2;
                y = y - height / 2;
                float touchAngle = MathUtil.getTouchAngle(x, y);//获得触摸的角度
                float touchRadius = (float) Math.sqrt(x * x + y * y);//获得触摸点的半径
                //判断触摸的点距离饼状图圆心的距离(饼状图对应圆的圆心)
                if (touchRadius < radius) {
                    //说明是一个有效点击区域,在饼状图内
                    //查找触摸的角度是否位于起始角度集合中
                    //binarySearch:参数二在参数1对应的集合中的索引,未找到,则返回-(和搜索的值附近的大于搜索值的正确值对应的索引值+1)
                    // 比如{1,2,3},搜索1:返回值1在集合中对应的索引0;搜索1.2:返回值为-(1【因为2的索引是1】+1)=-2;搜索1.8:返回值为-(1+1)=-2
                    int searchResult = Arrays.binarySearch(startAngles, touchAngle);
                    if (searchResult < 0) {
                        position = -searchResult - 1 - 1;
                    } else {
                        //证明是刚好点击了起始角度的位置,也就是扇形与扇形相交的位置
                        position = searchResult;
                    }
                    Log.d("cexo", "position:" + position);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }
}

接下来就在onDraw()方法中根据用户点中的position的位置进行处理,如下:

处理完毕,但是目前运行依然没有突出效果,为什么呢?

所以加上它,这样就达到开篇的效果啦。

说实话,这个效果虽说简单,但是实现的细节还挺多的,其中还涉及到一些数学公式的计算,需好好消化!!

原文地址:https://www.cnblogs.com/webor2006/p/7687320.html