svg实现可交互地图

使用 SVG 实现交互式地图

前言

说起 SVG ,大家肯定都已经使用过,但是如何更深刻的去探究 svg ,以及利用它和自定义 View 结合制作更加复杂的控件,这样才能通过不断的实践去提高自己。

如何制作呢?首先先上一个效果图。

如何实现?

SVG 的特性就是通过Path去绘制,而我们就是利用path绘制每一个省份,我们拼接成完整的地图。

  • 获得地图的svg图片 https://www.amcharts.com/dl/javascript-maps/ 可以在该链接下下载世界任意一个国家地图
  • svg 转化成 Vector Drawable Vector就是Android中的SVG实现(并不是支持全部的SVG语法,现已支持的完全足够用了) 利用 http://inloop.github.io/svg2android/ 转化
  • 解析获得的地图中每一个PATH,分别绘制
  • 交互式地图,判断触摸位置
  • 地图的适配工作

实现过程




前面两步直接完成,这里不再阐述,在得到 Vector 之后,我们需要xml解析,此处利用docounmet解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
InputStream stream = mContext.getResources().openRawResource(R.raw.chinamap);
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); //解析输入流 得到Docunmet实例
Document document = builder.parse(stream); //解析document
NodeList items = document.getElementsByTagName("path");
for (int i = 0; i < items.getLength(); i++) {
Element element = (Element) items.item(i);
String pathData = element.getAttribute("android:pathData");
//获得每一个省份的path
Path path = PathParser.createPathFromPathData(pathData);

接下来就是自定义view的知识,我们需要自定义view,在onDraw 中绘制地图,由于每一个省都要绘制,所以我们需要自定义每一个城市item ,item去完成绘制操作。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//遍历省,单独完成绘制
if (mProviceItems != null) {
canvas.save();
canvas.drawColor(Color.GRAY);
canvas.scale(scale, scale);
for (ProviceItem proviceItem : mProviceItems) {
//判读是否被选择
if (proviceItem != selectItem) {
proviceItem.onDrawItem(canvas, mPaint, false);
}
//单独绘制选中的item
if (selectItem != null) {
selectItem.onDrawItem(canvas, mPaint, true);
}
}
}
}

分析:此处截取的是 onDraw 方法,每一个都单独绘制,通过对选中的城市单独绘制 ProviceItem 就是每一个省份的item,下面贴出代码

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
/**
* Created by zqq on 2017/9/6.
* Description: 省份item
*/
public class ProviceItem {
private Path mPath;
//省份绘制的颜色
private int mColor;
//当前省份的
private String mCity;
public ProviceItem(Path path) {
mPath = path;
}
/**
* 绘制每一个省
*
* @param canvas
* @param paint
* @param isSelect 当前是否被选择
*/
public void onDrawItem(Canvas canvas, Paint paint, Boolean isSelect) {
//选中的身份
if (isSelect) {
//绘制背景
paint.setStrokeWidth(2);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL);
//点击的阴影效果
paint.setShadowLayer(8, 0, 0, 0xffffff);
canvas.drawPath(mPath, paint);
//绘制本身
paint.clearShadowLayer();
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(2);
paint.setColor(mColor);
canvas.drawPath(mPath, paint);
//绘制地图
} else {
//绘制本身
paint.clearShadowLayer();
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.FILL);
paint.setColor(getColor());
canvas.drawPath(mPath, paint);
//绘制描边效果
paint.setColor(0xFFD0E8F4);
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath, paint);
}
}
/**
* 判断当前item是否被点击
*/
public Boolean isTouch(float x, float y) {
//构造一个区域对象
RectF rectF = new RectF();
//计算控制点边界
mPath.computeBounds(rectF, true);
Region region = new Region();
//设置区域路径和剪辑描述的区域
region.setPath(mPath, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
return region.contains((int) x, (int) y);
}
public Path getPath() {
return mPath;
}
public void setPath(Path path) {
mPath = path;
}
public int getColor() {
return mColor;
}
public void setColor(int color) {
mColor = color;
}
public String getCity() {
return mCity;
}
public void setCity(String city) {
mCity = city;
}
}

分析:ProviceItem 负责自己的绘制逻辑,同时判断当前点击是否在当前地图范围内,如果判断下文会详细阐述

对于一个不规则的图形,如果去实现点击实现?

这里在Android中通过path绘制的图形,能够在以下的方式中完成判断

这里会介绍一个类 Region 它是区域的意思,就是能截图屏幕中一块区域。如何截取?就通过 RectF 去限定一个区域,PATH能获取自己的边界, Region 通过 setPath方法,能在path限定的边界中,截取path绘制的图形的部分通过 contains方法就行判断处当前点击的位置是否在区域内。

1
2
3
4
5
6
7
8
9
//构造一个区域对象
RectF rectF = new RectF();
//计算控制点边界
mPath.computeBounds(rectF, true);
Region region = new Region();
//设置区域路径和剪辑描述的区域
region.setPath(mPath, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
//
region.contains((int) x, (int) y);

到此处基本上就能完成可交互地图的编写,完成地图的逻辑,难点都在此处阐述。完整的代码会在文章最后给出。

接下来就是适配工作,如果去让地图能放缩?我们不仅仅需要获取控件本身的宽高,还要知道svg图片的大小,如何得到地图的大小?而地图的大小是每一个path组成的,就是说我们只能知道每一个path的大小,但是不能得到整个地图的大小,这里我们的解决办法是去遍历path,取四个方向path的最值,这样得到的边界就是地图本身的大小。

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
//定义四个方向
float left = -1;
float right = -1;
float top = -1;
float bottom = -1;
for (int i = 0; i < items.getLength(); i++) {
Element element = (Element) items.item(i);
String pathData = element.getAttribute("android:pathData");
//遍历获取每一个path的同时去判断每一个path对应方向的最值
Path path = PathParser.createPathFromPathData(pathData);
RectF rectF = new RectF();
path.computeBounds(rectF, true);
left = left == -1 ? rectF.left : Math.min(rectF.left, left);
top = top == -1 ? rectF.top : Math.min(rectF.top, top);
right = right == -1 ? rectF.right : Math.max(rectF.right, right);
bottom = bottom == -1 ? rectF.bottom : Math.max(rectF.bottom, bottom);
}
//此处获取的就是地图的大小
mRectF = new RectF(left, top, right, bottom);

在 onMeasure完成mapview的测量 默认高度,获得参考高度,这样不会造成宽高比例失调的情况

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
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthmode = MeasureSpec.getMode(widthMeasureSpec);
int widthsize = MeasureSpec.getSize(widthMeasureSpec);
int heightmode = MeasureSpec.getMode(heightMeasureSpec);
int heightsize = MeasureSpec.getSize(heightMeasureSpec);
mViewwidth = widthsize;
mViewheight = heightsize;
switch (widthmode) {
case MeasureSpec.EXACTLY:
mViewwidth = widthsize > minWidth ? widthsize : minWidth;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
mViewwidth = minWidth;
break;
}
//得到参考高度
int computHeight = minHeight * mViewheight / minHeight;
switch (heightmode) {
//布局中写死了dp
case MeasureSpec.EXACTLY:
mViewheight = heightsize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
//对照参考高度,取最大值
mViewheight = minHeight > computHeight ? minHeight : computHeight;
break;
}
setMeasuredDimension(MeasureSpec.makeMeasureSpec(mViewwidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mViewheight, MeasureSpec.EXACTLY));
}

利用控件宽高和图片本身大小实现完美放缩

1
2
3
4
5
6
//宽度 的放缩比例
scale = mViewwidth / mRectF.width();
//高度的放缩比例
mScale2 = mViewheight / mRectF.height();
//取最小的放缩比例,适应所有
scale = Math.min(scale, mScale2);

控件的宽高设置,决定放缩比例,而放缩比例在 onDraw方法中完成对canvas的放缩,从而实现适配

小结:

利用SVG绘制可交互式地图,回想看看,整个过程其实同样可以使用自定义View的开发,只要能有和原型图一样的SVG图片,我们利用这个方式就能完成自定义View,以后自定义View只要找UI要SVG图片就行了,哈哈。

最后附上代码链接 https://github.com/kehanchenk/workproject

结语

工作原因,也有自己的原因好久没更新博客,此次带来关于中国地图的svg的自定义view,希望能给我提出文章的不足或者漏洞,以此共勉!