相信大家都看了之前的新聞,世乒賽日本直播版,中二爆表,馬龍的六邊形戰力圖全滿。
圖是這樣的。
於是乎想實現一個自定義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