首先看看效果圖:
先簡要說一下這里需要涉及到的知識點(diǎn):
- 2D繪圖基礎(chǔ)博烂。
- 高中基本的三角函數(shù) Sin畜伐,Cos。
參考的文章:
這里為了尊重上面這篇文章的作者遇西,需要說明一下,下面的代碼有部分是參考上面這篇文章的严嗜。這里我學(xué)習(xí)之后有了自己的理解粱檀。做了一點(diǎn)小改動,然后以自己的思路來捋一捋漫玄。希望我的文字對你更有幫助茄蚯,哈哈。 (Pentagon --五邊形)
繪制思路:
- 計算三個五邊形的五個頂點(diǎn)的坐標(biāo)睦优,用 path 連接起來并繪制
- 計算要顯示的數(shù)據(jù)的五個頂點(diǎn)的坐標(biāo)渗常,用 path 連接起來并繪制
- 繪制五條射線
- 計算圖標(biāo)和標(biāo)題的坐標(biāo)位置,并繪制
- 繪制中間的分?jǐn)?shù)
第一步:繪制三個五邊形和紅色五邊形
初始化成員變量
private int dataCount = 5;//多邊形維度
private float radian = (float) (Math.PI * 2 / dataCount);//每個維度的角度
private float radius;//一條星射線的長度汗盘,即是發(fā)散的五條線白線
private int centerX;//中心坐標(biāo) Y
private int centerY;//中心坐標(biāo) X
private String[] titles = {"履約能力", "信用歷史", "人脈關(guān)系", "行為偏好", "身份特質(zhì)"};//標(biāo)題
private int[] icos = {R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher};//五個維度的圖標(biāo)
private float[] data = {170, 180, 100, 170, 150};//五個維度的數(shù)據(jù)值
private float maxValue = 190;//每個維度的最大值
private Paint mPaintText;//繪制文字的畫筆
private int radarMargin = 40;//
private int mAlpha;//白色五邊形的透明度
private Path mPentagonPath;//記錄白色五邊形的路徑
private Paint mPentagonPaint;//繪制白色五邊形的畫筆
private Path mDataPath;//記錄紅色五邊形的路徑
private Paint mDataPaint;//繪制紅色五邊形的畫筆
構(gòu)造方法中初始化的數(shù)據(jù)
private void init() {
mPentagonPaint = new Paint();//初始化白色五邊形的畫筆
mPentagonPaint.setAntiAlias(true);//
mPentagonPaint.setStrokeWidth(5);//
mPentagonPaint.setColor(Color.WHITE);//
mPentagonPaint.setStyle(Paint.Style.FILL_AND_STROKE);//
mDataPaint = new Paint();//初始化紅色五邊形的畫筆
mDataPaint.setAntiAlias(true);//
mDataPaint.setStrokeWidth(10);//
mDataPaint.setColor(Color.RED);//
mDataPaint.setAlpha(150);//
mDataPaint.setStyle(Paint.Style.STROKE);//
mPaintText = new Paint();//初始化文字畫筆
mPaintText.setAntiAlias(true);//
mPaintText.setTextSize(50);//
mPaintText.setColor(Color.WHITE);//
mPaintText.setStyle(Paint.Style.FILL);//
mPentagonPath = new Path();//初始化白色五邊形路徑
mDataPath = new Path();//初始化紅色五邊形路徑
radius = 80;//星射線的初始值凳谦,也是最小的五邊形的一條星射線的長度(后期會遞增)
mAlpha = 150;//白色五邊形的透明度(后期后遞減)
}
初始化數(shù)據(jù)之后是通過 radius 的長度來計算五邊形五個頂點(diǎn)的坐標(biāo)值,后期通過改變 radius 的值衡未,來達(dá)到計算三個白色五邊形的五個頂點(diǎn)的值尸执。這里給出一張圖,幫助理解缓醋。通過圖中的兩個紅色三角形就可以求出頂點(diǎn)的坐標(biāo)了如失。
**右上角的頂點(diǎn)為第一個點(diǎn),順時針計算送粱,position 依次是 0褪贵,1,2抗俄,3脆丁,4 **
public Point getPoint(int position) {
return getPoint(position, 0, 1);
}
// 參數(shù):position:頂點(diǎn)的位置,radarMargin:邊距动雹,percent:星射線長度的百分比槽卫,用于計算紅色五邊形的頂點(diǎn)
public Point getPoint(int position, int radarMargin, float percent) {//以五邊形的中心點(diǎn)為坐標(biāo)原點(diǎn)
int x = 0;
int y = 0;
switch (position) {
case 0://第一象限,右上角頂點(diǎn)的坐標(biāo)計算
x = (int) (centerX + (radius + radarMargin) * Math.sin(radian) * percent);
y = (int) (centerY - (radius + radarMargin) * Math.cos(radian) * percent);
break;
case 1://第四象限胰蝠,右下角頂點(diǎn)的坐標(biāo)計算
x = (int) (centerX + (radius + radarMargin) * Math.sin(radian / 2) * percent);
y = (int) (centerY + (radius + radarMargin) * Math.cos(radian / 2) * percent);
break;
case 2://第三象限歼培,左下角頂點(diǎn)的坐標(biāo)計算
x = (int) (centerX - (radius + radarMargin) * Math.sin(radian / 2) * percent);
y = (int) (centerY + (radius + radarMargin) * Math.cos(radian / 2) * percent);
break;
case 3://第二象限,左上角頂點(diǎn)的坐標(biāo)計算
x = (int) (centerX - (radius + radarMargin) * Math.sin(radian) * percent);
y = (int) (centerY - (radius + radarMargin) * Math.cos(radian) * percent);
break;
case 4:// Y 軸正方向頂點(diǎn)的計算
x = centerX;
y = (int) (centerY - (radius + radarMargin) * percent);
break;
}
return new Point(x, y);
}
基礎(chǔ)工作都做足了茸塞,那么就進(jìn)行五邊形的繪制了
private void drawPentagon(Canvas canvas) {
for (int j = 0; j < 3; j++) {//繪制三層白色五邊形
radius += 70;//每一層五邊形的星射線增加 70 的長度
mAlpha -= 30;//每一層五邊形的透明度減少 30
mPentagonPaint.setAlpha(mAlpha);
for (int i = 0; i < dataCount; i++) {//繪制一層
if (i == 0) {
mPentagonPath.moveTo(getPoint(i).x, getPoint(i).y);
} else {
mPentagonPath.lineTo(getPoint(i).x, getPoint(i).y);
}
}
mPentagonPath.close();
canvas.drawPath(mPentagonPath, mPentagonPaint);
}
for (int i = 0; i < dataCount; i++) {//繪制紅色五邊形
float percent = data[i] / maxValue;//數(shù)據(jù)值與最大值的百分比
if (i == 0) {
mDataPath.moveTo(getPoint(i, 0, percent).x, getPoint(i, 0, percent).y);//通過百分比計算出紅色頂點(diǎn)的位置
} else {
mDataPath.lineTo(getPoint(i, 0, percent).x, getPoint(i, 0, percent).y);
}
}
mDataPath.close();
canvas.drawPath(mDataPath, mDataPaint);
}
第二步:繪制五條星射線
先來看看這一步的效果圖:
繪制好五邊形之后 radius 的值已經(jīng)為最大五邊形的星射線的長度了躲庄。
private void drawFiveLine(Canvas canvas) {
mPentagonPaint.setColor(Color.WHITE);//設(shè)置顏色為白色
mPentagonPaint.setStrokeWidth(2);//設(shè)置寬度為2
for (int i = 0; i < dataCount; i++) {
canvas.drawLine(centerX, centerY, getPoint(i).x, getPoint(i).y, mPentagonPaint);//繪制
}
}
第三步:繪制五個標(biāo)題
先來看看這一步的效果圖:
在這一步,相對難一點(diǎn)的就是坐標(biāo)的計算了钾虐。第一個頂點(diǎn)的坐標(biāo)經(jīng)過添加 radarMargin 值之后就可以直接使用了噪窘,其他頂點(diǎn)還需要經(jīng)過計算得到。這里其實(shí)就是要計算每一個 Title 文字左下角的坐標(biāo)效扫。那么計算坐標(biāo)的代碼是這樣的:
private void drawTitle(Canvas canvas) {
for (int i = 0; i < dataCount; i++) {
int x = getPoint(i, radarMargin, 1).x;//獲取添加 radarMargin 值之后的 X 坐標(biāo)的指
int y = getPoint(i, radarMargin, 1).y;//獲取添加 radarMargin 值之后的 Y 坐標(biāo)的指
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), icos[i]);
int iconHeight = bitmap.getHeight();
int titleWidth = (int) mPaintText.measureText(titles[i]);
switch (i) {
case 1://說明一下為什么是 iconHeight / 2 倔监,主要是因?yàn)檫@樣會比較好看
y += iconHeight / 2;
break;
case 2:
x -= titleWidth;
y += iconHeight / 2;
break;
case 3:
x -= titleWidth;
break;
case 4:
x -= titleWidth / 2;
break;
}
canvas.drawText(titles[i], x, y, mPaintText);
}
}
第四步:繪制圖標(biāo)
先來看看這一步的效果:
這一步也是要進(jìn)行坐標(biāo)的計算直砂,主要的計算出放置圖標(biāo)左上角的坐標(biāo)值。這里還是相對簡單一點(diǎn)丐枉,不需要用到三角函數(shù)哆键。
private void drawIcon(Canvas canvas) {
for (int i = 0; i < dataCount; i++) {
int x = getPoint(i, radarMargin, 1).x;//獲取添加 radarMargin 值之后的 X 坐標(biāo)的指
int y = getPoint(i, radarMargin, 1).y;//獲取添加 radarMargin 值之后的 Y 坐標(biāo)的指
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), icos[i]);
int iconHeight = bitmap.getHeight();
int iconWidth = bitmap.getWidth();
int titleWidth = (int) mPaintText.measureText(titles[i]);
switch (i) {
case 0:
x += (titleWidth - iconWidth) / 2;
y -= (iconHeight + getTextHeight(titles[i]));
break;
case 1:
x += (titleWidth - iconWidth) / 2;
y -= (iconHeight / 2 + getTextHeight(titles[i]));
break;
case 2:
x -= titleWidth - (titleWidth - iconWidth) / 2;
y -= (iconHeight / 2 + getTextHeight(titles[i]));
break;
case 3:
x -= titleWidth - (titleWidth - iconWidth) / 2;
y -= (iconHeight + getTextHeight(titles[i]));
break;
case 4:
x -= (iconHeight / 2);
y -= (iconHeight + getTextHeight(titles[i]));
break;
}
canvas.drawBitmap(bitmap, x, y, mPaintText);
}
}
第五步:繪制中心點(diǎn)的分?jǐn)?shù)
這一步完成之后就可以得到最終效果了瘦锹,就是圖一的效果:
文字的坐標(biāo)是中心點(diǎn),那么計算出文字的寬度和高度就可以居中顯示文字了。
private void drawScore(Canvas canvas) {
mPaintText.setColor(getResources().getColor(R.color.colorAccent));
int score = 0;
for (int i = 0; i < data.length; i++) {//累加分?jǐn)?shù)值
score += data[i];
}
String str_score = String.valueOf(score);
Paint.FontMetrics fm = mPaintText.getFontMetrics();//用于計算文字的高度
canvas.drawText(str_score, centerX - mPaintText.measureText(str_score) / 2, (centerY + (int) Math.ceil(fm.descent - fm.ascent) / 2), mPaintText);
}
總結(jié)
一步步下來,對自定義 view 也有了進(jìn)一步的了解头岔。感覺需要更多的練習(xí)才能完全 hold 住。如果文中有什么知識點(diǎn)是錯誤的或者更好的實(shí)現(xiàn)方法类浪,請及時聯(lián)系我進(jìn)行修改,以免誤導(dǎo)別人肌似。謝謝费就。那么完整的代碼是這樣的。
/**
* Created by zone on 2017/4/9.
*/
public class PentagonView extends View {
private int dataCount = 5;//多邊形維度锈嫩,這里是五邊形
private float radian = (float) (Math.PI * 2 / dataCount);//每個維度的角度
private float radius;//一條星射線的長度受楼,即是發(fā)散的五條線白線
private int centerX;//中心坐標(biāo) Y
private int centerY;//中心坐標(biāo) X
private String[] titles = {"履約能力", "信用歷史", "人脈關(guān)系", "行為偏好", "身份特質(zhì)"};//標(biāo)題
private int[] icos = {R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher};//五個維度的圖標(biāo)
private float[] data = {170, 180, 100, 170, 150};//五個維度的數(shù)據(jù)值
private float maxValue = 190;//每個維度的最大值
private Paint mPaintText;//繪制文字的畫筆
private int radarMargin = 40;//
private int mAlpha;//白色五邊形的透明度
private Path mPentagonPath;//記錄白色五邊形的路徑
private Paint mPentagonPaint;//繪制白色五邊形的畫筆
private Path mDataPath;//記錄紅色五邊形的路徑
private Paint mDataPaint;//繪制紅色五邊形的畫筆
public PentagonView(Context context) {
super(context);
init();
}
public PentagonView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PentagonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public PentagonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
mPentagonPaint = new Paint();//初始化白色五邊形的畫筆
mPentagonPaint.setAntiAlias(true);//
mPentagonPaint.setStrokeWidth(5);//
mPentagonPaint.setColor(Color.WHITE);//
mPentagonPaint.setStyle(Paint.Style.FILL_AND_STROKE);//
mDataPaint = new Paint();//初始化紅色五邊形的畫筆
mDataPaint.setAntiAlias(true);//
mDataPaint.setStrokeWidth(10);//
mDataPaint.setColor(Color.RED);//
mDataPaint.setAlpha(150);//
mDataPaint.setStyle(Paint.Style.STROKE);//
mPaintText = new Paint();//初始化文字畫筆
mPaintText.setAntiAlias(true);//
mPaintText.setTextSize(50);//
mPaintText.setColor(Color.WHITE);//
mPaintText.setStyle(Paint.Style.FILL);//
mPentagonPath = new Path();//初始化白色五邊形路徑
mDataPath = new Path();//初始化紅色五邊形路徑
radius = 80;//星射線的初始值,也是最小的五邊形的一條星射線的長度(后期會遞增)
mAlpha = 150;//白色五邊形的透明度(后期后遞減)
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(getResources().getColor(R.color.colorAccent));
centerX = getWidth() / 2;
centerY = getHeight() / 2;
drawPentagon(canvas);//繪制白色五邊形和紅色五邊形
drawFiveLine(canvas);//繪制五條星射線
drawTitle(canvas);//繪制五個標(biāo)題
drawIcon(canvas);//繪制五個圖標(biāo)
drawScore(canvas);//繪制中間的分?jǐn)?shù)
}
private void drawPentagon(Canvas canvas) {
for (int j = 0; j < 3; j++) {//繪制三層白色五邊形
radius += 70;//每一層五邊形的星射線增加 70 的長度
mAlpha -= 30;//每一層五邊形的透明度減少 30
mPentagonPaint.setAlpha(mAlpha);
for (int i = 0; i < dataCount; i++) {//繪制一層
if (i == 0) {
mPentagonPath.moveTo(getPoint(i).x, getPoint(i).y);
} else {
mPentagonPath.lineTo(getPoint(i).x, getPoint(i).y);
}
}
mPentagonPath.close();
canvas.drawPath(mPentagonPath, mPentagonPaint);
}
for (int i = 0; i < dataCount; i++) {//繪制紅色五邊形
float percent = data[i] / maxValue;//數(shù)據(jù)值與最大值的百分比
if (i == 0) {
mDataPath.moveTo(getPoint(i, 0, percent).x, getPoint(i, 0, percent).y);//通過百分比計算出紅色頂點(diǎn)的位置
} else {
mDataPath.lineTo(getPoint(i, 0, percent).x, getPoint(i, 0, percent).y);
}
}
mDataPath.close();
canvas.drawPath(mDataPath, mDataPaint);
}
private void drawFiveLine(Canvas canvas) {
mPentagonPaint.setColor(Color.WHITE);//設(shè)置顏色為白色
mPentagonPaint.setStrokeWidth(2);//設(shè)置寬度為2
for (int i = 0; i < dataCount; i++) {
canvas.drawLine(centerX, centerY, getPoint(i).x, getPoint(i).y, mPentagonPaint);//繪制
}
}
private void drawIcon(Canvas canvas) {
for (int i = 0; i < dataCount; i++) {
int x = getPoint(i, radarMargin, 1).x;//獲取添加 radarMargin 值之后的 X 坐標(biāo)的指
int y = getPoint(i, radarMargin, 1).y;//獲取添加 radarMargin 值之后的 Y 坐標(biāo)的指
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), icos[i]);
int iconHeight = bitmap.getHeight();
int iconWidth = bitmap.getWidth();
int titleWidth = (int) mPaintText.measureText(titles[i]);
switch (i) {
case 0:
x += (titleWidth - iconWidth) / 2;
y -= (iconHeight + getTextHeight(titles[i]));
break;
case 1:
x += (titleWidth - iconWidth) / 2;
y -= (iconHeight / 2 + getTextHeight(titles[i]));
break;
case 2:
x -= titleWidth - (titleWidth - iconWidth) / 2;
y -= (iconHeight / 2 + getTextHeight(titles[i]));
break;
case 3:
x -= titleWidth - (titleWidth - iconWidth) / 2;
y -= (iconHeight + getTextHeight(titles[i]));
break;
case 4:
x -= (iconHeight / 2);
y -= (iconHeight + getTextHeight(titles[i]));
break;
}
canvas.drawBitmap(bitmap, x, y, mPaintText);
}
}
private int getTextHeight(String text) {
Paint.FontMetrics fm = mPaintText.getFontMetrics();
return (int) Math.ceil(fm.descent - fm.ascent);
}
private void drawTitle(Canvas canvas) {
for (int i = 0; i < dataCount; i++) {
int x = getPoint(i, radarMargin, 1).x;//獲取添加 radarMargin 值之后的 X 坐標(biāo)的指
int y = getPoint(i, radarMargin, 1).y;//獲取添加 radarMargin 值之后的 Y 坐標(biāo)的指
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), icos[i]);
int iconHeight = bitmap.getHeight();
int titleWidth = (int) mPaintText.measureText(titles[i]);
switch (i) {
case 1://說明一下為什么是 iconHeight / 2 呼寸,主要是因?yàn)檫@樣會比較好看
y += iconHeight / 2;
break;
case 2:
x -= titleWidth;
y += iconHeight / 2;
break;
case 3:
x -= titleWidth;
break;
case 4:
x -= titleWidth / 2;
break;
}
canvas.drawText(titles[i], x, y, mPaintText);
}
}
private void drawScore(Canvas canvas) {
mPaintText.setColor(getResources().getColor(R.color.colorAccent));
int score = 0;
for (int i = 0; i < data.length; i++) {//累加分?jǐn)?shù)值
score += data[i];
}
String str_score = String.valueOf(score);
Paint.FontMetrics fm = mPaintText.getFontMetrics();//用于計算文字的高度
canvas.drawText(str_score, centerX - mPaintText.measureText(str_score) / 2, (centerY + (int) Math.ceil(fm.descent - fm.ascent) / 2), mPaintText);
}
public Point getPoint(int position) {
return getPoint(position, 0, 1);
}
// 右上角的頂點(diǎn)為第一個點(diǎn),順時針計算猴贰,position 依次是 0对雪,1,2米绕,3瑟捣,4
// 參數(shù):position:頂點(diǎn)的位置馋艺,radarMargin:邊距,percent:星射線長度的百分比迈套,用于計算紅色五邊形的頂點(diǎn)
public Point getPoint(int position, int radarMargin, float percent) {//以五邊形的中心點(diǎn)為坐標(biāo)原點(diǎn)
int x = 0;
int y = 0;
switch (position) {
case 0://第一象限捐祠,右上角頂點(diǎn)的坐標(biāo)計算
x = (int) (centerX + (radius + radarMargin) * Math.sin(radian) * percent);
y = (int) (centerY - (radius + radarMargin) * Math.cos(radian) * percent);
break;
case 1://第四象限,右下角頂點(diǎn)的坐標(biāo)計算
x = (int) (centerX + (radius + radarMargin) * Math.sin(radian / 2) * percent);
y = (int) (centerY + (radius + radarMargin) * Math.cos(radian / 2) * percent);
break;
case 2://第三象限桑李,左下角頂點(diǎn)的坐標(biāo)計算
x = (int) (centerX - (radius + radarMargin) * Math.sin(radian / 2) * percent);
y = (int) (centerY + (radius + radarMargin) * Math.cos(radian / 2) * percent);
break;
case 3://第二象限踱蛀,左上角頂點(diǎn)的坐標(biāo)計算
x = (int) (centerX - (radius + radarMargin) * Math.sin(radian) * percent);
y = (int) (centerY - (radius + radarMargin) * Math.cos(radian) * percent);
break;
case 4:// Y 軸正方向頂點(diǎn)的計算
x = centerX;
y = (int) (centerY - (radius + radarMargin) * percent);
break;
}
return new Point(x, y);
}
}