總體實現的目標如下:
我們要實現一個戰鬥力的網狀圖,可以隨意改變網狀圖的邊數,從外面傳入邊數後可以自動調節各個屬性值,圖示爲正4、7、8、13邊形的戰力圖表。
根據圖中顯示,整體實現步驟大致可以分爲4步:
- 步驟一:首先是畫正N邊形
- 步驟二:從中心點到N邊形各個頂點的連線
- 步驟三:畫戰力區域
- 步驟四:畫戰力值文字
步驟一:首先是要畫一個正N邊形,圖例爲正六邊形:
畫正N邊形最重要的就是求出N邊形的每個頂點座標,然後將這些頂點座標連接起來就可以了。
延伸一下:我們可以將問題轉化爲求圓周上的每個點的座標,首先要學習下Math.sin(弧度)、Math.cos(弧度),注意這裏的參數是弧度而非角的度數。
弧度的計算公式爲: 角度*(PI/180)
30° 角度 的弧度 = 30 * (PI/180)
如何得到圓上每個點的座標?
解決思路:根據三角形的正玄、餘弦來得值;
假設一個圓的圓心座標是(a,b),半徑爲r,
則圓上每個點的:
X座標=a + Math.sin(角度 * (Math.PI / 180)) * r ;
Y座標=b + Math.cos(角數 * (Math.PI / 180)) * r ;
例子:求時鐘的秒針轉動一圈的軌跡?
假設秒針的初始值(起點)爲12點鐘方向,圓心的座標爲(a,b)。
解決思路:一分鐘爲60秒,一個圓爲360°,所以平均每秒的轉動角度爲 360°/60 = 6°;
for (int times = 0; times < 60; times++) {
int hudu = 6 * (Math.PI / 180) * times;
int X = a + Math.sin(hudu) * r;
int Y = b - Math.cos(hudu) * r // 注意此處是“-”號,因爲我們要得到的Y是相對於(0,0)而言的。
}
1、本例是以“12點爲起點, 角度增大時爲順時針方向“,求X座標和Y座標的方法是:
X座標=a + Math.sin(角度 * (Math.PI / 180)) * r ;
Y座標=b - Math.cos(角數 * (Math.PI / 180)) * r ;
2、一般“3點爲起點, 角度增大時爲逆時針方向“,求X座標和Y座標的方法是:
X座標 = a + Math.cos(角度 * (Math.PI / 180)) * r;
Y座標 = b - Math.sin(角度 * (Math.PI / 180)) * r;
明白了圓周上各個點的座標求法,下面看正N邊形,各個頂點座標是怎麼求的
六邊形座標計算:
右上角的頂點座標:
x點座標:中心點 + R*sin(60°)
y點座標:中心點 - R*cos(60°)
整體代碼:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
public class SpiderView extends View {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
int width;
int height;
//N邊形的邊數
int edges = 6;
//根據邊數求得每個頂點對應的度數
double degrees = 360 / edges;
//根據度數,轉化爲弧度
double hudu = (Math.PI / 180) * degrees;
{
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
paint.setColor(Color.BLACK);
}
public SpiderView(Context context) {
super(context);
}
public SpiderView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public SpiderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawPolygon(canvas, 500.0F);
drawPolygon(canvas, 400.0F);
drawPolygon(canvas, 300.0F);
drawPolygon(canvas, 200.0F);
drawPolygon(canvas, 100.0F);
}
private void drawPolygon(Canvas canvas, float radius) {
Path path = new Path();
path.moveTo(width / 2, height / 2 - radius);//從上面的頂點出發
float endx, endy;
for (int i = 0; i < edges; i++) {
endx = (float) (width / 2 + radius * Math.sin(hudu * i));
endy = (float) (height / 2 - radius * Math.cos(hudu * i));
path.lineTo(endx, endy);
}
path.close();
canvas.drawPath(path, paint);
}
}
上面代碼中,只需要改變正N邊形的邊數edges即可。
步驟二:從中心點到各個頂點的連線
思路和畫正N邊形一樣的,只是這裏每畫完一條射線都要將起始點挪動到原始點去,然後再畫從原始點到各個頂點的下一條射線。
/**
* 從中心點到各個頂點畫一條線
* @param canvas
* @param radius
*/
private void drawLines(Canvas canvas, float radius) {
//從中心點出發
Path path = new Path();
path.moveTo(width / 2, height / 2);
float endx, endy;
for (int i = 0; i < edges; i++) {
endx = (float) (width / 2 + radius * Math.sin(hudu * i));
endy = (float) (height / 2 - radius * Math.cos(hudu * i));
path.lineTo(endx, endy);
canvas.drawPath(path, radialLinesPaint);
//畫完一條線後,重置起點在中心點,再畫下一條直線
endx = width/2;
endy = height/2;
path.moveTo(endx, endy);
}
}
步驟三:畫戰力區域
戰力區域思路和畫正N邊形是一致的,只是這裏每個點是半徑的0~1的比率取值即可,這裏通過rankData取值:
/**
* 畫戰力值區域
*
* @param canvas
* @param radius
*/
private void drawRanks(Canvas canvas, float radius) {
Path path = new Path();
float endx, endy;
for (int i = 0; i < edges; i++) {
endx = (float) (width / 2 + radius * Math.sin(hudu * i) * rankData[i]);
endy = (float) (height / 2 - radius * Math.cos(hudu * i) * rankData[i]);
if (i == 0) {
path.moveTo(width / 2, (float) (height / 2 - radius * 0.5));
} else {
path.lineTo(endx, endy);
}
}
path.close();
canvas.drawPath(path, rankPaint);
}
步驟四:畫戰力值文字:
思路和上面兩個一致,這裏只是需要調整文字距離最外圈的距離:
/**
* 畫戰力文字
*
* @param canvas
* @param radius
*/
private void drawRankText(Canvas canvas, float radius) {
float endx, endy;
Rect bounds = new Rect();
for (int i = 0; i < edges; i++) {
rankTextPaint.getTextBounds(rankText[i], 0, rankText[i].length(), bounds);
endx = (float) (width / 2 + radius * 1.2 * Math.sin(hudu * i) - (bounds.right - bounds.left) / 2);
endy = (float) (height / 2 - radius * 1.1 * Math.cos(hudu * i) + (bounds.bottom - bounds.top) / 2);
canvas.drawText(rankText[i], endx, endy, rankTextPaint);
}
}
最後看下整體的實現效果,代碼中改變下edges的數目即可變成正N邊形:
戰力值的所有代碼:
package com.test.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
public class SpiderView extends View {
//正N邊形邊線
Paint edgesPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//中心發出的射線
Paint radialLinesPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//等級線
Paint rankPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//等級文字
Paint rankTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//自定義view的寬高
float width, height;
//戰力值數據
private double[] rankData = {0.5, 0.2, 0.8, 0.6, 0.9, 0.6, 0.2, 0.8, 0.4, 0.9, 0.1, 0.7, 0.2, 0.9};
//戰力種類
private String[] rankText = {"擊殺", "助攻", "金錢", "物理", "防禦", "魔法", "裝備", "血量", "魔抗", "穿甲", "綜合", "裝甲", "魔抗"};
//N邊形的邊數
int edges = 7;
//根據邊數求得每個頂點對應的度數
double degrees = 360 / edges;
//根據度數,轉化爲弧度
double hudu = (Math.PI / 180) * degrees;
{
edgesPaint.setStyle(Paint.Style.STROKE);
edgesPaint.setStrokeWidth(3);
edgesPaint.setColor(Color.BLACK);
radialLinesPaint.setStyle(Paint.Style.STROKE);
radialLinesPaint.setStrokeWidth(2);
radialLinesPaint.setColor(Color.BLUE);
rankPaint.setStyle(Paint.Style.STROKE);
rankPaint.setStrokeWidth(10);
rankPaint.setColor(Color.RED);
rankTextPaint.setStyle(Paint.Style.FILL);
rankTextPaint.setStrokeWidth(1);
rankTextPaint.setColor(Color.BLACK);
rankTextPaint.setTextSize(50);
}
public SpiderView(Context context) {
super(context);
}
public SpiderView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public SpiderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//正N邊形個數
edgesPaint.setStyle(Paint.Style.FILL);
edgesPaint.setColor(Color.parseColor("#c3e3e5"));
drawPolygon(canvas, 400.0F);
edgesPaint.setColor(Color.parseColor("#85cdd4"));
drawPolygon(canvas, 300.0F);
edgesPaint.setColor(Color.parseColor("#48afb6"));
drawPolygon(canvas, 200.0F);
edgesPaint.setColor(Color.parseColor("#22737b"));
drawPolygon(canvas, 100.0F);
//從中心點到各個頂點的射線
drawLines(canvas, 400);
//畫戰力值區域
drawRanks(canvas, 400);
//畫戰力文字
drawRankText(canvas, 400);
}
/**
* 畫戰力值區域
*
* @param canvas
* @param radius
*/
private void drawRanks(Canvas canvas, float radius) {
Path path = new Path();
float endx, endy;
for (int i = 0; i < edges; i++) {
endx = (float) (width / 2 + radius * Math.sin(hudu * i) * rankData[i]);
endy = (float) (height / 2 - radius * Math.cos(hudu * i) * rankData[i]);
if (i == 0) {
path.moveTo(width / 2, (float) (height / 2 - radius * 0.5));
} else {
path.lineTo(endx, endy);
}
}
path.close();
canvas.drawPath(path, rankPaint);
}
/**
* 畫戰力文字
*
* @param canvas
* @param radius
*/
private void drawRankText(Canvas canvas, float radius) {
float endx, endy;
Rect bounds = new Rect();
for (int i = 0; i < edges; i++) {
rankTextPaint.getTextBounds(rankText[i], 0, rankText[i].length(), bounds);
endx = (float) (width / 2 + radius * 1.2 * Math.sin(hudu * i) - (bounds.right - bounds.left) / 2);
endy = (float) (height / 2 - radius * 1.1 * Math.cos(hudu * i) + (bounds.bottom - bounds.top) / 2);
canvas.drawText(rankText[i], endx, endy, rankTextPaint);
}
}
/**
* 從中心點到各個頂點的射線
*
* @param canvas
* @param radius
*/
private void drawLines(Canvas canvas, float radius) {
//從中心點出發
Path path = new Path();
path.moveTo(width / 2, height / 2);
float endx, endy;
for (int i = 0; i < edges; i++) {
endx = (float) (width / 2 + radius * Math.sin(hudu * i));
endy = (float) (height / 2 - radius * Math.cos(hudu * i));
path.lineTo(endx, endy);
canvas.drawPath(path, radialLinesPaint);
//畫完一條線後,重置起點在中心點,再畫下一條直線
endx = width / 2;
endy = height / 2;
path.moveTo(endx, endy);
}
}
/**
* 畫正N邊形
*
* @param canvas
* @param radius
*/
private void drawPolygon(Canvas canvas, float radius) {
//從上面的頂點出發
Path path = new Path();
path.moveTo(width / 2, height / 2 - radius);
float endx, endy;
for (int i = 0; i < edges; i++) {
endx = (float) (width / 2 + radius * Math.sin(hudu * i));
endy = (float) (height / 2 - radius * Math.cos(hudu * i));
path.lineTo(endx, endy);
}
path.close();
canvas.drawPath(path, edgesPaint);
}
}