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

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

一、思路

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
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
pyecharts是一个基于Python的数据可视化库,可以用来创建各种类型的表,包括雷达雷达也被称为蜘蛛或星型,它可以用来展示多个维度的数据。 要创建自定义雷达,首先需要安装pyecharts库。可以使用以下命令进行安装: ``` pip install pyecharts ``` 接下来,可以使用以下代码创建一个简单的雷达: ```python from pyecharts import options as opts from pyecharts.charts import Radar # 定义维度和数据 dimensions = [ {"name": "维度1", "max": 100}, {"name": "维度2", "max": 100}, {"name": "维度3", "max": 100}, {"name": "维度4", "max": 100}, {"name": "维度5", "max": 100}, ] data = [[90, 80, 70, 60, 50]] # 创建雷达对象 radar = ( Radar() .set_global_opts(title_opts=opts.TitleOpts(title="自定义雷达")) .add_schema(schema=dimensions) .add("数据", data) ) # 生成HTML文件并打开 radar.render("radar.html") ``` 在上面的代码中,我们首先定义了维度和数据。维度是一个包含名称和最大值的字典列表,用于定义雷达的每个维度。数据是一个包含数值的二维列表,表示每个维度的具体数值。 然后,我们创建了一个Radar对象,并使用`set_global_opts`方法设置了雷达的标题。接着,使用`add_schema`方法添加了维度信息,使用`add`方法添加了数据。 最后,使用`render`方法将雷达生成为HTML文件,并可以在浏览器中打开查看。 这只是一个简单的示例,你可以根据自己的需求进行更多的自定义,例如设置颜色、添加标记点等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值