Android自定义View 雷达图(蛛网)动态实现
自定义View实现雷达图还是挺简单的,它能让使用让使用者能一目了然的了解各项指标的变动情形以及好坏趋势。使用得最多的便是Path路径,很适合初学者用来练习。
效果图如下:
下面是实体类的属性:
public class RadarMapData {
private int count;//数值项数
private String [] titles;//标题
private Double[] setData;//数值
private Float maxValue;//最大数值
private int mainPaintColor;//蜘蛛网颜色
private int textPaintColor;//标题颜色
private int valuePaintColor;//覆盖局域颜色
private int textSize;//字体大小
步骤:
1、画出背景上的几个正六边形
从下图可得出,若将c点看作原点,a点的x轴坐标为ac*cos(angle),y轴坐标为ac*sin(angle),以此类推计算出正多边形其它顶点坐标,将其连接即可绘制出正多边形。
代码如下:
public class RadarMapView extends View {
private RadarMapData radarMapData;
private float angle;//每项数值之间夹角
private int count=6;//数值项个数
private int contextX,contextY;//中心点坐标
private float radius=100;//网格最大半径
private Paint mainPaint; //雷达区画笔
private Paint valuePaint; //数据区画笔
private Paint textPaint; //文本画笔
public RadarMapView(Context context) {
this(context,null);
}
public RadarMapView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);;
mainPaint=new Paint();
textPaint = new Paint();
valuePaint = new Paint();
mainPaint.setColor(Color.BLUE);
mainPaint.setStyle(Paint.Style.STROKE);
mainPaint.setAntiAlias(true);
textPaint.setColor(Color.BLUE);
textPaint.setStyle(Paint.Style.STROKE);
textPaint.setAntiAlias(true);
valuePaint.setColor(Color.BLUE);
valuePaint.setStyle(Paint.Style.STROKE);
valuePaint.setAntiAlias(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
contextX=w/2;
contextY=h/2;
radius=Math.min(w,h)/2*0.8f;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawPolygon(canvas);//绘制正多边形
}
private void drawPolygon(Canvas canvas) {
Path path=new Path();
float r=radius/(count-1);
for(int i=1;i<count;i++){//绘制的层数
float curR=r*i;//当前层数半径
path.reset();//使path重置
for (int j=0;j<count;j++){
if(j==0){//第一次绘制,将起点移到第一个将要绘制的点,即多边形中心点移动到起点
path.moveTo(contextX+curR,contextY);
}else{
//使用三角函数计算x坐标和有坐标
float x= (float) (contextX+curR*Math.cos(angle*j));
float y= (float) (contextY+curR*Math.sin(angle*j));
path.lineTo(x,y);}
}
path.close();
canvas.drawPath(path,mainPaint);
}
}
public void setData(RadarMapData radarMapData) {
this.radarMapData = radarMapData;
count = radarMapData.getCount();
angle = (float) (Math.PI * 2 / count);
mainPaint.setColor(radarMapData.getMainPaintColor());
}
}
要注意的是,这里的contextX和contextY并非画布原点,而是正多边形中心所在的坐标点,画布原点仍在左上角。
Activaty代码:
private RadarMapView radarMapView;
private RadarMapData radarMapData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
radarMapView=findViewById(R.id.radar_map);
radarMapData=new RadarMapData();
radarMapData.setCount(6);
radarMapData.setMainPaintColor(Color.BLUE);
radarMapView.setData(radarMapData);
}
效果图如下:
2、绘制正多边形到各顶点的连线
效果图如下:和上面的差不多,这里只需要计算出最外层正多边形顶点坐标,再将其与正多边形中心点连线就可以了。代码如下:
private void drawLines(Canvas canvas) {
Path path=new Path();
for (int i=0;i<count;i++){
path.reset();
path.moveTo(contextX,contextY);//将起点移到多边形中心点
float x= (float) (contextX+radius*Math.cos(angle*i));
float y = (float) (contextY + radius * Math.sin(angle * i));
path.lineTo(x,y);
canvas.drawPath(path,mainPaint);
}
}
3、绘制各项数值的标题
这里需要注意,绘制文本时要考虑文本的宽高的影响,如果直接以正多边形顶点外就近处绘制文本时文字过长可能会导致第二、三、四象限的文本与雷达图重合。效果图如下:
代码:
private void drawTitle(Canvas canvas) {
Paint.FontMetrics fontMetrics=textPaint.getFontMetrics();
float fontHeight=fontMetrics.descent-fontMetrics.ascent;
for(int i=0;i<count;i++){
float x = (float) (contextX + (radius + fontHeight / 2) * Math.cos(angle * i));
float y = (float) (contextY + (radius + fontHeight / 2) * Math.sin(angle * i));
if(angle*i>=0&&angle*i<=Math.PI/2){
//第四象限
canvas.drawText(titles[i],x,y+fontHeight/3,textPaint);
}else if(angle*i>Math.PI/2&&angle*i<=Math.PI){
//第三象限
float drs=textPaint.measureText(titles[i]);
canvas.drawText(titles[i],x-drs,y+fontHeight/3,textPaint);
}else if(angle*i>Math.PI&&angle*i<3*Math.PI/2){
//第二象限
float drs=textPaint.measureText(titles[i]);
canvas.drawText(titles[i],x-drs,y,textPaint);
}else if(angle*i>3*Math.PI/2&&angle*i<=2*Math.PI){
//第一象限
canvas.drawText(titles[i],x,y,textPaint);
}
}
}
public void setData(RadarMapData radarMapData) {
this.radarMapData = radarMapData;
count = radarMapData.getCount();
angle = (float) (Math.PI * 2 / count);
mainPaint.setColor(radarMapData.getMainPaintColor());
titles=radarMapData.getTitles();
textPaint.setTextSize(radarMapData.getTextSize());
}
有些人可能会对float fontHeight=fontMetrics.descent-fontMetrics.ascent;为什么能获取文字高度有些疑惑,请看下图1. 基准点是baseline
2. Ascent是baseline之上至字符最高处的距离
3. Descent是baseline之下至字符最低处的距离
4. Leading文档说的很含糊,其实是上一行字符的descent到下一行的ascent之间的距离
5. Top指的是指的是最高字符到baseline的值,即ascent的最大值
6. 同上,bottom指的是最下字符到baseline的值,即descent的最大值
而在baseline上方的坐标为负,baseline下方的坐标为正,所以文字的高度为descent-Ascent。4、绘制覆盖区域
覆盖区域的绘制也很简单,只需通过各项数值和最大数值计算出所占比例,以此算出其在网格最大半径上的距离,再用三角函数计算坐标点绘制小圆并连线填充。
代码如下:
private void drawCover(Canvas canvas) {
Path path=new Path();
for (int i = 0; i < count; i++){
float percent = (float) (values[i] / maxValue);//当前数值占最大数值百分比
float x = (float) (contextX + radius * percent * Math.cos(angle * i));
float y = (float) (contextY + radius * percent * Math.sin(angle * i));
if (i == 0) {
path.moveTo(contextX + radius * percent, contextY);//将起点移到第一个将要绘制的点
} else {
path.lineTo(x, y);
}
valuePaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(x, y, 8, valuePaint);//绘制小圆点
}
valuePaint.setAlpha(130);//设置画笔透明度
valuePaint.setStyle(Paint.Style.FILL_AND_STROKE);//填充并且描边
canvas.drawPath(path, valuePaint);
}
public void setData(RadarMapData radarMapData) {
this.radarMapData = radarMapData;
count = radarMapData.getCount();
angle = (float) (Math.PI * 2 / count);
mainPaint.setColor(radarMapData.getMainPaintColor());
titles=radarMapData.getTitles();
textPaint.setTextSize(radarMapData.getTextSize());
values=radarMapData.getValuse();
}
public class MainActivity extends AppCompatActivity {
private RadarMapView radarMapView;
private RadarMapData radarMapData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
radarMapView=findViewById(R.id.radar_map);
radarMapData=new RadarMapData();
radarMapData.setCount(6);
radarMapData.setMainPaintColor(Color.BLUE);
radarMapData.setTitles(new String[]{"进攻","团战", "资源" , "防守","辅助" , "生存"});
radarMapData.setValuse(new Double[]{25.0,67.8,55.4,89.0,36.9,70.0});
radarMapData.setTextSize(30);
radarMapView.setData(radarMapData);
}
}
5、覆盖区域动放大出现
这个只需让各项数值从0开始逐步增加并绘制view就好了
public class RadarMapView extends View {
private RadarMapData radarMapData;
private float angle;//每项数值之间夹角
private int count=6;//数值项个数
private int contextX,contextY;//中心点坐标
private float radius=100;//网格最大半径
private Paint mainPaint; //雷达区画笔
private Paint valuePaint; //数据区画笔
private Paint textPaint; //文本画笔
private String[] titles={"a","b","c","d","e","f"};
private Double[] values={25.0,67.8,55.4,89.0,56.9,70.0};
private Double[] temValuses={0.0,0.0,0.0,0.0,0.0,0.0};
private float maxValue=100;//数据最大值
private float mDuration=1000;//动画时长
private float mCount=100;//分多少次运动
private float mCurrent = 100; // 当前已进行时长
private float mPiece = mDuration/mCount; // 每一份的时长
public RadarMapView(Context context) {
this(context,null);
}
public RadarMapView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);;
mainPaint=new Paint();
textPaint = new Paint();
valuePaint = new Paint();
mainPaint.setColor(Color.BLUE);
mainPaint.setStyle(Paint.Style.STROKE);
mainPaint.setAntiAlias(true);
textPaint.setColor(Color.BLUE);
textPaint.setStyle(Paint.Style.STROKE);
textPaint.setAntiAlias(true);
valuePaint.setColor(Color.BLUE);
valuePaint.setStyle(Paint.Style.STROKE);
valuePaint.setAntiAlias(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
contextX=w/2;
contextY=h/2;
radius=Math.min(w,h)/2*0.8f;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawPolygon(canvas);//绘制正多边形
drawLigature(canvas);
drawTitle(canvas);//绘制标题
drawCover(canvas);//绘制覆盖区域
}
private void drawLigature(Canvas canvas) {
Path path=new Path();
for (int i=0;i<count;i++){
path.reset();
path.moveTo(contextX,contextY);//将起点移到多边形中心点
float x= (float) (contextX+radius*Math.cos(angle*i));
float y = (float) (contextY + radius * Math.sin(angle * i));
path.lineTo(x,y);
canvas.drawPath(path,mainPaint);
}
}
private void drawPolygon(Canvas canvas) {
Path path=new Path();
float r=radius/(count-1);
for(int i=1;i<count;i++){//绘制的层数
float curR=r*i;//当前层数半径
path.reset();//使path重置
for (int j=0;j<count;j++){
if(j==0){//第一次绘制,将起点移到第一个将要绘制的点,即多边形中心点移动到起点
path.moveTo(contextX+curR,contextY);
}else{
//使用三角函数计算x坐标和有坐标
float x= (float) (contextX+curR*Math.cos(angle*j));
float y= (float) (contextY+curR*Math.sin(angle*j));
path.lineTo(x,y);}
}
path.close();
canvas.drawPath(path,mainPaint);
}
}
private void drawTitle(Canvas canvas) {
Paint.FontMetrics fontMetrics=textPaint.getFontMetrics();
float fontHeight=fontMetrics.descent-fontMetrics.ascent;//得到文字的高度
for(int i=0;i<count;i++){
float x = (float) (contextX + (radius + fontHeight / 2) * Math.cos(angle * i));
float y = (float) (contextY + (radius + fontHeight / 2) * Math.sin(angle * i));
if(angle*i>=0&&angle*i<=Math.PI/2){
//第四象限
canvas.drawText(titles[i],x,y+fontHeight/3,textPaint);
}else if(angle*i>Math.PI/2&&angle*i<=Math.PI){
//第三象限
float drs=textPaint.measureText(titles[i]);
canvas.drawText(titles[i],x-drs,y+fontHeight/3,textPaint);
}else if(angle*i>Math.PI&&angle*i<3*Math.PI/2){
//第二象限
float drs=textPaint.measureText(titles[i]);
canvas.drawText(titles[i],x-drs,y,textPaint);
}else if(angle*i>3*Math.PI/2&&angle*i<=2*Math.PI){
//第一象限
canvas.drawText(titles[i],x,y,textPaint);
}
}
}
private void drawCover(Canvas canvas) {
Path path=new Path();
for (int i = 0; i < count; i++) {
float percent = (float) (temValuses[i] / maxValue);//当前数值占最大数值百分比
float x = (float) (contextX + radius * percent * Math.cos(angle * i));
float y = (float) (contextY + radius * percent * Math.sin(angle * i));
if (i == 0) {
path.moveTo(contextX + radius * percent, contextY);//将起点移到第一个将要绘制的点
} else {
path.lineTo(x, y);
}
valuePaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(x, y, 8, valuePaint);//绘制小圆点
}
valuePaint.setAlpha(130);//设置画笔透明度
valuePaint.setStyle(Paint.Style.FILL_AND_STROKE);//填充并且描边
canvas.drawPath(path, valuePaint);
mCurrent+=mPiece;
if(mCurrent<mDuration) {
for (int i=0;i<count;i++){
temValuses[i]=values[i]*mCurrent/mDuration;
}
postInvalidateDelayed((long) mPiece);//刷新view
}
}
public void setData(RadarMapData radarMapData) {
this.radarMapData = radarMapData;
count = radarMapData.getCount();
angle = (float) (Math.PI * 2 / count);
mainPaint.setColor(radarMapData.getMainPaintColor());
titles=radarMapData.getTitles();
textPaint.setTextSize(radarMapData.getTextSize());
values=radarMapData.getValuse();
}
public void start(){
mCurrent=0;
postInvalidate();
}
}
完整代码
https://github.com/Huaireny/CustomView.git
感谢以下文章的帮助
http://blog.csdn.net/crazy__chen/article/details/50163693
上一篇: dart基础之环境安装
推荐阅读
-
Android自定义View实现公交成轨迹图
-
Android自定义view实现动态柱状图
-
Android自定义view利用Xfermode实现动态文字加载动画
-
Android自定义View实现饼状图带动画效果
-
Android自定义view之太极图的实现教程
-
Android自定义View实现圆形切图效果
-
Android自定义view、动手实现一个简单的xmind思维导图结构
-
Android 自定义View实现动画形式加载环形图
-
Android自定义控件:图形报表的实现(折线图、曲线图、动态曲线图)(View与SurfaceView分别实现图表控件)
-
Android自定义View 雷达图(蛛网)动态实现