上週專案中要用到一個蛛網評分控制元件,於是就先上Github搜,搜了半天沒搜著(也可能是我搜的關鍵詞不對),那隻好自己寫一個了,就叫SpiderWebScoreView
先放一張最終效果圖:
先整理一下需求:
- 要能支援任意個角以及任意層級
- 不管是多少個角都要能自動左右對稱
- 蛛網圖形外圈的文字要能自定義(甚至是圖片)
然後整理一下一個蛛網評分控制元件的實現流程:
- 首先要根據角的個數以及層數畫一個蜘蛛網
- 然後根據最大分值和所有分值畫一個分數圖形
- 最後在蜘蛛網的外圈把文案畫出來
我們第一步來畫蜘蛛網
先整理一下畫蜘蛛網需要的知識點:
- 一層一層的多邊形要用Path畫
- 從中心出發到每個角的線用drawLine
- 根據角度計算圓上的座標
接下來看一下怎麼根據角度計算圓上的座標,直接上程式碼
float centerX = 100f; // 已知圓心X座標為100
float centerY = 100f; // 已知圓心Y座標為100
float radius = 100f; // 已知半徑為100
float angle = 45f; // 已知角度為45°
double radians = Math.toRadians(angle); // 計算出弧度
float x = (float) (centerX + Math.sin(radians) * radius); // 計算出X座標
float y = (float) (centerY - Math.cos(radians) * radius); // 計算出Y座標
複製程式碼
既然上述技術問題都解決了,就開始畫吧
private int angleCount = 5; // 整個蛛網有幾個角
private int hierarchyCount = 5; // 整個蛛網分多少層(例如最大分數是10分,分5層,那麼每層就代表2分)
private int lineColor = 0xFF000000; // 蛛網線條的顏色
private float lineWidth = -1; // 蛛網線條的寬度
private float centerX; // 中心點X座標
private float centerY; // 中心點Y座標
private float radius; // 整個蛛網圖的半徑
private Paint linePaint;
private Path path;
....
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
reset();
}
private void reset(){
if(angleCount != 0 && hierarchyCount != 0){
int viewWidth = getWidth();
int viewHeight = getHeight();
centerX = viewWidth / 2;
centerY = viewHeight / 2;
radius = Math.min(viewWidth, viewHeight) / 2;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawAllHierarchy(canvas);
drawAllLine(canvas);
}
/**
* 繪製所有的層
* @param canvas Canvas
*/
private void drawAllHierarchy(Canvas canvas) {
float averageRadius = radius / hierarchyCount;
for(int w = 0; w < hierarchyCount; w++){
drawHierarchyByRadius(canvas, averageRadius *(w+1));
}
}
/**
* 根據半徑繪製一層
* @param canvas Canvas
* @param currentRadius 當前半徑
*/
private void drawHierarchyByRadius(Canvas canvas, float currentRadius) {
path.reset();
float nextAngle;
float nextRadians;
float nextPointX;
float nextPointY;
float averageAngle = 360 / angleCount;
float offsetAngle = averageAngle > 0 && angleCount % 2 == 0 ? averageAngle / 2 : 0;
for (int position = 0; position < angleCount; position++) {
nextAngle = offsetAngle + (position * averageAngle);
nextRadians = (float) Math.toRadians(nextAngle);
nextPointX = (float) (centerX + Math.sin(nextRadians) * currentRadius);
nextPointY = (float) (centerY - Math.cos(nextRadians) * currentRadius);
if(position == 0){
path.moveTo(nextPointX, nextPointY);
}else{
path.lineTo(nextPointX, nextPointY);
}
}
path.close();
canvas.drawPath(path, linePaint);
}
/**
* 繪製所有的線
* @param canvas Canvas
*/
private void drawAllLine(Canvas canvas){
float nextAngle;
float nextRadians;
float nextPointX;
float nextPointY;
float averageAngle = 360 / angleCount;
float offsetAngle = averageAngle > 0 && angleCount % 2 == 0 ? averageAngle / 2 : 0;
for(int position = 0; position < angleCount; position++){
nextAngle = offsetAngle + (position * averageAngle);
nextRadians = (float) Math.toRadians(nextAngle);
nextPointX = (float) (centerX + Math.sin(nextRadians) * radius);
nextPointY = (float) (centerY - Math.cos(nextRadians) * radius);
canvas.drawLine(centerX, centerY, nextPointX, nextPointY, linePaint);
}
}
複製程式碼
詳解:
- onSizeChanged的時候會根據view的寬高計算圓心座標和半徑
- 先根據層數計算每層的半徑
- 接下來再根據角個數計算出每個角的度數
- 有了角度和半徑後就可以根據上面的演算法算出每個點的座標了
- 座標都算出來了就直接用Path連線,然後繪製即可
這裡面需要額外注意的就是offsetAngle的計算,offsetAngle用來偏移整個圖形,讓整個圖形左右對稱,演算法就是對於角個數是奇數的不用偏移,是偶數的偏移平均角度除以2即可
第二步來畫分數圖形
和畫蜘蛛網所用到的知識點一樣,只不過半徑要根據分值和最大分值來算,如下:
private float maxScore = 10f; // 最大分數
private float[] scores; // 分數列表
private int scoreColor = 0x80F65801; // 分數圖形的顏色
private int scoreStrokeColor = 0xFFF65801; // 分數圖形描邊的顏色
private float scoreStrokeWidth = -1; // 分數圖形描邊的寬度
private boolean disableScoreStroke; // 禁用分數圖形的描邊
private Paint scorePaint;
private Paint scoreStrokePaint;
...
/**
* 繪製分數圖形
* @param canvas Canvas
*/
private void drawScore(Canvas canvas){
if(scores == null || scores.length <= 0){
return;
}
path.reset();
float nextAngle;
float nextRadians;
float nextPointX;
float nextPointY;
float currentRadius;
float averageAngle = 360 / angleCount;
float offsetAngle = averageAngle > 0 && angleCount % 2 == 0 ? averageAngle / 2 : 0;
for (int position = 0; position < angleCount; position++) {
currentRadius = (scores[position] / maxScore) * radius;
nextAngle = offsetAngle + (position * averageAngle);
nextRadians = (float) Math.toRadians(nextAngle);
nextPointX = (float) (centerX + Math.sin(nextRadians) * currentRadius);
nextPointY = (float) (centerY - Math.cos(nextRadians) * currentRadius);
if(position == 0){
path.moveTo(nextPointX, nextPointY);
}else{
path.lineTo(nextPointX, nextPointY);
}
}
path.close();
canvas.drawPath(path, scorePaint);
// 繪製描邊
if(!disableScoreStroke){
if(scoreStrokePaint == null){
scoreStrokePaint = new Paint();
scoreStrokePaint.setColor(scoreStrokeColor);
scoreStrokePaint.setStyle(Paint.Style.STROKE);
scoreStrokePaint.setAntiAlias(true);
if(scoreStrokeWidth > 0){
scoreStrokePaint.setStrokeWidth(scoreStrokeWidth);
}
}
canvas.drawPath(path, scoreStrokePaint);
}
}
複製程式碼
第三步畫外面那圈文字
到了這裡有些猶難辦了,考慮到外面這圈文字變數比較大,不同的APP會有不同的需求,比如有的需求是一行純文字,有的是兩行純文字但兩行的樣式不一樣,更甚者可能是是文字加圖片。
要是直接往Canvas上寫文字是絕對滿足不了這樣的需求的,既然我要做一個開源的控制元件那就必須要解決這樣的問題,要不然別人是用不了的
想到這裡腦海裡立馬就有解決方案了,那就是自定義一個圓形的Layout,就叫CircularLayout,把CircularLayout放在SpiderWebScoreView上面,用的時候直接往CircularLayout裡面新增View即可,這樣想要什麼樣式就加什麼樣的View即可
具體的自定義CircularLayout的細節就不再贅述了,用到的知識點跟SpiderWebScoreView一樣
最後放上蛛網評分控制元件的Github地址 github.com/xiaopansky/… ,歡迎大家訪問使用