地图路线叠加层实现方案

地图路线叠加层实现方案

六月 28, 2018

因为工作业务需要,需要在地图上显示点之间的连线,并且由点击事件,这个线路不是路线规划,也不是直线,还需要有点击事件的容错。因此寻思着自己写一个。

温馨提示: 本篇内容演示代码均为 Kotlin 语言。如果你是使用 Java,那么部分方法或者关键字需要稍微做一下转换,但是本身算法和代码是通用的。

首先送上效果图
效果图

思路

  1. 拥有经纬度,那么想办法转化为屏幕坐标。
  2. 将一组组的屏幕坐标整理出来,先设计一组坐标的绘制方案。
  3. 绘制两点之间的曲线。
  4. 计算曲线的路径,得到点击事件。

设计方案

计算屏幕坐标

高德有提供一个工具方法,可以将经纬度转换为当前地图上的屏幕坐标。所以这个可以一行代码搞定。

1
2
3
4
5
6
7
8
//获得投影对象
val projection = aMap.projection
//
val startPoint = projection.toScreenLocation(bean.startLoc)
val endPoint = projection.toScreenLocation(bean.endLoc)

//核心方法
projection.toScreenLocation(LatLng)

绘制曲线

我们的线路中有起点和终点,两点之间的连线要弯曲成一定的弧度。
那么既然要绘制曲线,可以有几种方案:
  1. 绘制一段弧线,Canvas.drawArc()。
  2. 贝塞尔曲线连接。
但是弧线的话,需要计算圆心位置,半径信息。而这些就比较复杂了。因为一段任意的有方向的线段,要计算他的所在圆的圆心,并不容易。因此放弃了这个方法。
剩下的就是贝塞尔曲线了,我们可以考虑将辅助点从两点的中点往与线段平行的方向移动一点距离,从而得到一条弧线。

思路已经有了,那么下一步呢?怎么计算这个辅助点呢?
首先,按照上面的思路,总结出关键字:中点,平行
也就是说,我们要在线段的中点位置做垂线,又是三角函数了。我就在想,有没有更加简单的方法?

方法也其实简单,我们在纸上怎么为线段画垂线的呢?三角板靠上去?如果不顺手怎么办?答案出来了,我们可以把纸张转一下啊。
那么我们是不是可以也借助这种思想呢?事实是可以的。
那么我们的算法就简单了,假设我们已经将画布旋转了,把线段与坐标系的X轴重叠,并且把起点放到了坐标系的起点位置,那么我们的线段就相当于X轴了,而给X轴做垂线?X不变,Y加上一段距离就好了,对吧。
那么,既然这样的话。
我们再整理一下

1
2
3
辅助点X = 线段长度 / 2
辅助点Y = 线段长度 * 弧度权重
//这里的弧度权重是我个人定义的,因为个人认为,根据长度*权重,可以使辅助点的位置跟随长度变大而变大,使人看起来弧度是一样的。

好了,上面就是我们的算法。那么问题来了,根据上面的算法,我们又需要另外2个东西了,一个是旋转角度,一个是线段长度。
线段长度的算法很简单:就是简单的勾股定理而已。

1
2
3
4
5
6
private fun getLineLength(startPointF: PointF, endPointF: PointF): Float{

return Math.sqrt(1.0 * (startPointF.x - endPointF.x)*(startPointF.x - endPointF.x)
+ (startPointF.y - endPointF.y)*(startPointF.y - endPointF.y)).toFloat()

}

而我们的角度算法就稍微繁琐一点了,因为涉及到三角函数和坐标系问题,因为屏幕的坐标系和手机坐标系是不一样的,屏幕上,左上角是起点,然后向下是Y的正轴,所以有些颠倒。不过也还好吧。因为算法也就那样,所以直接贴上角度代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private fun getAngle(start: PointF, end: PointF): Double{
val a = (end.y - start.y) * 1.0
val b = (end.x - start.x) * 1.0
val angle = try {
when{

a > 0 && b >= 0 -> {//√
360-Math.toDegrees(Math.atan(a / b))
}

a > 0 && b < 0 -> {//√
Math.toDegrees(Math.atan(a / -b)) - 180
}

a <= 0 && b > 0 -> {//√
Math.toDegrees(Math.atan(-a / b))
}

else -> {//√
180 - Math.toDegrees(Math.atan(-a / -b))
}

}

}catch (e: Exception){
0.0
}

return angle

}

本人能力有限,没有整理出一套通用的算法,所以我就只有四个象限分别计算了。

以上,2个主要参数都出来了,那么绘制呢?我们用Path来做绘制。也就是说,我们在Path里面拉出贝塞尔曲线,再Canvas.drawPath()。
而这里,我们使用三次贝塞尔来绘制,因为二次贝塞尔曲线不是很好看。

1
2
3
4
5
6
7
8
9
10
11
12
private fun onPointChange(){
//重置路径
path.reset()
//设置路径起点
path.moveTo(0F,0F)
//获取当前路径长度
pointSpacing = getLineLength(startPoint,endPoint)
//用贝塞尔曲线向下弯曲一定程度并且连接至终点
path.cubicTo(pointSpacing/3,pointSpacing*-0.1F,pointSpacing * 2 / 3,pointSpacing*-0.1F,pointSpacing,0F)

saveClickPoint()
}

这里说明一下path.cubicTo()函数,他有6个参数,分为3组,按照我的理解是:

1
2
3
4
5
{
(上一个点的辅助点X,上一个点的辅助点Y),
(下一个点的辅助点X,下一个点的辅助点Y),
(下一个点的X,下一个点的Y)
}

上面的算法可以看出来,我是将线段分为3份,分别在中间的2个节点插辅助点,辅助点的高度一样,都是长度的十分之一。这样的效果是不至于曲线峰顶不至于太过尖锐,让整个曲线比较平缓。而路径起点设置为(0,0)的原因是,前面说了,假设起点为坐标系原点。

路径已经设置好了,那么我们只要绘制就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override fun draw(canvas: Canvas?){

if(canvas == null){
return
}

//保存画布状态
canvas.save()
//移动画笔起点为线路起点位置
canvas.translate(arrow.startPoint.x,arrow.startPoint.y)
//旋转画布,使线路重叠与X轴
canvas.rotate(-1*arrow.getAngle().toFloat())
//绘制路径
canvas.drawPath(arrow.path,paint)
//恢复到上次保存的画布状态
canvas.restore()

}

好了,这样就行了,代码里面的arrow是我将前面的算法封装到Bean里面了,绘制的时候只需要拿数据就好了,因为后续会有很多线段,如果全部都在绘制的时候,计算,那么会相当卡顿,如果我放到Bean里面,那么将绘制和计算分离开,那么就可以加快绘制的速度了。

前面绘制已经完了,理想的情况下,是可以绘制出一条曲线的了。但是截图里面,还有个箭头,本来是有2个箭头的,将曲线三等分,但是考虑UI复杂度,所以变成了一个箭头,但是代码实现是支持多箭头的。

箭头的数量,其实并不重要,不过是一个循环而已,重点是,箭头的方向要和曲线相同,也就是说,需要知道曲线的切线。

很幸运的是,Google为我们提供了方法(我将算法和iOS小伙伴交流的时候发现,他们没有,窃喜)。方法也是比较简单的,PathMeasure类,路径的测量类。

使用方法也是非常简单的,只需要将Path关联到PathMeasure类上即可,

1
pathMeasure.setPath(path, false)

参数有2个,第一个是路径,第二个boolean值是说,是否在计算的时候,将路径视为闭合的,也就是说,算的时候,当做path已经closed,但是不会影响path本身。很贴心的功能,但是我们不需要。

那么怎么用呢?
我们需要用到的只有这个方法:

1
2
3
val position = FloatArray(2)
val tangent = FloatArray(2)
pathMeasure.getPosTan(length,position,tangent)

getPosTan方法的功能是:传入指定长度,然后将指定长度位置的坐标和对应的切线
position为传出的坐标位置。
tangent为传出的切线角度。
你可能会问了,曲线的长度呢?前面都是直线长度啊?
这里提一下,pathMeasure提供了获取路径长度的方法,叫做

1
pathMeasure.length

这样就够了,我们就可以使用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  private fun drawArrow(canvas: Canvas,arrow: Arrow){
//保存画布状态
canvas.save()
//将画笔的颜色设置为线条需要的颜色
paint.color = arrow.color
//为画笔设置线条宽度,lineWidth是一个固定值,这里是作为成员变量方便统一修改
paint.strokeWidth = lineWidth
//每个线条维护一个路径测量类,之所以独立起来,是为了方便扩展(尽管还没想到扩展什么)
//将路径与测量器关联起来
arrow.pathMeasure.setPath(arrow.path,false)

//点坐标
val position = FloatArray(2)
//点切线
val tangent = FloatArray(2)
//拿到弧线中间点的位置和切线(如果是2个,那么可以拿⅓和⅔长度的位置和切线,以此类推)
arrow.pathMeasure.getPosTan(arrow.pathMeasure.length / 2,position,tangent)
//将切线数据转化为角度,不要问我为啥,我也是搜索出来直接用的
val degrees = (Math.atan2(tangent[1].toDouble(), tangent[0].toDouble()) * 180.0 / Math.PI).toFloat()
//将画布坐标移动到箭头的起点,方便绘制
canvas.translate(position[0],position[1])
//旋转切线角度
canvas.rotate(degrees)

//绘制线条上方的那一边箭头
canvas.drawLine(0F,0F,-arrowSize,-arrowSize,paint)
//绘制线条下方的那一边箭头(与上一行对比,可以看出只有Y变了,一个在上,一个在下,对称)
canvas.drawLine(0F,0F,-arrowSize,arrowSize,paint)

//恢复画板坐标系状态为上一次储存的状态(你可以理解为Ctrl+Z,存和退是一组组对应的,就像括号)
canvas.restore()

}

OK,绘制基本上就全部完成了,我贴一个完整的绘制方法出来,包含了起点和终点的小圆点,多条线段绘制等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
  override fun draw(canvas: Canvas?) {
//画布为空则放弃绘制
if(canvas == null){
return
}

//对线路集合进行循环
for(arrow in arrowList){
//设置画笔颜色
paint.color = arrow.color
//设置画笔宽度
paint.strokeWidth = lineWidth
//设置为描边模式,设置为填充模式的话,就变色半圆色块了
paint.style = Paint.Style.STROKE

//保存画布状态
canvas.save()
//移动画笔起点为线路起点位置
canvas.translate(arrow.startPoint.x,arrow.startPoint.y)
//旋转画布,使线路重叠与X轴
canvas.rotate(-1*arrow.getAngle().toFloat())
//绘制路径
canvas.drawPath(arrow.path,paint)

//绘制线路上的箭头
drawArrow(canvas,arrow)

//设置模式为填充和描边
paint.style = Paint.Style.FILL_AND_STROKE
//描边宽度设置为1
paint.strokeWidth = 1F
//绘制点,设置颜色为边框颜色
paint.color = pointStrokeColor
//绘制起点的边框(因为不方便设置圆形包边,因此绘制2次)
canvas.drawCircle(0F,0F,pointR+pointStroke,paint)
//绘制终点的边框
canvas.drawCircle(arrow.pointSpacing,0F,pointR+pointStroke,paint)
//设置颜色为内圆颜色
paint.color = pointColor
//绘制起点内圆
canvas.drawCircle(0F,0F,pointR,paint)
//绘制终点内圆
canvas.drawCircle(arrow.pointSpacing,0F,pointR,paint)

//恢复到上次保存的画布状态
canvas.restore()
}

}

OK,我们的绘制就都做完了,至于你说刷新?你自己去监听地图拖拽和缩放,然后刷新点坐标数据啊,然后发起重绘啊。

这部分不属于本次内容范畴,所以查查地图API吧。

点击事件

接下来是最后一步了,线路的点击,计算怎么点击到线路上?

我们来理一下,我们真实的需求吧,我真正的需求是,怎么才能判断点击的位置是否在曲线上,或者曲线范围内。

既然是这样的话,怎么做呢?方案很多,我这里的方案是打关键点。
就是说,我在弧线上打多个关键点,每个点为圆心做一个圆,如果点击位置在园内,那么圆所在的线路,就认为被点击了。

是不是感觉似曾相识?没错,又要用到前面的路径测量器了。

这个就很简单了,只是像上面绘制箭头一样,拿到坐标就好了。但是,又是但是。
我前面说了,我们前面的算法都是基于旋转和位移,也就说,所有的坐标都是相对的,而我们的点击位置都是绝对坐标。所以我们还要研究一下反向算法。

怎么矫正呢?

首先是角度,我们把点的角度倒过来转就好了,这里不能使用canvas的rotate()了,我们要计算计算,而这个算法说到底,就是B点绕A点旋转而已。对吧,所以这个算法,我是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  private fun route(p: PointF, center: PointF, angle: Double){
//当前待处理点的X坐标
val x1 = p.x
//当前待处理点的Y坐标
val y1 = p.y
//旋转中心的X坐标
val x2 = center.x
//旋转中心的Y坐标
val y2 = center.y
//将旋转角度计算为弧度
val radians = Math.toRadians(angle)
//下面这段代码是抄过来的,懒得解释了,数学问题什么的最烦了。
//反正就是一个点绕另一个点绕一定角度后的位置
p.x = ((x1 - x2) * Math.cos(radians) + (y1 - y2) * Math.sin(radians) + x2).toFloat()
p.y = (-(x1 - x2) * Math.sin(radians) + (y1 - y2) * Math.cos(radians) + y2).toFloat()
}

嗯,完成上面的算法,那么就把坐标换算的问题都解决了,接下来就是记录关键点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  private fun saveClickPoint(){
//储存位置用的数组
val position = FloatArray(2)
//存储切线用的数组,此处没有用,只是为了避免空指针
val tangent = FloatArray(2)
//关联路径测量器和路径
pathMeasure.setPath(path,false)
//检查当前关键点的数量是否增加
while (clickPointList.size < clickPointSize){
clickPointList.add(PointF())
}
//检查当前关键点的数量是否有多余
while (clickPointList.size > clickPointSize){
clickPointList.removeAt(0)
}
//记录当前关键点密度的情况下,间隔长度
val step = pathMeasure.length / clickPointList.size
//循环为每个点做数据赋值
for(index in 0 until clickPointList.size){
//测量当前点的位置信息
pathMeasure.getPosTan(step * index ,position,tangent)
//对当前点进行赋值,并且将起点的坐标加上去,变为真实位置
clickPointList[index].set(position[0]+startPoint.x,position[1]+startPoint.y)
//旋转点角度,纠正角度
route(clickPointList[index],startPoint,getAngle())
}
}

如此就算是完了。对了,你会说,那么怎么用?很简单啊,for循环就好了啊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//判断点击位置是否在圆内
private fun checkPoint(point: PointF,x: Int,y: Int): Boolean{
//通过勾股定理,计算点击点位置和圆心的距离是否小于半径长度
val length = Math.sqrt(1.0 * (point.x - x)*(point.x - x)
+ (point.y - y)*(point.y - y)).toFloat()
return length <= clickRadius
}

//检查是否在某个点的范围内
fun checkOnClick(x: Int,y: Int){
//开启一个线程,因为太多的数据,可能带来耗时
//本人也没有太好的算法
TaskUtils.get().runAs {
//获取全部的线路,并且遍历
for (arrow in lineDrawable.arrowList){
//获取线路下全部的点,进行遍历
for(point in arrow.clickPointList){
//检查是否点击中
if(checkPoint(point,x, y)){
//当判定为点中时,发送回调并且终止循环
post {
onMapLineClick(arrow)
}
return@runAs
}
}
}
//如果循环结束仍然没有点击中的点,那么发送没有点中的回调
post {
onNoMapLineClick()
}
}
}

至此,所有的逻辑都讲完了。最后再贴上完整的代码。

全部源码

线路的Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import android.graphics.*

/**
* 地图线条的Bean
* @author Lollipop
*/
class Arrow{

val startPoint = PointF()

val endPoint = PointF()

var color = Color.BLACK

val path = Path()

var id = ""

var pointSpacing = 0F

val clickPointList = ArrayList<PointF>()

var clickPointSize = 0

var viewHeight = 0F

val pathMeasure = PathMeasure()

fun onPointChange(start: Point, end: Point){
startPoint.set(start.x.toFloat(),start.y.toFloat())
endPoint.set(end.x.toFloat(),end.y.toFloat())
onPointChange()
}

fun onPointChange(startX: Float,startY: Float, endX: Float, endY: Float){
startPoint.set(startX,startY)
endPoint.set(endX,endY)
onPointChange()
}

fun onPointChange(start: PointF, end: PointF){
startPoint.set(start)
endPoint.set(end)
onPointChange()
}

private fun onPointChange(){
//重置路径
path.reset()
//设置路径起点
path.moveTo(0F,0F)
//获取当前路径长度
pointSpacing = getLineLength(startPoint,endPoint)
//用贝塞尔曲线向下弯曲一定程度并且连接至终点
path.cubicTo(pointSpacing/3,pointSpacing*-0.1F,pointSpacing * 2 / 3,pointSpacing*-0.1F,pointSpacing,0F)

saveClickPoint()
}

private fun getLineLength(startPointF: PointF, endPointF: PointF): Float{

return Math.sqrt(1.0 * (startPointF.x - endPointF.x)*(startPointF.x - endPointF.x)
+ (startPointF.y - endPointF.y)*(startPointF.y - endPointF.y)).toFloat()

}

private fun saveClickPoint(){
val position = FloatArray(2)
val tangent = FloatArray(2)
pathMeasure.setPath(path,false)
while (clickPointList.size < clickPointSize){
clickPointList.add(PointF())
}
while (clickPointList.size > clickPointSize){
clickPointList.removeAt(0)
}
val step = pathMeasure.length / clickPointList.size
for(index in 0 until clickPointList.size){
pathMeasure.getPosTan(step * index ,position,tangent)
clickPointList[index].set(position[0]+startPoint.x,position[1]+startPoint.y)
route(clickPointList[index],startPoint,getAngle())
}
}

fun getAngle(): Double{
return getAngle(startPoint,endPoint)
}

private fun getAngle(start: PointF, end: PointF): Double{
val a = (end.y - start.y) * 1.0
val b = (end.x - start.x) * 1.0
val angle = try {
when{

a > 0 && b >= 0 -> {//√
360-Math.toDegrees(Math.atan(a / b))
}

a > 0 && b < 0 -> {//√
Math.toDegrees(Math.atan(a / -b)) - 180
}

a <= 0 && b > 0 -> {//√
Math.toDegrees(Math.atan(-a / b))
}

else -> {//√
180 - Math.toDegrees(Math.atan(-a / -b))
}

}

}catch (e: Exception){
0.0
}

return angle

}

private fun route(p: PointF, center: PointF, angle: Double){
val x1 = p.x
val y1 = p.y
val x2 = center.x
val y2 = center.y
val radians = Math.toRadians(angle)
p.x = ((x1 - x2) * Math.cos(radians) + (y1 - y2) * Math.sin(radians) + x2).toFloat()
p.y = (-(x1 - x2) * Math.sin(radians) + (y1 - y2) * Math.cos(radians) + y2).toFloat()
}

}

绘制线路的Drawable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import android.graphics.*
import android.graphics.drawable.Drawable

/**
* 地图线条的Drawable
* @author Lollipop
*/
class MapLineDrawable: Drawable() {

private val paint = Paint().apply {
style = Paint.Style.STROKE
isAntiAlias = true
isDither = true
}

val arrowList = ArrayList<Arrow>()

var lineWidth = 5F

var pointR = 5F

var pointStroke = 2F

var pointStrokeColor = Color.WHITE

var pointColor = Color.CYAN

var arrowSize = 2F

var clickRadius = 2F



override fun draw(canvas: Canvas?) {
if(canvas == null){
return
}

for(arrow in arrowList){
paint.color = arrow.color
paint.strokeWidth = lineWidth
paint.style = Paint.Style.STROKE

//保存画布状态
canvas.save()
//移动画笔起点为线路起点位置
canvas.translate(arrow.startPoint.x,arrow.startPoint.y)
//旋转画布,使线路重叠与X轴
canvas.rotate(-1*arrow.getAngle().toFloat())
//绘制路径
canvas.drawPath(arrow.path,paint)

drawArrow(canvas,arrow)

paint.style = Paint.Style.FILL_AND_STROKE
paint.strokeWidth = 1F
//绘制点
paint.color = pointStrokeColor
canvas.drawCircle(0F,0F,pointR+pointStroke,paint)
canvas.drawCircle(arrow.pointSpacing,0F,pointR+pointStroke,paint)
paint.color = pointColor
canvas.drawCircle(0F,0F,pointR,paint)
canvas.drawCircle(arrow.pointSpacing,0F,pointR,paint)

//恢复到上次保存的画布状态
canvas.restore()
}

}

private fun drawArrow(canvas: Canvas,arrow: Arrow){
canvas.save()
paint.color = arrow.color
paint.strokeWidth = lineWidth
arrow.pathMeasure.setPath(arrow.path,false)
val position = FloatArray(2)
val tangent = FloatArray(2)
arrow.pathMeasure.getPosTan(arrow.pathMeasure.length / 2,position,tangent)
val degrees = (Math.atan2(tangent[1].toDouble(), tangent[0].toDouble()) * 180.0 / Math.PI).toFloat()
canvas.translate(position[0],position[1])
canvas.rotate(degrees)

canvas.drawLine(0F,0F,-arrowSize,-arrowSize,paint)
canvas.drawLine(0F,0F,-arrowSize,arrowSize,paint)

canvas.restore()

}

override fun setAlpha(alpha: Int) {
paint.alpha = alpha
invalidateSelf()
}

override fun getOpacity(): Int {
return PixelFormat.TRANSPARENT
}

override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
invalidateSelf()
}



}

绘制线路的View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import android.content.Context
import android.graphics.Color
import android.graphics.Point
import android.graphics.PointF
import android.util.AttributeSet
import android.util.TypedValue
import android.widget.ImageView
import java.util.*
import kotlin.collections.ArrayList

/**
* 地图线路的绘制View
* @author Lollipop
*/
class MapLineView(context: Context, attrs: AttributeSet?, defStyleAttr:Int): ImageView(context, attrs, defStyleAttr),
OnMapLineClickListener {

constructor(context: Context, attrs: AttributeSet?):this(context,attrs,0)

constructor(context: Context):this(context,null)

private val recyclingList = LinkedList<Arrow>()

private val lineDrawable = MapLineDrawable()

private val onMapLineClickListenerList = ArrayList<OnMapLineClickListener>()

private var clickRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,20F,context.resources.displayMetrics)

init {
background = lineDrawable
lineDrawable.lineWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,2F,context.resources.displayMetrics)
lineDrawable.pointR = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,3F,context.resources.displayMetrics)
lineDrawable.pointStroke = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1F,context.resources.displayMetrics)
lineDrawable.pointStrokeColor = Color.WHITE
lineDrawable.arrowSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,3F,context.resources.displayMetrics)
lineDrawable.clickRadius = clickRadius
lineDrawable.pointColor = 0xFFFFAB3A.toInt()

}

fun clear(){
recyclingList.addAll(lineDrawable.arrowList)
lineDrawable.arrowList.clear()
}

fun add(start: PointF, end: PointF,color: Int): Arrow{
return add(getArrow().apply {
onPointChange(start, end)
this.color = color
})
}

fun add(startX: Float,startY: Float, endX: Float, endY: Float,color: Int): Arrow{
return add(getArrow().apply {
onPointChange(startX,startY,endX,endY)
this.color = color
})
}

fun add(start: Point, end: Point, color: Int): Arrow{
return add(getArrow().apply {
onPointChange(start, end)
this.color = color
})
}

fun add(arrow: Arrow): Arrow{
lineDrawable.arrowList.add(arrow)
lineDrawable.invalidateSelf()
arrow.id = "arrow:$arrow"
arrow.clickPointSize = 40
arrow.viewHeight = height.toFloat()
return arrow
}

// private val paint = Paint()

// override fun onDraw(canvas: Canvas?) {
// super.onDraw(canvas)
// if(canvas == null){
// return
// }
// for(arrow in lineDrawable.arrowList){
// canvas.drawCircle(arrow.startPoint.x,arrow.startPoint.y,5F,paint)
// canvas.drawCircle(arrow.endPoint.x,arrow.endPoint.y,5F,paint)
// }
// }

fun update(){
lineDrawable.invalidateSelf()
}

private fun getArrow(): Arrow{
return if(recyclingList.isEmpty()){Arrow()}else{recyclingList.removeFirst()}
}

override fun onMapLineClick(arrow: Arrow) {
for(lis in onMapLineClickListenerList){
lis.onMapLineClick(arrow)
}
}

fun addOnMapLineClickListener(lis: OnMapLineClickListener){
onMapLineClickListenerList.add(lis)
}

fun checkOnClick(x: Int,y: Int){
invalidate()
TaskUtils.get().runAs {
for (arrow in lineDrawable.arrowList){
for(point in arrow.clickPointList){
if(checkPoint(point,x, y)){
post {
onMapLineClick(arrow)
}
return@runAs
}
}
}
post {
onNoMapLineClick()
}
}
}

private fun checkPoint(point: PointF,x: Int,y: Int): Boolean{
val length = Math.sqrt(1.0 * (point.x - x)*(point.x - x)
+ (point.y - y)*(point.y - y)).toFloat()
return length <= clickRadius
}

override fun onNoMapLineClick() {
for(lis in onMapLineClickListenerList){
lis.onNoMapLineClick()
}
}


}

遗留问题

以上就是全部内容了,可以看出上面的算法还有很多的不足。

  1. 点击位置的精确计算问题。
    难点:曲线的检查并不像直线那样简单,怎么做到又精确又快?
  2. 绘制的冗余问题,超出屏幕的部分是应该跳过的。
    难点:起点或者终点都不在屏幕中时,怎么判断曲线经过了屏幕?

但是我能力有限,没有想到更好,效率更高的解决办法。如果你有好的方案,欢迎联系我。
相信聪明的你可以找到我的联系方式的。

最后,感谢你来我的小站。