最近的版本有这样一个需求:
有 3 个要素:
中国地图
高亮省区
中心显示数字
面对这样一个需求,该如何实现呢?
高德地图
因为项目是基于高德地图来做的,所以很自然而然的想到了高德。但是当查阅高德地图相关 Api 后,发现并没有能够实现这样需求的方法,所以只能另寻他法了。
图片叠加
让设计师出图,实现第一个要素开发成本极低。至于高亮省区
,也是继续让设计师出图,与全国地图分辨率保持一致,为每个省区设计一张高亮的图,其他地方透明,这样算下来设计师得出 35 张图。若不考虑性能,将图片无脑叠加倒也可以实现。但是作为 Android 开发都知道,这样的一张不算小的图片加载到手机里,占用的内存怕是个庞然大物,更别谈极端情况下要叠加 35 张这样的大图了。 优化下叠加方案:将高亮的省区做成小图,一个包含了省区所有区域的矩形,省区内部高亮,其他区域透明,这样图变小了,但是就得计算小图相对于全国大图的相对位置,对于每个小图都得计算一个比例。同时,绘制高亮省区时可以每次都只取2张图进行叠加,叠加完后释放一张图再加载另一张图,而不用一次性全部加载在内存中。这种方案想想是 ok 的,但是感觉依然还是很麻烦。于是继续探索~
SVG Path
其实网上有很多文章也是有类似的需求,简单搜一下就发现了 SVG 这个解决方案了。看了一眼,便决定就是它了! SVG:可缩放矢量图形(英语:Scalable Vector Graphics,SVG)是一种基于可扩展标记语言(XML),用于描述二维矢量图形的图形格式。 元素是 SVG 基本形状中最强大的一个,它不仅能创建其他基本形状,还能创建更多其他形状。
SVG Path 用 Android 绘制
这里先贴一下我找的北京市的 Path 数据:
1 2 3
<svg height="475 " width="565 "> <path ></svg>
这里要注意一点:SVG Path 里的数据都是在一个固定宽高的矩形里的坐标集合,所以当 Android View 与 SVG 的宽高不一致时,需要进行缩放。 注意下面代码中的 scale 属性:
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
private fun () { val hljRF = RectF() xPaths[HEILONGJIANG_CODE]?.computeBounds(hljRF, true ) val hnRF = RectF() xPaths[HAINAN_CODE]?.computeBounds(hnRF, true ) mapWidth = hljRF.right mapHeight = hnRF.bottom } override fun onMeasure (widthMeasureSpec: Int , heightMeasureSpec: Int ) { super .onMeasure(widthMeasureSpec, heightMeasureSpec) val speSize = View.MeasureSpec.getSize(widthMeasureSpec) scale = speSize / mapWidth setMeasuredDimension(speSize, (speSize * mapHeight / mapWidth).toInt()) } override fun onDraw (canvas: Canvas ) { super .onDraw(canvas) canvas.scale(scale, scale) ... }
再来看到 Path 里有一些 M、L、Z 等字符,这些都是 Path 元素里的指令,后面紧跟的数字即是坐标。
M x,y 移动指令,映射 Path 中的 moveTo L x,y 画直线指令,映射 Path 中的 lineTo H x 画水平线指令,映射 Path 中的 lineTo,不过要使用上一个坐标的 y V y 画垂直线指令,映射 Path 中的 lineTo,不过要使用上一个坐标的 x C x1,y1,x2,y2,x,y 三次贝塞尔曲线指令,映射 Path 中的 cubicTo S x2,y2,x,y 跟在 C 指令后面使用,用 C 指令的结束点做控制点,映射 cubicTo Q x1,y1,x,y 二次贝塞尔曲线指令,映射 quadTo T x,y 跟在 Q 指令后面使用,使用 Q 的 x,y 做控制点,映射 quadTo Z path 关闭指令,映射 close
注意小写指令为使用相对坐标 ,下面 2 行 Path 得到的结果是一样的:
1 2
M421 .139 ,189.75L420 .782 ,186.894 M421 .139 ,189.75l-0 .357 ,-2 .856
基于 Android Path 实现不了小写指令的那种效果,所以只能使用大写指令。这里贴一下一个将 SVG Path 转成 Android 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 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 大专栏 Android 绘制中国地图 "line">97 98 99 100 101 102 103 104 105
public class SvgPathToAndroidPath { private int svgPathLenght = 0 ; private String svgPath = null ; private int mIndex; private List<Integer> cmdPositions = new ArrayList <>(); public Path parser(String svgPath) { this .svgPath = svgPath; svgPathLenght = svgPath.length(); mIndex = 0 ; Path lPath = new Path (); lPath.setFillType(Path.FillType.WINDING); PointF lastPoint = new PointF (); findCommand(); for (int i = 0 ; i < cmdPositions.size(); i++) { Integer index = cmdPositions.get (i); switch (svgPath.charAt(index)) { case 'M' : { String ps[] = findPoints(i); lastPoint.set (Float .parseFloat(ps[0 ]), Float .parseFloat(ps[1 ])); lPath.moveTo(lastPoint.x, lastPoint.y); } break ; case 'L' : { String ps[] = findPoints(i); lastPoint.set (Float .parseFloat(ps[0 ]), Float .parseFloat(ps[1 ])); lPath.lineTo(lastPoint.x, lastPoint.y); } break ; case 'H' : { String ps[] = findPoints(i); lastPoint.set (Float .parseFloat(ps[0 ]), lastPoint.y); lPath.lineTo(lastPoint.x, lastPoint.y); } break ; case 'V' : { String ps[] = findPoints(i); lastPoint.set (lastPoint.x, Float .parseFloat(ps[0 ])); lPath.lineTo(lastPoint.x, lastPoint.y); } break ; case 'C' : { String ps[] = findPoints(i); lastPoint.set (Float .parseFloat(ps[4 ]), Float .parseFloat(ps[5 ])); lPath.cubicTo(Float .parseFloat(ps[0 ]), Float .parseFloat(ps[1 ]), Float .parseFloat(ps[2 ]), Float .parseFloat(ps[3 ]), Float .parseFloat(ps[4 ]), Float .parseFloat(ps[5 ])); } break ; case 'S' : { String ps[] = findPoints(i); lPath.cubicTo(lastPoint.x,lastPoint.y,Float .parseFloat(ps[0 ]), Float .parseFloat(ps[1 ]), Float .parseFloat(ps[2 ]), Float .parseFloat(ps[3 ])); lastPoint.set (Float .parseFloat(ps[2 ]), Float .parseFloat(ps[3 ])); } break ; case 'Q' : { String ps[] = findPoints(i); lastPoint.set (Float .parseFloat(ps[2 ]), Float .parseFloat(ps[3 ])); lPath.quadTo(Float .parseFloat(ps[0 ]), Float .parseFloat(ps[1 ]), Float .parseFloat(ps[2 ]), Float .parseFloat(ps[3 ])); } break ; case 'T' : { String ps[] = findPoints(i); lPath.quadTo(lastPoint.x,lastPoint.y,Float .parseFloat(ps[0 ]), Float .parseFloat(ps[1 ])); lastPoint.set (Float .parseFloat(ps[0 ]), Float .parseFloat(ps[1 ])); } break ; break ; case 'Z' : { lPath.close(); } break ; } } return lPath; } private String [] findPoints(int cmdIndexInPosition) { int cmdIndex = cmdPositions.get (cmdIndexInPosition); String pointString = svgPath.substring(cmdIndex + 1 , cmdPositions.get (cmdIndexInPosition + 1 )); return pointString.split("," ); } private void findCommand() { cmdPositions.clear(); while (mIndex < svgPathLenght) { char c = svgPath.charAt(mIndex); if ('A' <= c && c <= 'Z' ) { cmdPositions.add(mIndex); } ++mIndex; } } }
实现
利用工具类获取每个省区的 Android Path,全部绘制一遍,即可绘制出全国地图(优化:高亮的省区这一步不绘制,避免绘制两次)。
针对高亮省区,调整画笔颜色再绘制一遍即可。
显示数量:这个目前没想到什么好方法,只能让设计师参照地图宽高比 标出每个中心点的位置,就像这样: 然后手动算出每个点的横纵坐标占比,再进行绘制。绘制数量计算坐标时仍要考虑 scale 属性。
参考
Android 上绘制中国省份地图
SVG 转 Android Canvas Path