自定义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);}@Overrideprotected 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;}@Overrideprotected 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;@Overrideprotected 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;@Overrideprotected 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);}@Overrideprotected 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;}@Overrideprotected 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();}}
完整代码
/Huaireny/CustomView.git
感谢以下文章的帮助
/crazy__chen/article/details/50163693