本系列自定义View悉数采用kt
体系: 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
来看看代码:
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)
}
}
}
那么五边形其实便是吧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()
}
制作多条五边形
假如需求制作成这样子:
方才咱们制作的是最中心绿色的五边形,
那么这儿就需求定义一个变量,来标识每一个五边形之间的间隔
例如蓝色五边形和绿色五边形的间隔为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()
}
}
}
衔接最外层和最内层
衔接最内层和最外层也比较简单, 只需求循环有几条边的时候判别是否是最外层,
然后将最外层的点和最内层的点相衔接即可
假如需求和中心点相衔接,那么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()
}
}
那么现在需求一个
- 10边形
- 每一条边有7个点
- 最中心的半径为 20.dp
- 每一个边的间隔 = 20.dp
只需求改这4个变量即可:
companionobject{
// 半径
valSMALL_RADIUS=20.dp
// 几边形
constvalCOUNT=10
// 有几条边
constvalNUMBER=7
// 每一条边的间隔
valINTERVAL=20.dp
}
制作文字
仍是和上面的套路相同,先来思考文字需求制作到什么地方?
咱们的多边形只到赤色的,那么为了坚持和最外层有一点间隔,所以咱们需求将文字制作到虚线处,
仍是当制作最外层的时候开端制作 文字
@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)
}
}
...
}
}
到目前为止,蛛网的雏形就差不多了,接下来制作具体的数据
制作数据
制作数据之前先来看看现在点的坐标
假定咱们当时需求设置的数据为 3,2,3,1,1
那么咱们只需求从0坐标开端,算出每一个对应的五边形即可
那么最终结果应该为:
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)
}
手势滑动
雷达图的手势滑动和其他的不太相同, 因为他需求核算的是视点
场景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值即可
来看看核算视点代码:
@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)
}
设置fling事情
我坦白了,fling事情我是偷的MPAndroidChart的源码,
这个fling事情和平常的不太相同,有大坑.. 想了1天没想出来,只能看看长辈思路…
假如需求可以自行下载看细节
完好代码
原创不易,您的点赞便是对我最大的帮助!
- android 自定义View:九宫格解锁
- android自定义View: 制作图表(一)
- android 自定义view: 矩形图表(二)
- android 自定义View:仿QQ拖拽效果
- android 图解 PhotoView,从‘百草园’到‘三味书屋’!