前言
最近一直在學習
Flutter
,感覺還不錯,但是
Android
也不能拉下,回顧下前3篇的内容,讓我們一起畫個雷達圖吧。
- 【Android自定義View】繪圖之基礎篇(一)
- 【Android自定義View】繪圖之Path篇(二)
- 【Android自定義View】繪圖之文字篇(三)
- 【Android自定義View】繪圖之實戰篇(雷達圖)(四)
- 【Android自定義View】繪圖之Canvas篇(五)
先看效果圖
分析
需要解決的問題
- 正N邊形的繪制
- 虛線的繪制
- 間隔線的繪制
- 文字位置計算
- 數值坐标計算
解決問題
正N邊形的繪制
首先,我們以螢幕中心點O (
centerX, centerY
)正上方的點A為起始點,則其坐标為
centerX, centerY - radius
, 則B點的坐标應該為
centerX - radius * Math.sin(∠AOB)
,
centerY + radius - radius * Math.cos(∠AOB)
/**
* arc為弧度,在頂點處建立直角坐标系,用r和arc确定下一個點的坐标
*/
public Point nextPoint(Point point, double arc, int radius) {
Point p = new Point();
p.x = (int) (point.x - radius * Math.sin(arc));
p.y = (int) (point.y + radius - radius * Math.cos(arc));
return p;
}
複制代碼
請注意,此處的角度在java中要轉為弧度,
sideSize
為N邊
/**
* 角度制轉弧度制
*/
private double degree2radian() {
return * Math.PI / sideSize;
}
複制代碼
是以,此時,邊框的
Path
應該為:
Path
相關内容可檢視 【Android自定義View】繪圖之Path篇(二)
/**
* 傳回邊框的path
*
* @param sPoint (centerX, centerY - radiu)
* @param count
* @param radius
* @return
*/
private Path makePath(Point sPoint, int count, int radius) {
Path path = new Path();
path.moveTo(sPoint.x, sPoint.y);
for (int i = ; i < count; i++) {
Point point = nextPoint(sPoint, -degree2radian() * i, radius);
path.lineTo(point.x, point.y);
}
path.close();
return path;
}
複制代碼
虛線
可以使用
setPathEffect
來設定
Paint painte = new Paint();
painte.setStrokeWidth(lineWidth);
painte.setColor(cutlineColor);
painte.setStyle(Paint.Style.STROKE);
painte.setPathEffect(new DashPathEffect(new float[]{, }, ));
複制代碼
繪制的話,需要将上一步計算的坐标,每個和中心點相連即可
for (int i = ; i < sideSize; i++) {
Point point = nextPoint(sPoint, -degree2radian() * i, radius);
Path path = new Path();
path.moveTo(centerX, centerY);
path.lineTo(point.x, point.y);
//繪制分割線
canvas.drawPath(path, painte);
//計算文字位置使用
pointList.add(point);
}
複制代碼
間隔線的繪制
間隔線的處理跟邊框的繪制相似,隻是傳入的半徑不同
for (int i = ; i < spaceCount; i++) {
int radiu = radius / spaceCount * (i + );
Path p = makePath(new Point(centerX, centerY - radiu), sideSize, radiu);
paint.setColor(boxlineColor);
canvas.drawPath(p, paint);
}
複制代碼
文字位置計算
文字的位置計算,需要用到上一步儲存的頂點坐标
for (int i = ; i < pointList.size(); i++) {
if (labelText != null && labelText.size() > ) {
//繪制頂點文字
drawTextTop(pointList.get(i), labelText.get(i));
}
}
複制代碼
繪制文字,這裡涉及到的在上一篇中有較長的描述,詳情可檢視 【Android自定義View】繪圖之文字篇(三)
/**
* 繪制頂點文字
*
* @param point
* @param text
*/
private void drawTextTop(Point point, String text) {
Rect rect = new Rect();
paint.getTextBounds(text, , text.length(), rect);
int x;
int y;
//偏移處理
if (point.x - centerX == || Math.abs(point.x - centerX) < ) {
x = point.x;
} else if (point.x - centerX > textSpace) {
x = point.x + textSpace;
} else {
x = point.x - textSpace;
}
if (point.y - centerY == || Math.abs(point.y - centerY) < ) {
y = point.y;
} else if (point.y - centerY > textSpace) {
y = point.y + textSpace;
} else {
y = point.y - textSpace;
}
paint.setStyle(Paint.Style.FILL);
paint.setColor(textColor);
canvas.drawText(text, x, y + (rect.bottom - rect.top) / , paint);
paint.setStyle(Paint.Style.STROKE);
}
複制代碼
數值坐标計算
數值坐标計算相對麻煩點,通過中心點正上方的坐标來推導旋轉後的坐标
Path valuePath = makePath(radius, labelValue);
paint.setColor(valueColor);
paint.setStyle(Paint.Style.FILL);
canvas.drawPath(valuePath, paint);
複制代碼
private Path makePath(int radius, List<Double> values) {
Path path = new Path();
Point sPoint = new Point(centerX, (int) (centerY - radius * values.get()));
path.moveTo(sPoint.x, sPoint.y);
for (int i = ; i < values.size(); i++) {
sPoint = new Point(centerX, (int) (centerY - radius * values.get(i)));
Point point = nextPoint(sPoint, -degree2radian() * i, (int) (radius * values.get(i)));
path.lineTo(point.x, point.y);
}
path.close();
return path;
}
複制代碼
在加上一些自定義屬性,一個雷達圖就做好了
<resources>
<declare-styleable name="RadarView">
<!--線顔色-->
<attr name="ch_boxlineColor" format="color" />
<!--文字顔色-->
<attr name="ch_textColor" format="color" />
<!--分割線顔色-->
<attr name="ch_cutlineColor" format="color" />
<!--内容顔色-->
<attr name="ch_valueColor" format="color" />
<!--線寬-->
<attr name="ch_lineWidth" format="dimension" />
<!--文字大小-->
<attr name="ch_textSize" format="dimension" />
<!--幾邊形-->
<attr name="ch_sideSize" format="integer" />
<!--輔助線-->
<attr name="ch_spaceCount" format="integer" />
<!--文字離頂點的距離-->
<attr name="ch_textSpace" format="dimension" />
<!--邊距離-->
<attr name="ch_padding" format="dimension" />
</declare-styleable>
</resources>
複制代碼
最後
完整代碼如下:
public class RadarView extends View {
private Context context;
//線寬
private int lineWidth;
//線顔色
private int boxlineColor;
//内容顔色
private int valueColor;
//文字顔色
private int textColor;
//分割線顔色
private int cutlineColor;
//文字大小
private int textSize;
//文字離頂點的距離
private int textSpace;
//幾邊形
private int sideSize;
//輔助線
private int spaceCount;
//邊距
private int padding;
//半徑
private int radius;
private Paint paint;
private Canvas canvas;
//中心x
private int centerX;
//中心y
private int centerY;
private List<Point> pointList;
private List<String> labelText;
private List<Double> labelValue;
public void setLabelValue(List<Double> labelValue) {
this.labelValue = labelValue;
postInvalidate();
}
public void setLabelText(List<String> labelText) {
this.labelText = labelText;
postInvalidate();
}
public RadarView(Context context) {
super(context);
}
public RadarView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init(attrs);
}
public RadarView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init(attrs);
}
private void init(AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RadarView);
boxlineColor = array.getColor(R.styleable.RadarView_ch_boxlineColor, Color.BLACK);
textColor = array.getColor(R.styleable.RadarView_ch_textColor, Color.BLACK);
cutlineColor = array.getColor(R.styleable.RadarView_ch_cutlineColor, Color.MAGENTA);
valueColor = array.getColor(R.styleable.RadarView_ch_valueColor, Color.MAGENTA);
lineWidth = array.getDimensionPixelSize(R.styleable.RadarView_ch_lineWidth, );
textSize = array.getDimensionPixelSize(R.styleable.RadarView_ch_textSize, );
sideSize = array.getInt(R.styleable.RadarView_ch_sideSize, );
spaceCount = array.getInt(R.styleable.RadarView_ch_spaceCount, );
textSpace = array.getDimensionPixelSize(R.styleable.RadarView_ch_textSpace, );
padding = array.getDimensionPixelSize(R.styleable.RadarView_ch_padding, );
array.recycle();
paint = new Paint();
paint.setColor(boxlineColor);
paint.setStrokeWidth(lineWidth);
paint.setStyle(Paint.Style.STROKE);
paint.setTextSize(textSize);
paint.setTextAlign(Paint.Align.CENTER);
if (radius == ) {
radius = Math.min(getScreenHeight(), getScreenWidth()) / - padding;
}
pointList = new ArrayList<>();
centerX = getScreenWidth() / ;
centerY = getScreenHeight() / ;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.canvas = canvas;
Point sPoint = new Point(centerX, centerY - radius);
Paint painte = new Paint();
painte.setStrokeWidth(lineWidth);
painte.setColor(cutlineColor);
painte.setStyle(Paint.Style.STROKE);
painte.setPathEffect(new DashPathEffect(new float[]{, }, ));
pointList.clear();
for (int i = ; i < sideSize; i++) {
Point point = nextPoint(sPoint, -degree2radian() * i, radius);
Path path = new Path();
path.moveTo(centerX, centerY);
path.lineTo(point.x, point.y);
//繪制分割線
canvas.drawPath(path, painte);
pointList.add(point);
}
for (int i = ; i < pointList.size(); i++) {
if (labelText != null && labelText.size() > ) {
//繪制頂點文字
drawTextTop(pointList.get(i), labelText.get(i));
}
}
for (int i = ; i < spaceCount; i++) {
int radiu = radius / spaceCount * (i + );
Path p = makePath(new Point(centerX, centerY - radiu), sideSize, radiu);
paint.setColor(boxlineColor);
canvas.drawPath(p, paint);
}
//繪制值
if (labelValue == null || labelValue.size() == ) {
return;
}
Path valuePath = makePath(radius, labelValue);
paint.setColor(valueColor);
paint.setStyle(Paint.Style.FILL);
canvas.drawPath(valuePath, paint);
}
/**
* 繪制頂點文字
*
* @param point
* @param text
*/
private void drawTextTop(Point point, String text) {
Rect rect = new Rect();
paint.getTextBounds(text, , text.length(), rect);
int x;
int y;
//偏移處理
if (point.x - centerX == || Math.abs(point.x - centerX) < ) {
x = point.x;
} else if (point.x - centerX > textSpace) {
x = point.x + textSpace;
} else {
x = point.x - textSpace;
}
if (point.y - centerY == || Math.abs(point.y - centerY) < ) {
y = point.y;
} else if (point.y - centerY > textSpace) {
y = point.y + textSpace;
} else {
y = point.y - textSpace;
}
paint.setStyle(Paint.Style.FILL);
paint.setColor(textColor);
canvas.drawText(text, x, y + (rect.bottom - rect.top) / , paint);
paint.setStyle(Paint.Style.STROKE);
}
/**
* 傳回邊框的path
*
* @param sPoint
* @param count
* @param radius
* @return
*/
private Path makePath(Point sPoint, int count, int radius) {
Path path = new Path();
path.moveTo(sPoint.x, sPoint.y);
for (int i = ; i < count; i++) {
Point point = nextPoint(sPoint, -degree2radian() * i, radius);
path.lineTo(point.x, point.y);
}
path.close();
return path;
}
private Path makePath(int radius, List<Double> values) {
Path path = new Path();
Point sPoint = new Point(centerX, (int) (centerY - radius * values.get()));
path.moveTo(sPoint.x, sPoint.y);
for (int i = ; i < values.size(); i++) {
sPoint = new Point(centerX, (int) (centerY - radius * values.get(i)));
Point point = nextPoint(sPoint, -degree2radian() * i, (int) (radius * values.get(i)));
path.lineTo(point.x, point.y);
Log.e("cheng", point.toString());
}
path.close();
return path;
}
/**
* 擷取螢幕寬度
*
* @return
*/
private int getScreenWidth() {
Resources resources = getResources();
DisplayMetrics dm = resources.getDisplayMetrics();
return dm.widthPixels;
}
/**
* 擷取螢幕高度
*
* @return
*/
private int getScreenHeight() {
Resources resources = getResources();
DisplayMetrics dm = resources.getDisplayMetrics();
return dm.heightPixels;
}
/**
* 角度制轉弧度制
*/
private double degree2radian() {
return * Math.PI / sideSize;
}
// arc為弧度,在頂點處建立直角坐标系,用r和arc确定下一個點的坐标
public Point nextPoint(Point point, double arc) {
Point p = new Point();
p.x = (int) (point.x - radius * Math.sin(arc));
p.y = (int) (point.y + radius - radius * Math.cos(arc));
return p;
}
/**
* arc為弧度,在頂點處建立直角坐标系,用r和arc确定下一個點的坐标
*/
public Point nextPoint(Point point, double arc, int radius) {
Point p = new Point();
p.x = (int) (point.x - radius * Math.sin(arc));
p.y = (int) (point.y + radius - radius * Math.cos(arc));
return p;
}
}
複制代碼
轉載于:https://juejin.im/post/5cef286be51d4556be5b39eb