六边形战士—雷达图实现
相信大家都看了之前的新闻,世乒赛日本直播版,中二爆表,马龙的六边形战力图全满。
图是这样的。
于是乎想实现一个自定义view实现类似的效果。 这种图正式名称叫雷达图(Radar Chart),又可称为戴布拉图、蜘蛛网图(Spider Chart),是财务分析 报表的一种。但是现在已经应用到很多领域,特别是竞技体育方面对队伍或者选手的实力分析。
整理了一下思路和查询了一下相关知识,结合前人的代码,实现了自定义雷达图。
下面写一下实现思路:首先我把雷达图分为底层蜘蛛网+内容区,底层蜘蛛网的六个属性和内容区的六个点分别从2个数组去获取数值,接来下只要依次绘制两层图即可。
1.初始化
private int count=6; //六边形,数据个数6 private float angle= (float) (Math.PI/3); //60度 private double[] data={50,50,50,50,50,50,50}; //默认数据 private float maxValue=100; //默认最大值 private String[] titles={"a","b","c","d","e","f"}; //默认标题 private Paint radarPaint; //蜘蛛网画笔 private Paint valuePaint; //内容区画笔 private Paint textPaint; //文字画笔 private float radius; //网格最大半径 private int centerX; //中心X private int centerY; //中心Y public MyRadar(Context context) { this(context,null); } public MyRadar(Context context, AttributeSet attrs) { this(context, attrs,0); } public MyRadar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); }
private void init() { radarPaint=newPaint(); radarPaint.setAntiAlias(true); radarPaint.setColor(Color.GRAY); radarPaint.setStyle(Paint.Style.STROKE); valuePaint=newPaint(); valuePaint.setAntiAlias(true); valuePaint.setColor(Color.BLUE); valuePaint.setStyle(Paint.Style.FILL_AND_STROKE); textPaint=newPaint(); textPaint.setTextSize(sp2px(15)); textPaint.setAntiAlias(true); textPaint.setStyle(Paint.Style.FILL); textPaint.setColor(Color.BLACK); }
@Override protected voidonSizeChanged(intw,inth,intoldw,intoldh) { radius= Math.min(h, w)/2*0.65f; //得到半径 centerX= w/2; //得到中心点 centerY= h/2; postInvalidate(); super.onSizeChanged(w, h, oldw, oldh); }
2,重点的绘图过程来了,第一步,绘制蜘蛛网图 ,绘图之前我们先复习下数学的知识。
首先,一个正六边形是圆的内接正六边形,每个边对应的圆心角是六十度。
其次,Android中View的坐标系是我们数学课是不一样的!(很容易被忽视)
这里的1,2,3,4代表的是象限,因为y的方向不同,导致了象限与数学书中的不同
首先,利用三角函数的知识绘制蜘蛛网图
cosX对应映射在X轴上长度,sinX对应映射在Y轴上长度。所以可以通过每次X加上60度(1/3PI)去得到边角点。
private void drawHexagon(Canvas canvas) { Path path=new Path(); float r=radius/(count-1); for (int i = 0; i <count ; i++) { float R=r*i; path.reset(); for (int j=0;j<count;j++){ if(j==0){ path.moveTo(centerX+R,centerY); } else{ float x= (float) (centerX+R*Math.cos(angle*j)); float y= (float) (centerY+R*Math.sin(angle*j)); path.lineTo(x,y); } } path.close(); canvas.drawPath(path,radarPaint); } for (int i = 0; i <count ; i++) { path.reset(); path.moveTo(centerX,centerY); float x= (float) (centerX+radius*Math.cos(angle*i)); float y= (float) (centerY+radius*Math.sin(angle*i)); path.lineTo(x,y); canvas.drawPath(path,radarPaint); } }
效果:
接来下绘制标题,我们想要的效果是这样的 标题离边角有一定距离,且呈现对称效果。
这时的解决方案是将以比半径稍大的长度作为新的半径,这样可以在六个角外面得到相应的六个点,再在这六个点处绘制标题。
这里能否直接以六个点为坐标依次绘制文字? 答案是否定的。原因如下图:
没错,绘制文字时是将坐标作为文字的左下角,如果不在不同的象限做出处理,文字将无法实现对称。如下:
代码:
private void drawText(Canvas canvas) { Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); float fontHeight = fontMetrics.descent - fontMetrics.ascent; for (int i = 0; i <count ; i++) { float x= (float) (centerX+(radius+fontHeight/2)*Math.cos(angle*i)); float y= (float) (centerY+(radius+fontHeight/2)*Math.sin(angle*i)); if(angle*i>=0&&angle*i<=Math.PI/2){ canvas.drawText(titles[i], x,y+fontHeight/2,textPaint); } else if(angle*i>Math.PI/2&&angle*i<=Math.PI){ float dis = textPaint.measureText(titles[i]); canvas.drawText(titles[i], x-dis,y+fontHeight/2,textPaint); } else if(angle*i>=Math.PI&&angle*i<3*Math.PI/2){ float dis = textPaint.measureText(titles[i]); canvas.drawText(titles[i], x-dis,y,textPaint); }else if(angle*i>=3*Math.PI/2&&angle*i<=Math.PI*2){ canvas.drawText(titles[i], x,y,textPaint); } } }
最后绘制内容区域也不难:
private void drawRegion(Canvas canvas) { Path path = new Path(); valuePaint.setAlpha(255); for(int i=0;i<count;i++){ double percent = data[i]/maxValue; float x = (float) (centerX+radius*Math.cos(angle*i)*percent); float y = (float) (centerY+radius*Math.sin(angle*i)*percent); if(i==0){ path.moveTo(x, centerY); }else{ path.lineTo(x,y); } //绘制小圆点 canvas.drawCircle(x,y,5,valuePaint); } valuePaint.setStyle(Paint.Style.STROKE); canvas.drawPath(path, valuePaint); valuePaint.setAlpha(127); //绘制填充区域 valuePaint.setStyle(Paint.Style.FILL_AND_STROKE); canvas.drawPath(path, valuePaint); }
完整的draw方法
@Override protected void onDraw(Canvas canvas) { drawHexagon(canvas); drawText(canvas); drawRegion(canvas); }
到这里差不多就结束了,后续就是对外暴露一些方法,以及wrapcontent设置默认大小
//设置数值 public void setData(double[] data) { this.data = data; } public float getMaxValue() { return maxValue; } //设置最大数值 public void setMaxValue(float maxValue) { this.maxValue = maxValue; } //设置标题颜色 public void setTextPaintColor(int color){ textPaint.setColor(color); } //设置覆盖局域颜色 public void setValuePaintColor(int color){ valuePaint.setColor(color); } //设置雷达图颜色 public void setMainPaintColor(int color){ radarPaint.setColor(color); }
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(sp2px(300),sp2px(300)); } else if (widthMeasureSpec==MeasureSpec.AT_MOST){ setMeasuredDimension(sp2px(250),heightSpecSize); }else if (heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,sp2px(250)); } }
最终在acitivty设置数据和标题,最终效果:
public class MainActivity extends AppCompatActivity { private MyRadar mRadar; double[] data={100,100,100,100,50,100,20}; String[] titles={"发球","经验","防守","技巧","速度","力量"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRadar= (MyRadar) findViewById(R.id.radar); mRadar.setData(data); mRadar.setTitles(titles); } }
源码在github:https://github.com/xurui1995/Radar