这次自定义实现雷达图,它可以用在分析某些内容所占的比例,比较直观地突出某些数据,比如可以用在游戏玩家的各项能力的分析上,那么它的各项指标就比较明显地看出来了。效果图如下:
看完这幅图大家就清楚要实现的内容吧。下面来实现它。
一、思路
1、先画背景的正六边形。
(1)可以看到每一部分三角形都是相同的,那么我们可以先画其中一部分的三角形,剩下的就重复操作就行了。
就是上面红色的三角形部分(画的有的丑,这不是重点)。如果单独画这部分内容,相信大家都会有自己的想法了。可以看到图中同一个顶点(原点)有5个三角形,我一开始的想法是从最小那个三角形画起,然后重复的操作画剩余的三角形,这当然可以画出来,但是有个问题就是它们的线重复了,就是画完最小三角形之后画第二个三角形的时候,它的边在次经过上一个三角形的边,因此上一个三角形的边和后面三角形的边重复部分就会比较粗,这就不符合我们的需求了。因此我重新想另外一个方法,就是先画顶点为原点那两条最大三角形的边,然后把每一个三角形的最后一条边分别画上去,那样三角形的每一条边都没有重复了。
(2)画剩余的三角形,让它们组合成正六边形。原理就是让画布旋6次,把每次画的结果都保存下来就可以组合成正六边形了,具体看后面的代码。
2、画文字,我这里是逆时针画文字的,就是“个人”,“团队”这样顺序。
3、画各项能力值所组成的图形,并把能力值以点形式画出来。
二、代码实现
说了那么多,终于要上代码了。
1、自定义控件的一些属性。
在res/values/目录下新建attrs.xml文件。然后就写上自己要定义的属性。这里就定义了几个简单的属性,用户可以自己添加。
<!-- 蜘蛛网图 -->
<declare-styleable name="MyNetPic">
<attr name="lineColor" format="color"/><!-- 线的颜色 -->
<attr name="cotentColor" format="color"/><!-- 图形的颜色 -->
<attr name="side" format="dimension"/> <!-- 三角形边长 -->
<attr name="distance" format="dimension"/> <!-- 当前三角形和上一个三角形的距离 -->
<attr name="number" format="integer"/><!-- 三角形的数量 -->
</declare-styleable>
2、代码中获取自定义属性的值。
获取完之后记得recycle,具体可以看以下的代码。在构造方法里调用这个init方法即可。
// 默认的颜色值
private final int green = 0xaf93d150;
private final int blue = 0xff4aadff;
private final int white = 0xffffffff;
private final int black = 0xff000000;
// 自定义的属性值
private int lineColor;// 线的颜色
private int contentColor;// 图形的内部的颜色
private float side;// 三角形的边长
private float distance;// 当前三角形和上一个三角形的距离
private int num;// 三角形的数量
private void init(Context context, AttributeSet attrs) {
this.context = context;
// 获取自定义属性的值
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.MyNetPic);
lineColor = a.getColor(R.styleable.MyNetPic_lineColor, blue);
contentColor = a.getColor(R.styleable.MyNetPic_cotentColor, green);
side = a.getDimension(R.styleable.MyNetPic_side, 25);
distance = a.getDimension(R.styleable.MyNetPic_distance, 25);
num = a.getInteger(R.styleable.MyNetPic_number, 5);
a.recycle();
paint = new Paint();
textPaint = new Paint();
contentPaint = new Paint();
// 把dp转换为px
side = dip2px(context, side);
distance = dip2px(context, distance);
textDistance = dip2px(context, textDistance);
textSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
textSize, getResources().getDisplayMetrics());
drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG
| Paint.FILTER_BITMAP_FLAG);
texts = new String[] { "个人", "团队", "意识", "领悟", "思维", "敏捷" };
abilitys = new float[] { 150, 145, 130, 160, 120, 105 };
}
3、重写onMeasure方法。
处理为wrap_content情况,那么它的specMode是AT_MOST模式,在这种模式下它的宽/高等于spectSize,这种情况下view的spectSize是parentSize,而parentSize是父容器目前可以使用大小,就是父容器当前剩余的空间大小, 就相当于使用match_parent一样 的效果,因此我们可以设置一个默认的值。我这里设置默认的宽高都是200。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpectMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpectSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpectMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpectSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpectMode == MeasureSpec.AT_MOST
&& heightSpectMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpectMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpectSize);
} else if (heightSpectMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpectSize, mHeight);
}
}
4、在onlayout里获取控件的宽高。
@Override
protected void onLayout(boolean changed, int left, int top, int right,int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
mWidth = right - left;
mHeight = bottom - top;
}
}
5、在onDraw方法实现图形的绘制。
@Override
protected void onDraw(Canvas canvas) {
// 从canvas层面去除绘制时锯齿
canvas.setDrawFilter(drawFilter);
// 移到区域的中心
canvas.translate(mWidth / 2, mHeight / 2);
// 将y轴翻转
// canvas.scale(1f, -1f);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1f);
drawBackGroundPic(canvas);
drawMyText(canvas);
drawContent(canvas);
}
6、画背景的正六边形。
使用canvas.save();来保存上一次的图层,在新的图层里画其它部分的三角形, 最后用canvas.restore();把新的图层添加到原来的图层上。
/**
* 画作为背景的正六边形
*
* @param canvas
*/
private void drawBackGroundPic(Canvas canvas) {
paint.setAntiAlias(true);
paint.setColor(lineColor);
// 先画三角形
Path path = new Path();
float x2, x3;
int AngleCount = 6;
float xArray[] = new float[num];// 存储x坐标
float yArray[] = new float[num];// 存储y坐标
for (int j = 0; j < AngleCount; j++) {
canvas.save();
canvas.rotate(j * 60);
// 计算每个三角形第三个点的坐标
for (int i = 0; i < num; i++) {
x2 = side + i * distance;// 第二个点
xArray[i] = x3 = x2 / 2.0f;// 第三个点的横坐标,因为cos60=1/2;
// 用勾股定理计算第三个点的y坐标
yArray[i] = -(float) Math.sqrt(x2 * x2 - x3 * x3);
}
// 先画最大那个三角形的两条边
path.moveTo(0, 0);
path.lineTo(side + (num - 1) * distance, 0);
path.moveTo(0, 0);
path.lineTo(xArray[num - 1], yArray[num - 1]);
// 再画每个三角形的第三条边
for (int i = 0; i < num; i++) {
path.moveTo(xArray[i], yArray[i]);
path.lineTo(side + i * distance, 0);
}
canvas.drawPath(path, paint);
canvas.restore();
}
}
7、逆时针方向画文字。
我这里是逆时针画文字的,就是“个人”,“团队”这样顺序。要注意的是使用正余弦函数的时候要转换一下不是直接拿角度就用,如Math.cos(60.0 * Math.PI / 180)。可以用Rect textRect = new Rect();textPaint.getTextBounds(texts[0], 0,texts[0].length(), textRect);方法来获取文字的宽高。
/**
* 逆时针画文字,最右边的为第一个
*
* @param canvas
*/
private void drawMyText(Canvas canvas) {
textPaint.setColor(black);
textPaint.setTextSize(textSize);
// 文字距离原点的大小,为最大的三角形边长+文字距离三角形的大小
float d = side + (num - 1) * distance + textDistance;
// 因为图形是对称的,所以直接计算其中一个角度的坐标,之后就可以重复使用了
float dx = (float) (d * Math.cos(60.0 * Math.PI / 180));
float dy = (float) (d * Math.sin(60.0 * Math.PI / 180));
Rect textRect = new Rect();
textPaint.getTextBounds(texts[0], 0, texts[0].length(), textRect);
canvas.drawText(texts[0], d, textRect.height() / 2, textPaint);
canvas.drawText(texts[1], dx, -dy, textPaint);
canvas.drawText(texts[2], -dx - textRect.width(), -dy, textPaint);
canvas.drawText(texts[3], -d - textRect.width(), textRect.height() / 2,textPaint);
canvas.drawText(texts[4], -dx - textRect.width(),
dy + textRect.height(), textPaint);
canvas.drawText(texts[5], dx, dy + textRect.height(), textPaint);
}
8、画能力值形成的多边形图形。
要注意的地方是颜色要有一定的透明度,才能够看到底部背景正六边形,这里就设置为private final int green = 0xaf93d150;这颜色。用户需要自己设置6个能力所对应的值,然后计算每个值对应(x,y)坐标,最后用path类把它们连起来,同时画出这6个点。
/**画能力值形成的图形
* @param canvas
*/
private void drawContent(Canvas canvas) {
contentPaint.setColor(contentColor);
float d =side + (num - 1) * distance;
//用两个数组来保存6个点的坐标
float xArray[] = new float[abilitys.length];
float yArray[] = new float[abilitys.length];
int count = abilitys.length;
//计算6个能力值的x,y坐标
for (int i = 0; i < count; i++) {
float conX = (float) (Math.cos(i * 60.0 * Math.PI / 180));
float conY = (float) (Math.sin(i * 60.0 * Math.PI / 180));
// 为了防止能力值比最大的三角形的边长还要大,这里就求余
xArray[i] = abilitys[i] % d * conX;
yArray[i] = -abilitys[i] % d * conY;
}
//画图形
Path path = new Path();
path.moveTo(xArray[0], yArray[0]);
for(int i=1;i<count;i++){
path.lineTo(xArray[i], yArray[i]);
}
path.close();
//画6个顶点
canvas.drawPath(path, contentPaint);
contentPaint.setColor(black);
for(int i=0;i<count;i++){
canvas.drawCircle(xArray[i], yArray[i],dip2px(context, 3),contentPaint);
}
}
9、在布局里使用。
要使用自定义的属性,则要在根节点里添加xmlns:app=”http://schemas.android.com/apk/res-auto”, 这里的”app”是可以随便定义的,但是在控件里使用自定义属性的时候它的前缀要和这里一样。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/white" >
<com.example.test22.view.NetPicture
android:id="@+id/myNetPic"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:lineColor="@android:color/holo_blue_bright"/>
10、使用Builder封装。
到上述的步骤这控件应经可以使用了,但是为了更好的调用,还是简单的进行封装一下,对外提供一些方法。
public void show(){
postInvalidate();
}
public static class NetPicBuilder {
private static NetPicture netPicture;
private static NetPicBuilder netPicBuilder;
private NetPicBuilder(){
}
public static NetPicBuilder createBuilder(NetPicture netPic){
netPicture = netPic;
synchronized (NetPicBuilder.class) {
if(netPicBuilder==null){
netPicBuilder = new NetPicBuilder();
}
}
return netPicBuilder;
}
/**设置文本的内容
* @param s
* @return
*/
public static NetPicBuilder setTextContent(String[] s){
netPicture.setTexts(s);
return netPicBuilder;
}
/**设置能力值
* @param ab
* @return
*/
public static NetPicBuilder setAbilitys(float[] ab){
netPicture.setAbilitys(ab);
return netPicBuilder;
}
/**
* 把图形显示出来
*/
public static void show(){
if(netPicture==null){
throw new NullPointerException("NetPicBuilder is null");
}
netPicture.show();
}
}
11、一些公共的方法。
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
总结
这控件比较适合自己练习,所以就自己动手去实现一下,虽然不是什么高大上的控件,但是每一个控件的实现都能让自己有所收获的,进步一点点就是最大的收获了。