用Kotlin自定义一个"蜘蛛网图"

前言

趁着这几天比较清闲就了解了一下Kotlin,Google在2017年就已经推出Kotlin作为Android开发官方语言,它相比于java最大的优势就是简洁。其实在最初我是抗拒去学习Kotlin的,可能是忠于对java的一种情怀吧,但在初步了解了Kotlin之后我立即决定去学习这门语言,因为Kotlin也是基于JVM的一种语言,只是对java比较啰嗦的部位进行了一些改良。既然Kotlin的优势这么明显那为什么在国内的推广如此缓慢呢?我总结一下大概有两点,第一点:java用了这么多年突然又让我去学一门新语言,那之前的java不就是白学了嘛。第二点:Kotlin毕竟是一种语言,学习成本应该会很大。我负责任的告诉大家完全没必要有这种困惑,首先Kotlin是基于JVM的,目前很多特性只能用java代码实现的,二者是不冲突的,想成为一名优秀的Android工程师还是要在Java上费功夫的,我更愿意把Kotlin描述成一种工具。其次如果有Java基础学习Kotlin是非常快的,专心致志一个星期足矣。本篇文章我会基于Kotlin实现一个自定义网状图,也算是为国内Kotlin推广做一份微小贡献吧。

1. 自定义属性

首先我来晒出效果图供大家参考,如下图:


10073662-4d2efc2070b970ba.PNG
net.PNG

我们需要自定义的属性有:文字大小、文字颜色、蜘蛛网颜色、蜘蛛网边框宽度、属性区域背景颜色。

XML中定义

首先创建一个attrs文件,然后再在里面自定义这几个属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="net">
        <attr name="netColor" format="color" />
        <attr name="pathColor" format="color" />
        <attr name="textColor" format="color" />
        <attr name="netWidth" format="dimension" />
        <attr name="textSize" format="dimension" />
    </declare-styleable>
</resources>
View中获取
 var type = context.theme.obtainStyledAttributes(attrs,R.styleable.net,defStyleAttr,0)
        mNetColor = type.getColor(R.styleable.net_netColor,Color.GREEN)
        mPathColor = type.getColor(R.styleable.net_pathColor,Color.BLACK)
        mTextColor = type.getColor(R.styleable.net_textColor,Color.BLACK)
        mNetWidth = type.getDimension(R.styleable.net_netWidth,6f)
        mTextSize = type.getDimension(R.styleable.net_textSize,30f)
        type.recycle()

获取到属性设置到对应的画笔中即可

2. 宽高测量

因为对宽高没有特殊需求,所以直接通过View的resolveSize()方法进行测量即可,代码如下:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        mWidth = resolveSize(600,widthMeasureSpec)
        mHeight = resolveSize(400,heightMeasureSpec)
        setMeasuredDimension(mWidth,mHeight)
        radius = mWidth/3
        ...
        ...
    }

测量完宽高后设置六边形半径radius (顶点到原点的距离)

3. 图像绘制

绘图大概有三部分组成:蜘蛛网底图、属性区域、文字描述,本我们首先来看底图的绘制

3.1 底图绘制

我们看到底图是由五个六边形组成,首先我来跟大家捋一下绘制六边形的思路

  • 通过三角函数计算出6个点坐标
  • 通过path将6个点相连
  • 通过canvas绘制path
计算坐标点

因为是完全对称六边形,所以每个顶点与原点相连后形成的六条边与临边的夹角都是360/60=60度,所以确定了第一个点坐标其余五个点都可以通过三角函数计算得出,来看代码实现:

   private var mArrX = FloatArray(6)//记录外圈6个点x轴坐标
   private var mArrY = FloatArray(6)//记录外圈6个点y轴坐标
  ..
  ..
  //获取到6个点
  for (i in 0..5){
      mArrX[i] = (radius*Math.cos((Math.PI/3)*i).toFloat())
      mArrY[i] = (radius*Math.sin((Math.PI/3)*i)).toFloat()
  }

其中radius原点到一个顶点的距离,计算出坐标值后分别存入对应的数组中

绘制path

将六个顶点通过path进行连接然后绘制,实例中存在5个大小不同的六边形,所以要循环绘制五个逐渐缩小的六边形,代码如下

//绘制底图
    private fun drawBack(canvas: Canvas?){
        var path = Path()
        //循环绘制5个
        for (i in 1..5) {
            //绘制六边形
            for (j in 0..5) {
                if (j == 0) {
                    path.moveTo(mArrX[j]*i*0.2.toFloat(), mArrY[j]*i*0.2.toFloat())
                } else {
                    path.lineTo(mArrX[j]*i*0.2.toFloat(), mArrY[j]*i*0.2.toFloat())
                }
            }
            //闭合path
            path.close()
        }
        //当canvas不为null的时候执行let中代码
        canvas?.let { it.drawPath(path,mPaint) }
        //绘制直线
        for(i in 0..5){
            canvas?.let {it.drawLine(0f,0f,mArrX[i],mArrY[i],mPaint) }
        }
    }

绘制完5个六边形后再将每个顶点与原点相连接,效果如下图:


10073662-209a6236e80998aa.PNG
back.PNG

至此整个蜘蛛网的背景图就绘制完毕

3.2 绘制文字

我们要在六个顶点旁边绘制描述文字,在View中文字的绘制是在坐标的右上方进行的,所以绘制文字的坐标要在顶点坐标的延长处,然后将文字稍作平移即可,来看代码

//绘制文字
    private fun drawText(canvas: Canvas?){
        for (i in 0..5){
            //mList为bean类集合
            var content = mList[i].key
            val textBound = Rect()
            mPaint.getTextBounds(content, 0, content.length, textBound)
            canvas?.let{it.drawCircle(mArrX[i],mArrY[i],10f,mPathPaint)}
            //将文字中心平移至坐标处
            canvas?.let{ it.drawText(content,mArrX[i]*1.2f- textBound.width()-14
                    ,mArrY[i]*1.2f+ textBound.height() / 2+7,mTextPaint) }
        }
    }

效果如下图


10073662-82fa1ddfa3beca2e.PNG
point.PNG

文字中心的绿点即绘制文字的坐标,将顶点坐标*1.2即可。

3.3 属性区域绘制

属性区域和六边形绘制基本相同,首先根据提供的属性值计算出坐标位置,然后通过path将六个点进行连接,最后将画笔设置为Paint.Style.FILL即可填充整个区域,下面来看代码:

 //绘制属区域
    private fun drawValue(canvas: Canvas?){
        var path = Path()
        for (j in 0..5) {
            var value = mList[j].value
            if (j == 0) {
                path.moveTo(mArrX[j]*value*percentage, mArrY[j]*value*percentage)
            } else {
                path.lineTo(mArrX[j]*value*percentage, mArrY[j]*value*percentage)
            }
        }
        path.close()
        canvas?.let{it.drawPath(path,mPathPaint)}
    }

到这一步就可以绘制出文章开头亮出那个效果图,基本可以使用,但整个过程都是静态的,看着着实让人难受,所以我决定给它加个动画,往下看。。。

4. 施加动画

需求:将属性区域由小到大进行展示

我们可以通过自定义属性动画来实现这一需求,定义一个属性percentage并为其提供一个setter方法,用来记录属性动画进行进度,一定要在setter方法中执行postInvalidate()进行视图更新,否则动画无法进行。

 private var percentage:Float = 0f//用于属性动画
 fun setPercentage(percentage:Float){
        this.percentage = percentage
        postInvalidate()
   }

然后定义一个属性动画并与percentage关联,将属性值设置为0-1用时1秒

fun start(list:ArrayList<Bean>){
        this.mList = list
        var animator = ObjectAnimator.ofFloat(this,"percentage",0f,1f)
        animator.setDuration(1000)
                .start()
 }
Activity中代码
 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var list = ArrayList<Bean>()
        list.add(Bean("投篮",0.6f))
        list.add(Bean("突破",0.9f))
        list.add(Bean("篮板",0.8f))
        list.add(Bean("助攻",0.9f))
        list.add(Bean("抢断",0.8f))
        list.add(Bean("盖帽",0.6f))
        netView.start(list)
    }

不用findViewById了,拿着xml中id直接用,不要太爽......来看一下最终效果图

10073662-f2ef4ae1feac6f27.gif
anim.gif

嘿,你还别说,还挺像那回事~~~ Demo已托管至 Github,需要的朋友可以去下载。。

总结

这是我发布的第一篇关于Kotlin的文章,所以就选了一个比较简单的自定义View作为案例,由于逻辑过于简单导致Kotlin中Lambda表达式以及一些高阶函数都用到,所以Kotlin 的简洁体现的也不是很充分。最后我想声明一点,今天是我学习Kotlin的第四天,没错就是第四天我已经可以常规使用Kotlin了,所以Kotlin并不难,希望大家有时间一定要好好的学一遍,它能很大程度的提高你的开发效率。

猜你喜欢

转载自blog.csdn.net/weixin_33995481/article/details/86858028