Android 自定义雷达图(蜘蛛网图)

来源:互联网 发布:sql怎么去掉重复值 编辑:程序博客网 时间:2024/04/28 03:09

这次自定义实现雷达图,它可以用在分析某些内容所占的比例,比较直观地突出某些数据,比如可以用在游戏玩家的各项能力的分析上,那么它的各项指标就比较明显地看出来了。效果图如下:
这里写图片描述
看完这幅图大家就清楚要实现的内容吧。下面来实现它。

一、思路

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);    }

总结

这控件比较适合自己练习,所以就自己动手去实现一下,虽然不是什么高大上的控件,但是每一个控件的实现都能让自己有所收获的,进步一点点就是最大的收获了。

源码下载

0 0