Android 自定义多边形统计图

  最近学习了自定义View的一些知识,为了巩固,写了一个小东西,顺便分享出来

简介

  一个多边形统计图。边数,每个方向的值,每个点的文字等等都是可以设置的。


PolygonView

下面就来分析一下这个自定义View
这个view由以下几个部分组成

  • M层N边形
  • 中心到各顶点的连线
  • 填充区域
  • 文字
 @Override
    protected void onDraw(Canvas canvas) {
        if (!canDraw()) {
            return;
        }
        canvas.translate(width / 2, height / 2);
        computeMaxPoint();
        drawPolygon(canvas);
        drawLine(canvas);
        drawArea(canvas);
        drawText(canvas);
    }

我们一步一步来说明

绘制多边形

  绘制多边形主要用到的是Path这个东西。具体的思路就是先计算好每个点的位置,同Path的lineTo方法连接起来,然后绘制。
  我的做法是先算出最大的半径(再之后还会用到,建议单独存起来),然后根据所在层数来计算每一层的半径,利用cos函数各sin函数计算出每一层各顶点的位置。

  • 计算最大半径并且保存顶点
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        width = w;
        height = h;
        maxRadius = (float) ((width / 2) * 0.8);
        postInvalidate();
    }
     /*
        计算最大半径,之后的位置都是基于最大半径的比例
     */
    public void computeMaxPoint() {
        maxPointXList = new ArrayList<>();
        maxPointYList = new ArrayList<>();
        for (int i = 0; i < eageCount; i++) {

            float currentAngle = i * angle - 90;

            float currentX = (float) (maxRadius * Math.cos((currentAngle / 180) * Math.PI));
            float currentY = (float) (maxRadius * Math.sin((currentAngle / 180) * Math.PI));
            maxPointXList.add(currentX);
            maxPointYList.add(currentY);
        }
    }

  注意,cos和sin都是按照弧度制计算的,要换算。
  这里解释一下为currentAngle什么要减去90度
  按照android的坐标系,如果不减去90度直接乘上cos的话,第一个顶点会默认在中心的右侧,而一般的认知是第一个点在正上方,所以减去90度

  • 按照比例和层数边数绘制多边形
     /*
        绘制多边形和每一层
     */
    private void drawPolygon(Canvas canvas) {
        Path path = new Path();
        for (int i = 0; i < loopCount; i++) {
            path.reset();
            //依据最大半径和角度来判断每一层点的位置
            float rate = computeRate(i + 1, loopCount);
            for (int j = 0; j < eageCount; j++) {
                float currentX = maxPointXList.get(j) * rate;
                float currentY = maxPointYList.get(j) * rate;
                if (j == 0) {
                    path.moveTo(currentX, currentY);
                } else {
                    path.lineTo(currentX, currentY);
                }
            }
            path.close();
            canvas.drawPath(path, eagePaint);
        }
    }

  代码还是很容易的吧,要是看不懂的话自己动手算算就知道了,很容易计算各个点的位置。

绘制连线

  由于之前保存了顶点的坐标,这个就很容易了

     /*
        画出从中心向各顶点的连线
     */
    private void drawLine(Canvas canvas) {
        Path path = new Path();
        for (int i = 0; i < eageCount; i++) {
            path.reset();
            path.lineTo(maxPointXList.get(i), maxPointYList.get(i));
            canvas.drawPath(path, eagePaint);
        }
    }

绘制覆盖区域

  这个原理其实和绘制多边形是一样的,就是对顶点坐标乘的比例发生了变化。每个方向的数值是由用户传递进来的。

     /*
        绘制个方向值覆盖的区域
     */
    private void drawArea(Canvas canvas) {
        Path path = new Path();
        //原理就是用path根据各方向值创建一个封闭的区域,然后填充
        for (int i = 0; i < eageCount; i++) {
            float rate = pointValue.get(i);
            float currentX = maxPointXList.get(i) * rate;
            float currentY = maxPointYList.get(i) * rate;
            if (i == 0) {
                path.moveTo(currentX, currentY);
            } else {
                path.lineTo(currentX, currentY);
            }
        }
        path.close();
        canvas.drawPath(path, areaPaint);
    }

绘制文字

  说实话,前面的没有什么难点,但是唯独绘制文字有许多麻烦事。主要是文字是默认自左向右的,最上面和最先面的文字倒是没啥,左侧和右侧的文字就会出现问题了,文字会绘制到多边形上,看起来特别难受。这里我的解决办法就是前面图中看到的,让字跟着多边形的顶点位置一起旋转。

     /*
        绘制文字
     */
    private void drawText(Canvas canvas) {
        if (pointName == null) {
            return;
        }
        //绘制文字的难点在于无法最好的适配屏幕的位置,会发生难以控制的偏倚
        for (int i = 0; i < pointName.size(); i++) {
            //解决办法就是让文字在不同的角度也发生旋转,并且在x轴上减去一定的数值来保证正确的位置
            float currentAngle = i * angle;
            //180度需要也别的处理,让它正着显示,不然就是倒着的
            if (currentAngle == 180) {
                float currentX = maxPointXList.get(i) * 1.1f;
                float currentY = maxPointYList.get(i) * 1.1f;
                canvas.drawText(pointName.get(i), currentX - (textPaint.getTextSize() / 4)
                        * (pointName.get(i).length()), currentY, textPaint);
            } else {
                canvas.save();
                float currentX = maxPointXList.get(0) * 1.1f;
                float currentY = maxPointYList.get(0) * 1.1f;
                //旋转画布,达到旋转文字的效果
                canvas.rotate(currentAngle);
                canvas.drawText(pointName.get(i), currentX - (textPaint.getTextSize() / 4)
                        * (pointName.get(i).length()), currentY, textPaint);
                canvas.restore();
            }
        }
    }

到这里,整个组件就绘制完成了

额外的属性

如果单纯只是想画出这个组件来,其实没啥难度。我们可以在加一些别的东西让他更加实用。

  • 动画效果
      利用属性动画的知识,我们可以做到让中间的填充区域慢慢的扩散出来。原理也简单,就是把0到1用属性计算展开,当做一个演化的比例,让各个方向的值乘上这个数值,绘制一个比原先覆盖区域小的区域就可以了。
     /*
        用属性动画绘制组件
     */
    public void draw() {
        if (canDraw()) {
            final Float[] trueValues = pointValue.toArray(new Float[pointValue.size()]);
            ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
            valueAnimator.setDuration(1000);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float rate = animation.getAnimatedFraction();
                    for (int i = 0; i < pointValue.size(); i++) {
                        pointValue.set(i, trueValues[i] * rate);
                    }
                    invalidate();
                }
            });
            valueAnimator.start();
        }
    }
  • 定义xml属性
      我们正常使用系统组件的时候都会写一大堆的xml来控制我们组件的属性,自定义View也可以尝试这些
      首先在value下创建atts文件


    atts

      然后指定你想要的属性名称和类型


    image.png

  再然后就是让atts和我们的view联系上。这个也简单,仔细观察View的构造方法中的参数,有这么一个玩意 AttributeSet attrs

public PolygonView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }
    public PolygonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

  它就是联系xml和view的纽带

xmlns:app="http://schemas.android.com/apk/res-auto"

<com.totoro.xkf.polygonview.PolygonView
        android:id="@+id/pv_polygon_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:areaColor="@android:color/holo_blue_light"
        app:eageColor="@android:color/black"
        app:eageCount="6"
        app:loopCount="4"
        app:textColor="@android:color/black" />
public void init(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Polygon);
        initPaint();
        setTextColor(typedArray.getColor(R.styleable.Polygon_textColor, Color.BLACK));
        setLoopCount(typedArray.getInteger(R.styleable.Polygon_loopCount, 0));
        setEageCount(typedArray.getInteger(R.styleable.Polygon_eageCount, 0));
        setAreaColor(typedArray.getColor(R.styleable.Polygon_areaColor, Color.BLUE));
        setEageColor(typedArray.getColor(R.styleable.Polygon_eageColor, Color.GRAY));
        typedArray.recycle();
    }
xmlns:app="http://schemas.android.com/apk/res-auto"

这个东西不能忘了

快速使用

  感谢你看到这里,如果你想使用这个组件但是不想自己写的话欢迎访问

  • 项目Github
    里面有讲如何添加依赖,导入组件
    如果能帮到你的话,不胜荣幸!!!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容