效果如图,点击省份区域,即可蓝色高亮显示
实现思路:
地图资源
Android SVG to VectorDrawable
1.首先获取SVG图片,然后将SVG转化为VectorDrawable,然后放入res/raw目录下(SVG图片代码在文章末尾)
2.通过DocumentBuilderFactory获取svg中的根节点rootElement
3.通过rootElement获取所有path节点
4.遍历path节点,获取pathData
5.将pathData转换为Path
6.根据区域大小缩放画布,绘制Path
7.onTouch中进行监听,根据点击的(x,y)判断是否在path区域中(使用Region),如果在,就将此区域高亮显示。
代码:
package com.test.svgdemo.PathParse;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Region;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import com.test.svgdemo.R;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
/**
* Created by ygdx_lk on 17/6/24.
*/
public class DrawTaiWan extends View {
private Paint mPaint;
private List<Path> pathList;
private float left, top, right, bottom;
private float scale;
private Path selectPath;
private int width, height;
public DrawTaiWan(Context context) {
super(context);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.GRAY);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(3);
mPaint.setStyle(Paint.Style.STROKE);
//解析svg
parsePath();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float svg_w = right - left;
float svg_h = bottom - top;
float scaleX = svg_w / width;
float scaleY = svg_h / height;
scale = scaleX < scaleY ? scaleY : scaleX;
//缩放画笔,保证绘制的SVG图片完整显示在屏幕内
canvas.scale(scale, scale);
//绘制path
for (Path path :
pathList) {
if(path != null){
if(selectPath == path){
//选中的绘制成蓝色
mPaint.setColor(Color.BLUE);
mPaint.setStrokeWidth(3);
}else {
mPaint.setStrokeWidth(1);
mPaint.setColor(Color.GRAY);
}
canvas.drawPath(path, mPaint);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
float x = event.getX();
float y = event.getY();
//由于花布被缩放过,所以计算点击点是否在path中也需要进行相应缩放
boolean contains = containsRegion((int)(x / scale), (int)(y / scale));
if(contains){
invalidate();
}
break;
}
return super.onTouchEvent(event);
}
/**
* 判断按下的点,是否在path内
* @param x
* @param y
* @return
*/
private boolean containsRegion(int x, int y) {
for (Path path : pathList) {
RectF bounds = new RectF();
path.computeBounds(bounds, true);
Region region = new Region((int)(bounds.left), (int)(bounds.top), (int)(bounds.right), (int)(bounds.bottom));
if(region.contains(x, y)) {
selectPath = path;
return true;
}
}
return false;
}
/**
* 解析SVG
*/
private void parsePath() {
new Thread(new Runnable() {
@Override
public void run() {
//新建pathList列表
pathList = new ArrayList<>();
//获取svg输入流
InputStream inputStream = getResources().openRawResource(R.raw.china);
try {
//读取跟节点
Element rootElement = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream).getDocumentElement();
//获取所有path节点
NodeList paths = rootElement.getElementsByTagName("path");
//遍历paths
for (int i = 0; i < paths.getLength(); i++) {
Element item = (Element) paths.item(i);
if(item != null){
//获取pathData
String d = item.getAttribute("d");
if(TextUtils.isEmpty(d))
d = item.getAttribute("android:pathData");
//将pathData转换成Path
Path path = PathParser.createPathFromPathData(d);
if(path != null){
//获取path区域,并且计算所有path组合的上下左右边界
RectF bounds = new RectF();
path.computeBounds(bounds, true);
left = left > bounds.left ? bounds.left : left;
top = top > bounds.top ? bounds.top : top;
right = right < bounds.right ? bounds.right : right;
bottom = bottom < bounds.bottom ? bounds.bottom : bottom;
pathList.add(path);
}
}
}
//解析完毕,绘制界面
postInvalidate();
} catch (Exception e) {
e.printStackTrace();
}finally {
if(inputStream != null){
try{inputStream.close(); inputStream = null;}catch (Exception e){}}
}
}
}).start();
}
}
PathParser
package com.test.svgdemo.PathParse;
import android.graphics.Path;
import android.util.Log;
import java.util.ArrayList;
/**
* path 路径解析兼容类(兼容标准svg)
*/
public class PathParser {
private static final String LOG_TAG = "PathParser";
// Copy from Arrays.copyOfRange() which is only available from API level 9.
/**
* Copies elements from {@code original} into a new array, from indexes start (inclusive) to
* end (exclusive). The original order of elements is preserved.
* If {@code end} is greater than {@code original.length}, the result is padded
* with the value {@code 0.0f}.
*
* @param original the original array
* @param start the start index, inclusive
* @param end the end index, exclusive
* @return the new array
* @throws IllegalArgumentException if {@code start > end}
* @throws NullPointerException if {@code original == null}
*/
private static float[] copyOfRange(float[] original, int start, int end) {
if (start > end) {
throw new IllegalArgumentException();
}
int originalLength = original.length;
if (start < 0 || start > originalLength) {
throw new ArrayIndexOutOfBoundsException();
}
int resultLength = end - start;
int copyLength = Math.min(resultLength, originalLength - start);
float[] result = new float[resultLength];
System.arraycopy(original, start, result, 0, copyLength);
return result;
}
/**
* @param pathData The string representing a path, the same as "d" string in svg file.
* @return the generated Path object.
*/
public static Path createPathFromPathData(String pathData) {
Path path = new Path();
PathDataNode[] nodes = createNodesFromPathData(pathData);
if (nodes != null) {
try {
PathDataNode.nodesToPath(nodes, path);
} catch (RuntimeException e) {
throw new RuntimeException("Error in parsing " + pathData, e);
}
return path;
}
return null;
}
/**
* @param pathData The string representing a path, the same as "d" string in svg file.
* @return an array of the PathDataNode.
*/
public static PathDataNode[] createNodesFromPathData(String pathData) {
if (pathData == null) {
return null;
}
int start = 0;
int end = 1;
ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
while (end < pathData.length()) {
end = nextStart(pathData, end);
String s = pathData.substring(start, end).trim();
if (s.length() > 0) {
float[] val = getFloats(s);
addNode(list, s.charAt(0), val);
}
start = end;
end++;
}
if ((end - start) == 1 && start < pathData.length()) {
addNode(list, pathData.charAt(start), new float[0]);
}
return list.toArray(new PathDataNode[list.size()]);
}
/**
* @param source The array of PathDataNode to be duplicated.
* @return a deep copy of the <code>source</code>.
*/
public static PathDataNode[] deepCopyNodes(PathDataNode[] source) {
if (source == null) {
return null;
}
PathDataNode[] copy = new PathDataNode[source.length];
for (int i = 0; i < source.length; i++) {
copy[i] = new PathDataNode(source[i]);
}
return copy;
}
/**
* @param nodesFrom The source path represented in an array of PathDataNode
* @param nodesTo The target path represented in an array of PathDataNode
* @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
*/
public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) {
if (nodesFrom == null || nodesTo == null) {
return false;
}
if (nodesFrom.length != nodesTo.length) {
return false;
}
for (int i = 0; i < nodesFrom.length; i++) {
if (nodesFrom[i].type != nodesTo[i].type
|| nodesFrom[i].params.length != nodesTo[i].params.length) {
return false;
}
}
return true;
}
/**
* Update the target's data to match the source.
* Before calling this, make sure canMorph(target, source) is true.
*
* @param target The target path represented in an array of PathDataNode
* @param source The source path represented in an array of PathDataNode
*/
public static void updateNodes(PathDataNode[] target, PathDataNode[] source) {
for (int i = 0; i < source.length; i++) {
target[i].type = source[i].type;
for (int j = 0; j < source[i].params.length; j++) {
target[i].params[j] = source[i].params[j];
}
}
}
private static int nextStart(String s, int end) {
char c;
while (end < s.length()) {
c = s.charAt(end);
// Note that 'e' or 'E' are not valid path commands, but could be
// used for floating point numbers' scientific notation.
// Therefore, when searching for next command, we should ignore 'e'
// and 'E'.
if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
&& c != 'e' && c != 'E') {
return end;
}
end++;
}
return end;
}
private static void addNode(ArrayList<PathDataNode> list, char cmd, float[] val) {
list.add(new PathDataNode(cmd, val));
}
private static class ExtractFloatResult {
// We need to return the position of the next separator and whether the
// next float starts with a '-' or a '.'.
int mEndPosition;
boolean mEndWithNegOrDot;
}
/**
* Parse the floats in the string.
* This is an optimized version of parseFloat(s.split(",|\\s"));
*
* @param s the string containing a command and list of floats
* @return array of floats
*/
private static float[] getFloats(String s) {
if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') {
return new float[0];
}
try {
float[] results = new float[s.length()];
int count = 0;
int startPosition = 1;
int endPosition = 0;
ExtractFloatResult result = new ExtractFloatResult();
int totalLength = s.length();
// The startPosition should always be the first character of the
// current number, and endPosition is the character after the current
// number.
while (startPosition < totalLength) {
extract(s, startPosition, result);
endPosition = result.mEndPosition;
if (startPosition < endPosition) {
results[count++] = Float.parseFloat(
s.substring(startPosition, endPosition));
}
if (result.mEndWithNegOrDot) {
// Keep the '-' or '.' sign with next number.
startPosition = endPosition;
} else {
startPosition = endPosition + 1;
}
}
return copyOfRange(results, 0, count);
} catch (NumberFormatException e) {
throw new RuntimeException("error in parsing \"" + s + "\"", e);
}
}
/**
* Calculate the position of the next comma or space or negative sign
*
* @param s the string to search
* @param start the position to start searching
* @param result the result of the extraction, including the position of the
* the starting position of next number, whether it is ending with a '-'.
*/
private static void extract(String s, int start, ExtractFloatResult result) {
// Now looking for ' ', ',', '.' or '-' from the start.
int currentIndex = start;
boolean foundSeparator = false;
result.mEndWithNegOrDot = false;
boolean secondDot = false;
boolean isExponential = false;
for (; currentIndex < s.length(); currentIndex++) {
boolean isPrevExponential = isExponential;
isExponential = false;
char currentChar = s.charAt(currentIndex);
switch (currentChar) {
case ' ':
case ',':
foundSeparator = true;
break;
case '-':
// The negative sign following a 'e' or 'E' is not a separator.
if (currentIndex != start && !isPrevExponential) {
foundSeparator = true;
result.mEndWithNegOrDot = true;
}
break;
case '.':
if (!secondDot) {
secondDot = true;
} else {
// This is the second dot, and it is considered as a separator.
foundSeparator = true;
result.mEndWithNegOrDot = true;
}
break;
case 'e':
case 'E':
isExponential = true;
break;
}
if (foundSeparator) {
break;
}
}
// When there is nothing found, then we put the end position to the end
// of the string.
result.mEndPosition = currentIndex;
}
/**
* Each PathDataNode represents one command in the "d" attribute of the svg
* file.
* An array of PathDataNode can represent the whole "d" attribute.
*/
public static class PathDataNode {
/*package*/
char type;
float[] params;
private PathDataNode(char type, float[] params) {
this.type = type;
this.params = params;
}
private PathDataNode(PathDataNode n) {
type = n.type;
params = copyOfRange(n.params, 0, n.params.length);
}
/**
* Convert an array of PathDataNode to Path.
*
* @param node The source array of PathDataNode.
* @param path The target Path object.
*/
public static void nodesToPath(PathDataNode[] node, Path path) {
float[] current = new float[6];
char previousCommand = 'm';
for (int i = 0; i < node.length; i++) {
addCommand(path, current, previousCommand, node[i].type, node[i].params);
previousCommand = node[i].type;
}
}
/**
* The current PathDataNode will be interpolated between the
* <code>nodeFrom</code> and <code>nodeTo</code> according to the
* <code>fraction</code>.
*
* @param nodeFrom The start value as a PathDataNode.
* @param nodeTo The end value as a PathDataNode
* @param fraction The fraction to interpolate.
*/
public void interpolatePathDataNode(PathDataNode nodeFrom,
PathDataNode nodeTo, float fraction) {
for (int i = 0; i < nodeFrom.params.length; i++) {
params[i] = nodeFrom.params[i] * (1 - fraction)
+ nodeTo.params[i] * fraction;
}
}
private static void addCommand(Path path, float[] current,
char previousCmd, char cmd, float[] val) {
int incr = 2;
float currentX = current[0];
float currentY = current[1];
float ctrlPointX = current[2];
float ctrlPointY = current[3];
float currentSegmentStartX = current[4];
float currentSegmentStartY = current[5];
float reflectiveCtrlPointX;
float reflectiveCtrlPointY;
switch (cmd) {
case 'z':
case 'Z':
path.close();
// Path is closed here, but we need to move the pen to the
// closed position. So we cache the segment's starting position,
// and restore it here.
currentX = currentSegmentStartX;
currentY = currentSegmentStartY;
ctrlPointX = currentSegmentStartX;
ctrlPointY = currentSegmentStartY;
path.moveTo(currentX, currentY);
break;
case 'm':
case 'M':
case 'l':
case 'L':
case 't':
case 'T':
incr = 2;
break;
case 'h':
case 'H':
case 'v':
case 'V':
incr = 1;
break;
case 'c':
case 'C':
incr = 6;
break;
case 's':
case 'S':
case 'q':
case 'Q':
incr = 4;
break;
case 'a':
case 'A':
incr = 7;
break;
}
for (int k = 0; k < val.length; k += incr) {
switch (cmd) {
case 'm': // moveto - Start a new sub-path (relative)
currentX += val[k + 0];
currentY += val[k + 1];
if (k > 0) {
// According to the spec, if a moveto is followed by multiple
// pairs of coordinates, the subsequent pairs are treated as
// implicit lineto commands.
path.rLineTo(val[k + 0], val[k + 1]);
} else {
path.rMoveTo(val[k + 0], val[k + 1]);
currentSegmentStartX = currentX;
currentSegmentStartY = currentY;
}
break;
case 'M': // moveto - Start a new sub-path
currentX = val[k + 0];
currentY = val[k + 1];
if (k > 0) {
// According to the spec, if a moveto is followed by multiple
// pairs of coordinates, the subsequent pairs are treated as
// implicit lineto commands.
path.lineTo(val[k + 0], val[k + 1]);
} else {
path.moveTo(val[k + 0], val[k + 1]);
currentSegmentStartX = currentX;
currentSegmentStartY = currentY;
}
break;
case 'l': // lineto - Draw a line from the current point (relative)
path.rLineTo(val[k + 0], val[k + 1]);
currentX += val[k + 0];
currentY += val[k + 1];
break;
case 'L': // lineto - Draw a line from the current point
path.lineTo(val[k + 0], val[k + 1]);
currentX = val[k + 0];
currentY = val[k + 1];
break;
case 'h': // horizontal lineto - Draws a horizontal line (relative)
path.rLineTo(val[k + 0], 0);
currentX += val[k + 0];
break;
case 'H': // horizontal lineto - Draws a horizontal line
path.lineTo(val[k + 0], currentY);
currentX = val[k + 0];
break;
case 'v': // vertical lineto - Draws a vertical line from the current point (r)
path.rLineTo(0, val[k + 0]);
currentY += val[k + 0];
break;
case 'V': // vertical lineto - Draws a vertical line from the current point
path.lineTo(currentX, val[k + 0]);
currentY = val[k + 0];
break;
case 'c': // curveto - Draws a cubic Bézier curve (relative)
path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
val[k + 4], val[k + 5]);
ctrlPointX = currentX + val[k + 2];
ctrlPointY = currentY + val[k + 3];
currentX += val[k + 4];
currentY += val[k + 5];
break;
case 'C': // curveto - Draws a cubic Bézier curve
path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
val[k + 4], val[k + 5]);
currentX = val[k + 4];
currentY = val[k + 5];
ctrlPointX = val[k + 2];
ctrlPointY = val[k + 3];
break;
case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
reflectiveCtrlPointX = 0;
reflectiveCtrlPointY = 0;
if (previousCmd == 'c' || previousCmd == 's'
|| previousCmd == 'C' || previousCmd == 'S') {
reflectiveCtrlPointX = currentX - ctrlPointX;
reflectiveCtrlPointY = currentY - ctrlPointY;
}
path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
val[k + 0], val[k + 1],
val[k + 2], val[k + 3]);
ctrlPointX = currentX + val[k + 0];
ctrlPointY = currentY + val[k + 1];
currentX += val[k + 2];
currentY += val[k + 3];
break;
case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
reflectiveCtrlPointX = currentX;
reflectiveCtrlPointY = currentY;
if (previousCmd == 'c' || previousCmd == 's'
|| previousCmd == 'C' || previousCmd == 'S') {
reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
}
path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
ctrlPointX = val[k + 0];
ctrlPointY = val[k + 1];
currentX = val[k + 2];
currentY = val[k + 3];
break;
case 'q': // Draws a quadratic Bézier (relative)
path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
ctrlPointX = currentX + val[k + 0];
ctrlPointY = currentY + val[k + 1];
currentX += val[k + 2];
currentY += val[k + 3];
break;
case 'Q': // Draws a quadratic Bézier
path.quadTo(val[k + 0], val[