本系列自定义View悉数采用kt

体系: mac

android studio: 4.1.3

kotlin version: 1.5.0

gradle: gradle-6.5-bin.zip

本篇效果:

android 自定义view: 蛛网/雷达图 (三)

蛛网图其实便是由多个多边形来组成蛛网的,那么先来画1个多边形来练练手

画多边形

首要咱们先来画一个五边形,

想要制作一个五边形,那么便是求出5个点即可

例如这样:

android 自定义view: 蛛网/雷达图 (三)

首要咱们需求定义圆的半径,也是五边形的“半径”

只需求算出每一个角的视点,那么就可以通过三角函数算出每一个点的坐标

  • 0的视点为360 / 5 * 0
  • 1的视点为360 / 5 * 1
  • 2的视点为360 / 5 * 2
  • 3的视点为360 / 5 * 3
  • 4的视点为360 / 5 * 4

来看看代码:

classE3PolygonChartBlogView@JvmOverloadsconstructor(
 context:Context,attrs:AttributeSet?=null,defStyleAttr:Int=0
) :View(context,attrs,defStyleAttr) {
​
 companionobject{
   // 半径
   valSMALL_RADIUS=100.dp
​
   // 几边形
   constvalCOUNT=5
 }
​
 privatevalpaint=Paint(Paint.ANTI_ALIAS_FLAG)
​
 // 中心方位
 privatevalcenterLocationbylazy{
   PointF(width/2f,height/2f)
 }
​
 overridefunonDraw(canvas:Canvas) {
​
   valcx=centerLocation.x
   valcy=centerLocation.y
   // 辅佐圆
   canvas.drawCircle(cx,cy,SMALL_RADIUS,paint)
​
​
   // 每一个的间隔
   valeachAngle=360/COUNT
   (0untilCOUNT).forEach{
​
     valangle=it*eachAngle.toDouble()
​
     valx=
       (SMALL_RADIUS*cos(Math.toRadians(angle))+centerLocation.x).toFloat()
     valy=
       (SMALL_RADIUS*sin(Math.toRadians(angle))+centerLocation.y).toFloat()
​
     paint.color=colorRandom
     // 制作每一个小圆
     canvas.drawCircle(x,y,10.dp,paint)
   }
 }
}

android 自定义view: 蛛网/雷达图 (三)

那么五边形其实便是吧5个点衔接起来即可

privatevalpaint=Paint(Paint.ANTI_ALIAS_FLAG)
​
 privatevalpath=Path()
 overridefunonDraw(canvas:Canvas) {
​
   // 每一个的间隔
   valeachAngle=360/COUNT
   (0untilCOUNT).forEach{
     valangle=it*eachAngle.toDouble()
     valx=
       (SMALL_RADIUS*cos(Math.toRadians(angle))+centerLocation.x).toFloat()
     valy=
       (SMALL_RADIUS*sin(Math.toRadians(angle))+centerLocation.y).toFloat()
// 衔接每一个点
     if(it==0) {
       path.moveTo(x,y)
     }else{
       path.lineTo(x,y)
     }
   }
   path.close()// 闭合
​
   paint.strokeWidth=2.dp
   paint.style=Paint.Style.STROKE
   canvas.drawPath(path,paint)// 制作
   path.reset()
 }

android 自定义view: 蛛网/雷达图 (三)

制作多条五边形

假如需求制作成这样子:

android 自定义view: 蛛网/雷达图 (三)

方才咱们制作的是最中心绿色的五边形,

那么这儿就需求定义一个变量,来标识每一个五边形之间的间隔

例如蓝色五边形和绿色五边形的间隔为20.dp

那么蓝色五边形 五个点的半径 = 绿色五边形的半径 + 20.dp

以此类推

classE3PolygonChartBlogView@JvmOverloadsconstructor(
 context:Context,attrs:AttributeSet?=null,defStyleAttr:Int=0
) :View(context,attrs,defStyleAttr) {
​
 companionobject{
   // 半径
   valSMALL_RADIUS=100.dp
​
   // 几边形
   constvalCOUNT=5// 有几条边
   constvalNUMBER=3// 每一条边的间隔
   valINTERVAL=20.dp
 }
​
 privatevalpaint=Paint(Paint.ANTI_ALIAS_FLAG)
​
 // 中点
 privatevalcenterLocationbylazy{
   PointF(width/2f,height/2f)
 }
​
 privatevalpath=Path()
 overridefunonDraw(canvas:Canvas) {
   // 每一个的间隔
   valeachAngle=360/COUNT
  
   // 循环有几条边
   (0untilNUMBER).forEachIndexed{index,element->
​
     // 循环每一条边有几个点
     (0untilCOUNT).forEach{count->
       // 半径 = 当时是第几条边 * 间隔 + 最中心的间隔
       valradius=element*INTERVAL+SMALL_RADIUS
                 
       valangle=count*eachAngle.toDouble()
​
       valx=
         (radius*cos(Math.toRadians(angle))+centerLocation.x).toFloat()
       valy=
         (radius*sin(Math.toRadians(angle))+centerLocation.y).toFloat()
       if(count==0) {
         path.moveTo(x,y)
       }else{
         path.lineTo(x,y)
       }
     }
     path.close()// 闭合
     paint.strokeWidth=2.dp
     paint.style=Paint.Style.STROKE
     canvas.drawPath(path,paint)
     paint.reset()
   }
 }
}

android 自定义view: 蛛网/雷达图 (三)

衔接最外层和最内层

衔接最内层和最外层也比较简单, 只需求循环有几条边的时候判别是否是最外层,

然后将最外层的点和最内层的点相衔接即可

假如需求和中心点相衔接,那么stop点为 centerLocation即可

overridefunonDraw(canvas:Canvas) {
   // 每一个的间隔
   valeachAngle=360/COUNT
   // 循环有几条边
   (0untilNUMBER).forEachIndexed{index,element->
​
     // 循环每一条边有几个点
     (0untilCOUNT).forEach{count->
       // 半径 = 当时是第几条边 * 间隔 + 最中心的间隔
       valradius=element*INTERVAL+SMALL_RADIUS
       valangle=count*eachAngle.toDouble()
​
       valx=
         (radius*cos(Math.toRadians(angle))+centerLocation.x).toFloat()
       valy=
         (radius*sin(Math.toRadians(angle))+centerLocation.y).toFloat()
        .....
​
       // 当时是最后一层
       if(index==NUMBER-1) {
         // 最内层x,y 坐标
         valstopX=
           (SMALL_RADIUS*cos(Math.toRadians(angle))+centerLocation.x).toFloat()
         valstopY=
           (SMALL_RADIUS*sin(Math.toRadians(angle))+centerLocation.y).toFloat()
         canvas.drawLine(x,y,stopX,stopY,paint)
         // 衔接中心点
         // canvas.drawLine(x, y, centerLocation.x, centerLocation.y, paint)
       }
     }
     path.close()// 闭合canvas.drawPath(path,paint)
     paint.reset()
   }
 }

android 自定义view: 蛛网/雷达图 (三)

那么现在需求一个

  • 10边形
  • 每一条边有7个点
  • 最中心的半径为 20.dp
  • 每一个边的间隔 = 20.dp

只需求改这4个变量即可:

companionobject{
 // 半径
 valSMALL_RADIUS=20.dp
​
 // 几边形
 constvalCOUNT=10// 有几条边
 constvalNUMBER=7// 每一条边的间隔
 valINTERVAL=20.dp
}

android 自定义view: 蛛网/雷达图 (三)

制作文字

仍是和上面的套路相同,先来思考文字需求制作到什么地方?

android 自定义view: 蛛网/雷达图 (三)

咱们的多边形只到赤色的,那么为了坚持和最外层有一点间隔,所以咱们需求将文字制作到虚线处,

仍是当制作最外层的时候开端制作 文字

 @SuppressLint("DrawAllocation")
 overridefunonDraw(canvas:Canvas) {
​
   // 每一个的间隔
   valeachAngle=360/COUNT
   // 循环有几条边
   (0untilNUMBER).forEachIndexed{index,element->
​
     // 循环每一条边有几个点
     (0untilCOUNT).forEach{count->
       // 半径 = 当时是第几条边 * 间隔 + 最中心的间隔
       valradius=element*INTERVAL+SMALL_RADIUS
       valangle=count*eachAngle.toDouble()
​
       valx=
         (radius*cos(Math.toRadians(angle))+centerLocation.x).toFloat()
       valy=
         (radius*sin(Math.toRadians(angle))+centerLocation.y).toFloat()
       ...
​
       // 制作最外层和内层衔接线
      ...
​
​
       // 设置文字
       if(index==NUMBER-1) {
         valtext="文字${count}"
​
         valrect=Rect()
​
         // 核算文字宽高 核算完成之后会把值赋值给rect
         paint.getTextBounds(text,0,text.length,rect)
         valtextWidth=rect.width()
         valtextHeight=rect.height()
​
         valtempRadius=radius+textHeight
         valtextX=
           (tempRadius*cos(Math.toRadians(angle))+centerLocation.x).toFloat()-textWidth/2f
         valtextY=
           (tempRadius*sin(Math.toRadians(angle))+centerLocation.y).toFloat()
​
         paint.textSize=16.dp
         paint.style=Paint.Style.FILL
         paint.color=E3PolygonChartView.TEXT_COLOR
         // 制作最外层文字
         canvas.drawText(text,textX,textY,paint)
​
       }
     }
    ...
   }
 }

android 自定义view: 蛛网/雷达图 (三)

到目前为止,蛛网的雏形就差不多了,接下来制作具体的数据

制作数据

制作数据之前先来看看现在点的坐标

android 自定义view: 蛛网/雷达图 (三)

假定咱们当时需求设置的数据为 3,2,3,1,1

那么咱们只需求从0坐标开端,算出每一个对应的五边形即可

那么最终结果应该为:

android 自定义view: 蛛网/雷达图 (三)

overridefunonDraw(canvas:Canvas) {

 // 制作网格
    ...

 // 制作数据
   drawArea(canvas)
}
​
vardata=listOf(3f,2f,3f,1f,1f)
privatefundrawArea(canvas:Canvas) {
   data.forEachIndexed{index,value->
     vallocation=getLocation(index,value)
​
     if(index==0) {
       path.moveTo(location.x,location.y)
     }else{
       path.lineTo(location.x,location.y)
     }
   }
   path.close()
​
​
   paint.style=Paint.Style.STROKE
   paint.color=Color.RED
   canvas.drawPath(path,paint)// 制作边
​
   paint.style=Paint.Style.FILL
   paint.alpha=(255*0.1).toInt()
   canvas.drawPath(path,paint)// 制作内边
   path.reset()
 }
/*
* 作者:史大拿
* 创建时刻: 9/27/22 2:54 PM
* @number 第几个点
* @count 第几条边
*/
privatefungetLocation(number:Int,count:Float):PointF=let{
 // 视点
 valangle=360/COUNT*number

 // 半径
 valradius=(count-1)*INTERVAL+SMALL_RADIUS

 valx=
   (radius*cos(Math.toRadians(angle.toDouble()))+centerLocation.x).toFloat()
 valy=
   (radius*sin(Math.toRadians(angle.toDouble()))+centerLocation.y).toFloat()
​
 returnPointF(x,y)
}

android 自定义view: 蛛网/雷达图 (三)

手势滑动

雷达图的手势滑动和其他的不太相同, 因为他需求核算的是视点

场景1(右下角)

android 自定义view: 蛛网/雷达图 (三)

假定当时滑动的方位在右下角,那么他的视点就为 赤色的视点

  • 赤色的视点 = atan(dy / dx)

场景2 (左下角)

android 自定义view: 蛛网/雷达图 (三)

假定当时滑动的方位在左下角,那么他的视点就为 黑色的视点 + 绿色的视点

  • 绿色视点 = 90度
  • 赤色的视点 = atan(dy / dx)
  • 黑色视点 = 90 – 赤色视点

场景3(左上角)

android 自定义view: 蛛网/雷达图 (三)

假定当时滑动的方位在左上角,那么他的视点就为 赤色的视点 + 绿色视点

dx = centerLocation.x – event.x

dy = centerLocation.x – event.y

  • 赤色的视点 = atan(dy / dx)
  • 绿色的视点 = 180度

场景4(右上角)

android 自定义view: 蛛网/雷达图 (三)

假定当时滑动的方位在右上角,那么他的视点就为 绿色视点 + 黑色视点

  • 黑色视点 = 90度 – 赤色视点
  • 赤色视点 = atan(dy / dx)
  • 绿色视点 = 270度

判别是否是左上角 或者右上角,只需求判别两个点的x,y值即可

来看看核算视点代码:

@paramstartP:开端点
@paramendP:完毕点
​
funPointF.angle(endP:PointF):Float{
 valstartP=this// 原始方位
 valangle=if(startP.x>=endP.x&&startP.y>=endP.y) {
   Log.e("szjLocation","end在start右下角")
   0
 }elseif(startP.x>=endP.x&&startP.y<=endP.y) {
   Log.e("szjLocation","end在start右上角")
   270
 }elseif(startP.x<=endP.x&&startP.y<=endP.y) {
   Log.e("szjLocation","end在start左上角")
   180
 }elseif(startP.x<=endP.x&&startP.y>=endP.y) {
   Log.e("szjLocation","end在start左下角")
   90
 }else{
   0
 }
 // 核算间隔
 valdx=startP.x-endP.x
 valdy=startP.y-endP.y
 // 弧度
 valradian=abs(atan(dy/dx))
​
 // 弧度转视点
 vara=Math.toDegrees(radian.toDouble()).toFloat()
​
 if(startP.x<=endP.x&&startP.y>=endP.y) {
   // 左下角
   a=90-a
 }elseif(startP.x>=endP.x&&startP.y<=endP.y) {
   // 右上角
   a=90-a
 }
 returna+angle
}
varoffsetAngle=0f// 偏移视点
privatevardownAngle=0f// 按下视点
privatevaroriginAngle=0f// 原始视点@SuppressLint("ClickableViewAccessibility")
overridefunonTouchEvent(event:MotionEvent):Boolean{
 when(event.action) {
   MotionEvent.ACTION_DOWN->{
     downAngle=centerLocation.angle(PointF(event.x,event.y))
     originAngle=offsetAngle
   }
   MotionEvent.ACTION_MOVE->{
     parent.requestDisallowInterceptTouchEvent(true)
​
     // 当时偏移视点 = 现在视点 - 按下视点 + 原始视点
     offsetAngle=
       centerLocation.angle(PointF(event.x,event.y))-downAngle+originAngle
​
     Log.e("szjOffset","$offsetAngle")
   }
   MotionEvent.ACTION_UP->{
   }
 }
 invalidate()
​
 returntrue
}

假如这儿视点不知道为啥 = 现在视点 – 按下视点 + 原始视点 可以看第一篇道理都是相同的,就不过多解说了!

最后核算出来的视点直接赋值给onDraw即可

overridefunonDraw(canvas:Canvas) {
   // 每一个的间隔
   valeachAngle=360/COUNT
   // 循环有几条边
   (0untilNUMBER).forEachIndexed{index,element->
​
     // 循环每一条边有几个点
     (0untilCOUNT).forEach{count->
       valangle=count*eachAngle.toDouble()+offsetAngle// TODO 设置视点
​
       valx=
         (radius*cos(Math.toRadians(angle))+centerLocation.x).toFloat()
       valy=
         (radius*sin(Math.toRadians(angle))+centerLocation.y).toFloat()
       if(count==0) {
         path.moveTo(x,y)
       }else{
         path.lineTo(x,y)
       }
​
       // 衔接最外层和最内层
 ....
​
​
       // 设置文字
       ....
       
     }
     ....
​
     canvas.drawPath(path,paint)
     path.reset()
   }
​
   // 制作数据
   drawArea(canvas)
 }

android 自定义view: 蛛网/雷达图 (三)

设置fling事情

我坦白了,fling事情我是偷的MPAndroidChart的源码,

这个fling事情和平常的不太相同,有大坑.. 想了1天没想出来,只能看看长辈思路…

假如需求可以自行下载看细节

完好代码

原创不易,您的点赞便是对我最大的帮助!

  • android 自定义View:九宫格解锁
  • android自定义View: 制作图表(一)
  • android 自定义view: 矩形图表(二)
  • android 自定义View:仿QQ拖拽效果
  • android 图解 PhotoView,从‘百草园’到‘三味书屋’!