服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

Android自定义View之蜘蛛网雷达效果

日期: 来源:郭霖收集编辑:史大拿


/   今日科技快讯   /

近日,在将聊天机器人Bard作为主要用于搜索的AI服务推出后,谷歌似乎正在偏离初衷,没有提供任何具体的用例。在最近举行的全体员工会议上,该公司高管辩称,Bard不仅仅用于搜索,他们正探索构建一种协作式AI服务。

/   作者简介   /

本篇文章转自史大拿的博客,文章主要分享了自定义View中蛛网的效果,相信会对大家有所帮助!

原文地址:
https://juejin.cn/post/7147966036803846152

/   前言   /

系统:mac
android studio:4.1.3
kotlin version:1.5.0
gradle:gradle-6.5-bin.zip

本篇效果:


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

/   画多边形   /

首先我们先来画一个五边形,想要绘制一个五边形,那么就是求出5个点即可。例如这样:


首先我们需要定义圆的半径,也是五边形的“半径”。只需要算出每一个角的角度,那么就可以通过三角函数算出每一个点的坐标。

  • 0的角度为360 / 5 * 0
  • 1的角度为360 / 5 * 1
  • 2的角度为360 / 5 * 2
  • 3的角度为360 / 5 * 3
  • 4的角度为360 / 5 * 4

来看看代码:

 class E3PolygonChartBlogView @JvmOverloads constructor(
     context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
 ) : View(context, attrs, defStyleAttr) {
 
     companion object {
         // 半径
         val SMALL_RADIUS = 100.dp
 
         // 几边形
         const val COUNT = 5
    }
 
     private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
 
     // 中心位置
     private val centerLocation by lazy {
         PointF(width / 2f, height / 2f)
    }
 
     override fun onDraw(canvas: Canvas) {
 
         val cx = centerLocation.x
         val cy = centerLocation.y
         // 辅助圆
         canvas.drawCircle(cx, cy, SMALL_RADIUS, paint)
 
 
         // 每一个的间隔
         val eachAngle = 360 / COUNT
        (0 until COUNT).forEach {
 
             val angle = it * eachAngle.toDouble()
 
             val x =
                (SMALL_RADIUS * cos(Math.toRadians(angle)) + centerLocation.x).toFloat()
             val y =
                (SMALL_RADIUS * sin(Math.toRadians(angle)) + centerLocation.y).toFloat()
 
             paint.color = colorRandom
             // 绘制每一个小圆
             canvas.drawCircle(x, y, 10.dp, paint)
        }
    }
 }


那么五边形其实就是吧5个点连接起来即可。

   private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
 
     private val path = Path()
     override fun onDraw(canvas: Canvas) {
 
         // 每一个的间隔
         val eachAngle = 360 / COUNT
        (0 until COUNT).forEach {
             val angle = it * eachAngle.toDouble()
             val x =
                (SMALL_RADIUS * cos(Math.toRadians(angle)) + centerLocation.x).toFloat()
             val y =
                (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()
    }


/   绘制多条五边形   /

假如需要绘制成这样子:


刚才我们绘制的是最中心绿色的五边形,那么这里就需要定义一个变量,来标识每一个五边形之间的间距。例如蓝色五边形和绿色五边形的间距为20.dp。

那么蓝色五边形五个点的半径 = 绿色五边形的半径 + 20.dp。以此类推:

 class E3PolygonChartBlogView @JvmOverloads constructor(
     context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
 ) : View(context, attrs, defStyleAttr) {
 
     companion object {
         // 半径
         val SMALL_RADIUS = 100.dp
 
         // 几边形
         const val COUNT = 5
 
         // 有几条边
         const val NUMBER = 3
 
         // 每一条边的间隔
         val INTERVAL = 20.dp
    }
 
     private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
 
     // 中点
     private val centerLocation by lazy {
         PointF(width / 2f, height / 2f)
    }
 
     private val path = Path()
     override fun onDraw(canvas: Canvas) {
         // 每一个的间隔
         val eachAngle = 360 / COUNT

         // 循环有几条边
        (0 until NUMBER).forEachIndexed { index, element ->
 
             // 循环每一条边有几个点
            (0 until COUNT).forEach { count ->
                 // 半径 = 当前是第几条边 * 间距 + 最中间的距离
                 val radius = element * INTERVAL + SMALL_RADIUS

                 val angle = count * eachAngle.toDouble()
 
                 val x =
                    (radius * cos(Math.toRadians(angle)) + centerLocation.x).toFloat()
                 val y =
                    (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()
        }
    }
 }


/   连接最外层和最内层   /

连接最内层和最外层也比较简单,只需要循环有几条边的时候判断是否是最外层,然后将最外层的点和最内层的点相连接即可。如果需要和中心点相连接,那么stop点为 centerLocation即可。

 override fun onDraw(canvas: Canvas) {
         // 每一个的间隔
         val eachAngle = 360 / COUNT
         // 循环有几条边
        (0 until NUMBER).forEachIndexed { index, element ->
 
             // 循环每一条边有几个点
            (0 until COUNT).forEach { count ->
                 // 半径 = 当前是第几条边 * 间距 + 最中间的距离
                 val radius = element * INTERVAL + SMALL_RADIUS
                 val angle = count * eachAngle.toDouble()
 
                 val x =
                    (radius * cos(Math.toRadians(angle)) + centerLocation.x).toFloat()
                 val y =
                    (radius * sin(Math.toRadians(angle)) + centerLocation.y).toFloat()
                  .....
 
                 // 当前是最后一层
                 if (index == NUMBER - 1) {
                     // 最内层x,y 坐标
                     val stopX =
                        (SMALL_RADIUS * cos(Math.toRadians(angle)) + centerLocation.x).toFloat()
                     val stopY =
                        (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()
        }
    }


那么现在需要一个:

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

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

 companion object {
     // 半径
     val SMALL_RADIUS = 20.dp
 
     // 几边形
     const val COUNT = 10
 
     // 有几条边
     const val NUMBER = 7
 
     // 每一条边的间隔
     val INTERVAL = 20.dp
 }


/   绘制文字   /

还是和上面的套路一样,先来思考文字需要绘制到什么地方?


我们的多边形只到红色的,那么为了保持和最外层有一点距离,所以我们需要将文字绘制到虚线处,还是当绘制最外层的时候开始绘制文字。

     @SuppressLint("DrawAllocation")
     override fun onDraw(canvas: Canvas) {
 
         // 每一个的间隔
         val eachAngle = 360 / COUNT
         // 循环有几条边
        (0 until NUMBER).forEachIndexed { index, element ->
 
             // 循环每一条边有几个点
            (0 until COUNT).forEach { count ->
                 // 半径 = 当前是第几条边 * 间距 + 最中间的距离
                 val radius = element * INTERVAL + SMALL_RADIUS
                 val angle = count * eachAngle.toDouble()
 
                 val x =
                    (radius * cos(Math.toRadians(angle)) + centerLocation.x).toFloat()
                 val y =
                    (radius * sin(Math.toRadians(angle)) + centerLocation.y).toFloat()
                ...
 
                 // 绘制最外层和内层连接线
                ...
 
 
                 // 设置文字
                 if (index == NUMBER - 1) {
                     val text = "文字${count}"
 
                     val rect = Rect()
 
                     // 计算文字宽高 计算完成之后会把值赋值给rect
                     paint.getTextBounds(text, 0, text.length, rect)
                     val textWidth = rect.width()
                     val textHeight = rect.height()
 
                     val tempRadius = radius + textHeight
                     val textX =
                        (tempRadius * cos(Math.toRadians(angle)) + centerLocation.x).toFloat() - textWidth / 2f
                     val textY =
                        (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)
 
                }
            }
          ...
        }
    }


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

/   绘制数据   /

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


假设我们当前需要设置的数据为3,2,3,1,1。那么我们只需要从0坐标开始,算出每一个对应的五边形即可。那么最终结果应该为:


 override fun onDraw(canvas: Canvas) {

   // 绘制网格
          ...

   // 绘制数据
         drawArea(canvas)
 }
 
 var data = listOf(3f, 2f, 3f, 1f, 1f)
  private fun drawArea(canvas: Canvas) {
         data.forEachIndexed { index, value ->
             val location = 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 第几条边
  */
 private fun getLocation(number: Int, count: Float): PointF = let {
     // 角度
     val angle = 360 / COUNT * number

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

     val x =
        (radius * cos(Math.toRadians(angle.toDouble())) + centerLocation.x).toFloat()
     val y =
        (radius * sin(Math.toRadians(angle.toDouble())) + centerLocation.y).toFloat()
 
     return PointF(x, y)
 }


/   手势滑动   /

雷达图的手势滑动和其他的不太一样,因为他需要计算的是角度。

场景1(右下角)


假设当前滑动的位置在右下角。那么他的角度就为红色的角度。

红色的角度 = atan(dy / dx)

场景2(左下角)


假设当前滑动的位置在左下角,那么他的角度就为黑色的角度 + 绿色的角度。

绿色角度 = 90度
红色的角度 = atan(dy / dx)
黑色角度 = 90 - 红色角度

场景3(左上角)


假设当前滑动的位置在左上角,那么他的角度就为红色的角度 + 绿色角度。

  • dx = centerLocation.x - event.x
  • dy = centerLocation.x - event.y

红色的角度 = atan(dy / dx)
绿色的角度 = 180度

场景4(右上角)


假设当前滑动的位置在右上角,那么他的角度就为绿色角度 + 黑色角度。

黑色角度 = 90度 - 红色角度
红色角度 = atan(dy / dx)
绿色角度 = 270度

判断是否是左上角或者右上角,只需要判断两个点的x,y值即可。来看看计算角度代码:

 @param startP: 开始点
 @param endP: 结束点
 
 fun PointF.angle(endP: PointF): Float {
     val startP = this
 
     // 原始位置
     val angle = if (startP.x >= endP.x && startP.y >= endP.y) {
         Log.e("szjLocation", "end在start右下角")
         0
    } else if (startP.x >= endP.x && startP.y <= endP.y) {
         Log.e("szjLocation", "end在start右上角")
         270
    } else if (startP.x <= endP.x && startP.y <= endP.y) {
         Log.e("szjLocation", "end在start左上角")
         180
    } else if (startP.x <= endP.x && startP.y >= endP.y) {
         Log.e("szjLocation", "end在start左下角")
         90
    } else {
         0
    }
     // 计算距离
     val dx = startP.x - endP.x
     val dy = startP.y - endP.y
     // 弧度
     val radian = abs(atan(dy / dx))
 
     // 弧度转角度
     var a = Math.toDegrees(radian.toDouble()).toFloat()
 
     if (startP.x <= endP.x && startP.y >= endP.y) {
         // 左下角
         a = 90 - a
    } else if (startP.x >= endP.x && startP.y <= endP.y) {
         // 右上角
         a = 90 - a
    }
     return a + angle
 }

 var offsetAngle = 0f // 偏移角度
 private var downAngle = 0f // 按下角度
 private var originAngle = 0f // 原始角度
 
 @SuppressLint("ClickableViewAccessibility")
 override fun onTouchEvent(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()
 
     return true
 }

如果这里角度不知道为啥 = 现在角度 - 按下角度 + 原始角度。可以看之前的文章,道理都是一样的,就不过多解释了!最后计算出来的角度直接赋值给onDraw即可。

 override fun onDraw(canvas: Canvas) {
         // 每一个的间隔
         val eachAngle = 360 / COUNT
         // 循环有几条边
        (0 until NUMBER).forEachIndexed { index, element ->
 
             // 循环每一条边有几个点
            (0 until COUNT).forEach { count ->
                 val angle = count * eachAngle.toDouble() + offsetAngle // TODO 设置角度
 
                 val x =
                    (radius * cos(Math.toRadians(angle)) + centerLocation.x).toFloat()
                 val y =
                    (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)
    }


/   设置fling事件   /

我坦白了,fling事件我是偷的MPAndroidChart的源码,这个fling事件和平常的不太一样,有大坑...想了1天没想出来,只能看看前辈思路...如果需要可以自行下载看细节。

完整代码地址如下:
https://gitee.com/lanyangyangzzz/custom-view-project

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
2022年终总结,我的10年Android之旅
手把手带你搞懂AMS启动原理

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注


相关阅读

  • 英语世界《庄子》“卮言”研究述评

  • 寓言、重言和卮言,是《庄子》独特的言说方式,历来歧解纷出,其中,“卮言”尤为学界所重,英语世界亦多有探讨。《庄子》的各种英译本及相关论文中,卮言的译法只有几种,大多遵从中国
  • 3月2日热点话题选题参考及报道方向

  • 1、代表建议春节假期延至9天 取消调休3月1日,#建议春节假期延至9天,取消调休#冲上热搜,引发网友热议。全国两会在即,全国人大代表、安徽省农业科学院副院长赵皖平2月28日在个人
  • 3月3日热点话题选题参考及报道方向

  • 1、2022年《政府工作报告》19项量化指标基本完成国务院3月2日发布2022年《政府工作报告》量化指标任务完成情况。总体来看,所有19项量化指标任务基本完成,其中城镇新增就业和
  • 2021 年,218 条认真阅读的理由|获得了新的角度

  • 2021 年,218 条认真阅读的理由获得了新的角度你现在正在首届阅览室庙会活动的第四部分「获得了新的角度」。当我们在生活里忙碌和盲目时,阅读让我们跳出来,重新思考熟悉的事物,
  • 带着孩子使用ChatGPT后的一些思考

  • 本着让孩子从小就接触前沿技术的一贯想法,这些天抽空带着孩子去跟ChatGPT互动,不断的向这个人工智能提问题。测试的过程中却有一个意外的发现,小朋友测试ChatGPT时,结果远没有之
  • 万能的人像拍摄技巧,拿去吧!!

  • 哈喽~大家早上好呀!关于人像题材的拍摄,你们认为最重要的是什么呢?相信有朋友肯定都会觉得离不开三要素:模特好看、模特好看、还是模特好看!!不可否认,这确实是挺重要的一点。当然,
  • 苹果专利不同角度可呈现不同内容

  • 科技边角料获悉苹果公司申请的“基于个体的相应位置生成和显示内容”专利日前公布。天眼查App显示,该方法包括基于第一个体相对于该显示器的第一观看角度来确定第一渲染特性,
  • 为什么蒙上眼睛,人就不会走直线了?

  • 在世界著名的水城威尼斯,有个圣马可(SanMarco)广场。广场的一端有一座宽82米的雄伟教堂。教堂的前面是一方开阔地。这片开阔地经常吸引着四方游人到这里做一个奇怪的游戏:把眼

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • Weblogic安全漫谈(二)

  • 前言继本系列上篇从CVE-2015-4852入手了解T3协议的构造后,本篇继续分析开启T3反序列化魔盒后的修复与绕过。Weblogic对于10.3.6推出了p20780171和p22248372用于修复CVE-2015-
  • 已破姐神仙软件!双端可用,这下被爽到了!

  • 大家好,我是你们的好朋友大明,欢迎大家来到【大明青年】。今日分享大家会不会想去k歌,但是有时候自己去又不得劲,还不如在家躺,但是有没有能一边在家一边k歌呢。今天大明就给大家
  • Android自定义View之蜘蛛网雷达效果

  • / 今日科技快讯 /近日,在将聊天机器人Bard作为主要用于搜索的AI服务推出后,谷歌似乎正在偏离初衷,没有提供任何具体的用例。在最近举行的全体员工会议上,该公司高管辩称,Bard
  • 关于银行信贷,政府工作报告透露了哪些信号?

  • ‍3月5日出炉的2023年政府工作报告19次提及“金融”,也释放了下一步金融工作的重点任务和要求。银行是金融行业的主力军。关于货币政策和银行信贷,政府工作报告透露了哪些信号
  • 后疫情时代硅谷创新创业的现状、挑战与启示

  • 科情智库摘 要:2022年2月17日,硅谷合资企业地区研究学院发布了最新的综合性区域发展评价报告《2022硅谷指数》。本文综合该报告的分析结果,结合长期对硅谷地区的跟踪了解情况,概