前言
最近一直在学习Flutter
,感觉还不错,但是Android
也不能拉下,回顾下前3篇的内容,让我们一起画个雷达图吧。
先看效果图
分析
需要解决的问题
- 正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 2 * 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 = 1; 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[]{10, 10}, 0));
绘制的话,需要将上一步计算的坐标,每个和中心点相连即可
for (int i = 0; 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 = 0; i < spaceCount; i++) {
int radiu = radius / spaceCount * (i + 1);
Path p = makePath(new Point(centerX, centerY - radiu), sideSize, radiu);
paint.setColor(boxlineColor);
canvas.drawPath(p, paint);
}
- 文字位置计算
文字的位置计算,需要用到上一步保存的顶点坐标
for (int i = 0; i < pointList.size(); i++) {
if (labelText != null && labelText.size() > 0) {
//绘制顶点文字
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, 0, text.length(), rect);
int x;
int y;
//偏移处理
if (point.x - centerX == 0 || Math.abs(point.x - centerX) < 5) {
x = point.x;
} else if (point.x - centerX > textSpace) {
x = point.x + textSpace;
} else {
x = point.x - textSpace;
}
if (point.y - centerY == 0 || Math.abs(point.y - centerY) < 5) {
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) / 2, 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(0)));
path.moveTo(sPoint.x, sPoint.y);
for (int i = 1; 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, 5);
textSize = array.getDimensionPixelSize(R.styleable.RadarView_ch_textSize, 14);
sideSize = array.getInt(R.styleable.RadarView_ch_sideSize, 6);
spaceCount = array.getInt(R.styleable.RadarView_ch_spaceCount, 4);
textSpace = array.getDimensionPixelSize(R.styleable.RadarView_ch_textSpace, 100);
padding = array.getDimensionPixelSize(R.styleable.RadarView_ch_padding, 200);
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 == 0) {
radius = Math.min(getScreenHeight(), getScreenWidth()) / 2 - padding;
}
pointList = new ArrayList<>();
centerX = getScreenWidth() / 2;
centerY = getScreenHeight() / 2;
}
@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[]{10, 10}, 0));
pointList.clear();
for (int i = 0; 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 = 0; i < pointList.size(); i++) {
if (labelText != null && labelText.size() > 0) {
//绘制顶点文字
drawTextTop(pointList.get(i), labelText.get(i));
}
}
for (int i = 0; i < spaceCount; i++) {
int radiu = radius / spaceCount * (i + 1);
Path p = makePath(new Point(centerX, centerY - radiu), sideSize, radiu);
paint.setColor(boxlineColor);
canvas.drawPath(p, paint);
}
//绘制值
if (labelValue == null || labelValue.size() == 0) {
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, 0, text.length(), rect);
int x;
int y;
//偏移处理
if (point.x - centerX == 0 || Math.abs(point.x - centerX) < 5) {
x = point.x;
} else if (point.x - centerX > textSpace) {
x = point.x + textSpace;
} else {
x = point.x - textSpace;
}
if (point.y - centerY == 0 || Math.abs(point.y - centerY) < 5) {
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) / 2, 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 = 1; 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(0)));
path.moveTo(sPoint.x, sPoint.y);
for (int i = 1; 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 2 * 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;
}
}