Android自定义Sunburst Chart(太阳图)

功能

一种显示树形结构的图表,内圆是外圆的父节点,一层层显示绘制

效果图

使用示例

package com.eroad.product.tools;

import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class SunBurstChart extends View {
    private float density;                                    //屏幕密度
    private float viewWidth;                                //控件宽度
    private float viewHeight;                                //控件高度
    private float pieCenterX, pieCenterY, pieRadius;        //圆心X轴坐标,圆心Y轴坐标,圆半径
    private Paint textPaint, piePaint, linePaint, criclePaint, ringPaint, inTextPaint;//文本画笔,扇形画笔,线画笔,内圆画笔,圆环画笔
    private RectF pieOval;                                    //矩形边界框
    private String TAG = "SunBurstChart";
    private float textSize = 14;        //字体大小
    private float cricle;                //内圆半径
    //扇形的弧度
    private ArrayList<Float> arraydegrees;
    private List<Point> inPoints;//内圆用以记录每个矩形的度数

    //内圆的饼图
    private RectF pieInOval;                                    //内圆矩形边界框
    private float inTotalValue;                                //总数
    //内圆扇形的弧度
    private ArrayList<Float> inArraydegrees;
    private List<Nodes> mDatas;
    private boolean animate = false;
    private int depth = 1;
    private Path textPath;
    private float ringWidth = 3f;//圆环宽度
    private float inTextSize = 12;        //内圆字体大小

    int[] colors = {Color.parseColor("#0288D1"), Color.parseColor("#F8BBD0"), Color.parseColor("#D4E157"), Color.parseColor("#FF6E40")}; //颜色值

    public SunBurstChart(Context context) {
        super(context);
        // TODO Auto-generated constructor stub

    }

    public SunBurstChart(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO Auto-generated constructor stub
        //获取屏幕密度
        Display display = ((Activity) context).getWindowManager().getDefaultDisplay();
        DisplayMetrics displayMetrics = new DisplayMetrics();
        display.getMetrics(displayMetrics);
        density = displayMetrics.density;
    }

    public SunBurstChart(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // TODO Auto-generated constructor stub
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);
        //获取控件的高度和宽度
        viewWidth = getWidth();
        viewHeight = getHeight();
        this.init();
        drawNormal(canvas);
    }

    private float animatedValue;//圆环动画

    private void initAnimator() {
        ValueAnimator anim = ValueAnimator.ofFloat(0, 360);
        anim.setDuration(5000);
        anim.setInterpolator(new AccelerateDecelerateInterpolator());
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                animatedValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        anim.start();
    }

    public void startDraw() {
        if (mDatas.size() > 0 && animate) {
            initAnimator();
        } else {
            postInvalidate();
        }
    }

    /**
     * 没有任何动画的圆
     *
     * @param canvas
     */
    public void drawNormal(Canvas canvas) {

        float start = 0.0f;
        if (mDatas == null) {
            return;
        }
        drawSunBurstChart(canvas);
    }

    Random random = new Random();

    private void drawSunBurstChart(Canvas canvas) {
        float start = 0.0f;
        int level = 2;
        int lastIndex = 0;
        for (int i = 0; i < mDatas.size(); i++) {
            Nodes parentNode = mDatas.get(i);
            double value = parentNode.getValue();
            String title = parentNode.getName();
            int index = random.nextInt(colors.length - 1);
            if (index == lastIndex) {
                index = random.nextInt(colors.length - 1);
            }
            int color = colors[index];

            float sweep = (float) (value / inTotalValue * 360);//计算度数
            inArraydegrees.add(sweep);


            drawChild(start, canvas, sweep, parentNode.getChildNodes(), level);
            drawInCircleAndLine(i, canvas, start, sweep, value, title, color);
            ringPaint.setStrokeWidth(ringWidth); //设置圆环的宽度
            canvas.drawCircle(pieCenterX, pieCenterY, cricle, ringPaint); //画出圆环

            inPoints.get(i).x = (int) start;
            inPoints.get(i).y = (int) (start) + (int) sweep;
            start += sweep;
            lastIndex = index;
        }
    }

    private void drawChild(float start, Canvas canvas, double angle, List<Nodes> childNodes, int level) {
        double childTotalValue = 0;
        for (int i = 0; i < childNodes.size(); i++) {
            Nodes child = childNodes.get(i);
            double value = child.getValue();
            childTotalValue += value;
        }
        for (int i = 0; i < childNodes.size(); i++) {
            Nodes child = childNodes.get(i);
            double value = child.getValue();
            String title = child.getName();
            float sweep = (float) (value / childTotalValue * angle);//计算度数
            if (child.hasChild()) {
                float tempstart = start;
                drawChild(tempstart, canvas, sweep, child.getChildNodes(), level + 1);
            }

            int color = colors[random.nextInt(colors.length - 1)];
            arraydegrees.add(sweep);
            piePaint.setColor(color);//设置颜色
            pieOval = new RectF(pieCenterX - cricle * level, pieCenterY - cricle * level, pieCenterX + cricle * level, pieCenterY + cricle * level);
            if (Math.min(sweep, animatedValue - start) >= 0 && animate) {
                canvas.drawArc(pieOval, start, Math.min(sweep - 1, animatedValue - start), true, piePaint);//画扇形
            } else {
                canvas.drawArc(pieOval, start, sweep, true, piePaint);//画扇形
                canvas.drawArc(pieOval, start, sweep, true,
                        ringPaint);
            }

            float radius = cricle * level - cricle / 2f;
            int middleAngle = (int) (start + sweep / 2);
            float textPointX = (float) (pieOval.centerX() + radius * Math.cos(Math.toRadians(middleAngle)));
            float textPointY = (float) (pieOval.centerY() + radius * Math.sin(Math.toRadians(middleAngle)));
            float textPointEndX = (float) (pieInOval.centerX() + cricle * (level - 1) * Math.cos(Math.toRadians(middleAngle)));
            float textPointEndY = (float) (pieInOval.centerY() + cricle * (level - 1) * Math.sin(Math.toRadians(middleAngle)));
            inTextPaint.setTextAlign(Paint.Align.LEFT);
            textPath.reset();
            textPath.moveTo(textPointX, textPointY);
            textPath.lineTo(textPointEndX, textPointEndY);
            canvas.drawTextOnPath(title, textPath, 0, 0, inTextPaint);
            start += sweep;
        }
    }

    /**
     * 内圆正常画圆和画线
     *
     * @param i
     * @param canvas
     * @param start
     * @param sweep
     */
    public void drawInCircleAndLine(int i, Canvas canvas, float start, float sweep, double value, String title, int color) {
        criclePaint.setColor(color);//设置颜色
        if (animate) {
            canvas.drawArc(pieInOval, start, Math.min(sweep, animatedValue - start), true, criclePaint);//画扇形
        } else {
            canvas.drawArc(pieInOval, start, sweep, true, criclePaint);//画扇形
            canvas.drawArc(pieInOval, start, sweep, true, ringPaint);//画边框

        }

        // 绘制圆环上扇形的文字
        int middleAngle = (int) (start + sweep / 2);

        float radius = (cricle) / 3f;
        float textPointX = (float) (pieInOval.centerX() + radius * Math.cos(Math.toRadians(middleAngle)));
        float textPointY = (float) (pieInOval.centerY() + radius * Math.sin(Math.toRadians(middleAngle)));

        float textPointEndX = (float) (pieInOval.centerX() + cricle * Math.cos(Math.toRadians(middleAngle)));
        float textPointEndY = (float) (pieInOval.centerY() + cricle * Math.sin(Math.toRadians(middleAngle)));
        inTextPaint.setTextAlign(Paint.Align.LEFT);
        textPath.reset();
        textPath.moveTo(textPointX, textPointY);
        textPath.lineTo(textPointEndX, textPointEndY);
        canvas.drawPath(textPath, linePaint);
        canvas.drawTextOnPath(title, textPath, 0, 0, inTextPaint);
    }

    //初始化画笔和画刷
    public void init() {
        float min = Math.min(viewWidth, viewHeight);//获取屏幕中高宽中的最小值,用以确认圆心位置
        pieCenterX = viewWidth / 2;
        pieCenterY = viewHeight / 2;
        pieRadius = 2 * min / 5;//获得饼图的半径
        cricle = pieRadius / depth;//内圆的半径
        textSize = 12 * density;//获取字体的大小
        //得到饼图的矩形
        pieOval = new RectF(pieCenterX - cricle, pieCenterY - cricle, pieCenterX + cricle, pieCenterY + cricle);
        //初始化文字的画笔
        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setTextSize(textSize);
        textPaint.setColor(Color.parseColor("#939393"));// 颜色
        //初始化饼图的画笔
        piePaint = new Paint();
        piePaint.setAntiAlias(true);
        piePaint.setStyle(Paint.Style.FILL);
        //初始化线的画笔
        linePaint = new Paint();
        linePaint.setAntiAlias(true);
        linePaint.setStyle(Paint.Style.FILL);
        linePaint.setStrokeWidth(1);
        linePaint.setColor(Color.parseColor("#939393"));// 颜色
        //初始化内圆的画笔
        criclePaint = new Paint();
        criclePaint.setAntiAlias(true);
        criclePaint.setStyle(Paint.Style.FILL);
        criclePaint.setColor(Color.parseColor("#ffffff"));
        pieInOval = new RectF(pieCenterX - cricle, pieCenterY - cricle, pieCenterX + cricle, pieCenterY + cricle);

        ringPaint = new Paint();//圆环画笔

        ringPaint.setColor(Color.parseColor("#ffffff")); //设置圆环的颜色
        ringPaint.setStyle(Paint.Style.STROKE); //设置空心
        ringPaint.setAntiAlias(true);  //消除锯齿

        inTextPaint = new Paint();//初始化内圆的字体画笔
        inTextPaint.setAntiAlias(true);
        inTextPaint.setStyle(Paint.Style.FILL);
        inTextPaint.setTextSize(inTextSize);
        inTextPaint.setColor(Color.parseColor("#ffffff"));// 颜色
        arraydegrees = new ArrayList<Float>();
        inArraydegrees = new ArrayList<Float>();
        //饼图放大部分的矩形
        textPath = new Path();


    }

    /**
     * 设置数据源
     *
     * @param nodes
     */
    public void setDatas(List<Nodes> nodes) {
        mDatas = nodes;
        inPoints = new ArrayList<>();
        inPoints = new ArrayList<>();
        for (Nodes node : nodes) {
            inTotalValue += node.getValue();
            Point point = new Point();
            inPoints.add(point);
            countNodeDepth(node.getChildNodes());
        }
        Log.i(TAG, "setDatas: " + depth);
        postInvalidate();//刷新
    }

    int depth1 = 2;

    public int countNodeDepth(List<Nodes> nodes) {
        for (Nodes node : nodes) {
            if (node.hasChild()) {
                depth1++;
                countNodeDepth(node.getChildNodes());
            } else {
                depth = Math.max(depth1, depth);
                depth1 = 2;
                continue;
            }
        }

        return depth1;
    }
}

数据结构

package com.eroad.product.tools;

import java.util.List;

/**
 * 节点实体类
 * @author Administrator
 *
 */
public class Nodes {

    private String id;

    //父节点
    private String pid;

    private String name;
    private double value;

    List<Nodes> childNodes;
    private boolean hasChild;

    public Nodes(String id, String pid, String name, double value) {
        this.id = id;
        this.pid = pid;
        this.name = name;
        this.value = value;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPid() {
        return pid;
    }

    public void setPid(String pid) {
        this.pid = pid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Nodes> getChildNodes() {
        return childNodes;
    }

    public void setChildNodes(List<Nodes> childNodes) {
        this.childNodes = childNodes;
    }

    public double getValue() {
        return value;
    }

    public void setValue(double value) {
        this.value = value;
    }

    public boolean hasChild() {
        return !(childNodes == null || childNodes.size() == 0);
    }
}

测试代码

package com.example.linecharttest.tab;

import com.eroad.product.tools.Nodes;
import com.eroad.product.tools.SunBurstChart;
import com.example.linecharttest.R;
import android.app.Activity;
import android.os.Bundle;

import java.util.ArrayList;
import java.util.List;

/**
 * 可点击的饼状图
 * @author wanjuanjuan
 *
 */
public class TestActivity extends Activity{

	private SunBurstChart ringview;
	private String TAG = "TestActivity"; //TAG

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		// TODO Auto-generated method stub
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_ring);
		ringview = (SunBurstChart) findViewById(R.id.ring);

		List<Nodes> parentNodes = new ArrayList<Nodes>();
		parentNodes.add(new Nodes("1","1","udp",2112));
		parentNodes.add(new Nodes("2","2","tcp",2000));
		parentNodes.add(new Nodes("3","3","其他",2000));
		parentNodes.add(new Nodes("4","4","更多",1000));

		for (int i= 0;i<parentNodes.size();i++){
			Nodes parentNode = parentNodes.get(i);
			List<Nodes> childNodes = new ArrayList<Nodes>();
			if(parentNode.getId().equals("1")){
				childNodes.add(new Nodes("5","1","餐饮",250));
				childNodes.add(new Nodes("6","1","娱乐",100));
				childNodes.add(new Nodes("7","1","学习",120));
			}
			if(parentNode.getId().equals("2")){
				childNodes.add(new Nodes("9","2","餐饮",200));
				childNodes.add(new Nodes("10","2","娱乐",100));
			}

			if(parentNode.getId().equals("3")){
				childNodes.add(new Nodes("11","3","餐饮",300));
				childNodes.add(new Nodes("12","3","学习",120));
			}

			if(parentNode.getId().equals("4")){
				childNodes.add(new Nodes("13","4","餐饮",100));
				childNodes.add(new Nodes("14","4","娱乐",100));
				childNodes.add(new Nodes("15","4","人际关系",160));
			}
			parentNode.setChildNodes(childNodes);
		}

		for (int i= 0;i<parentNodes.size();i++){
			Nodes parentNode = parentNodes.get(i);
			List<Nodes> childNodes = parentNode.getChildNodes();
			for (int j= 0;j<childNodes.size();j++){
				Nodes childNode = childNodes.get(j);
				List<Nodes> childchildNodes = new ArrayList<Nodes>();
				String id = j*10+"";
				childchildNodes.add(new Nodes(id, childNode.getId(), "餐饮", 200));
				childchildNodes.add(new Nodes(id, childNode.getId(), "娱乐", 100));
				childNode.setChildNodes(childchildNodes);
			}
		}

		for (int i= 0;i<parentNodes.size();i++){
			Nodes parentNode = parentNodes.get(i);
			List<Nodes> childNodes = parentNode.getChildNodes();
			for (int j= 0;j<childNodes.size();j++){
				List<Nodes> childchildNodes = childNodes.get(j).getChildNodes();
				for (int k= 0;k<childchildNodes.size();k++){
					Nodes childchildNode = childchildNodes.get(k);
					List<Nodes> childchildchildNodes = new ArrayList<Nodes>();
					String id = k*10+"";
					childchildchildNodes.add(new Nodes(id, childchildNode.getId(), "餐饮", 20));
					childchildchildNodes.add(new Nodes(id, childchildNode.getId(), "娱乐", 10));
					childchildchildNodes.add(new Nodes(id, childchildNode.getId(), "学习", 12));
					childchildNode.setChildNodes(childchildchildNodes);
				}

			}
		}
		ringview.setDatas(parentNodes);
		ringview.startDraw();
	}

}

正在开发的功能

动画,手势放大,显示优化(现在还有点丑)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值