无图无真相,先上图
这个效果看上去很高大上,通过常规手段来实现的难度很大,使用SVG技术来实现非常合适。
下面分析一下实现思路:
1)不规则的区域需要使用SVG Path
2)地图控件肯定是Canvas来绘制的自定义控件
3)点击变色使用Android原生的触摸事件即可解决
地图的SVG矢量图可以通过网络找到资源,再通过处理使之变成Android系统可以使用的矢量图(本质上是把SVG的标签进行简化),下面以台湾地图为例:fillColor定义Path填充的颜色,strokeColor定义Path边界的颜色,strokeWidth定义Path边界的宽度,pathData定义Path实际的路径,在本文中也就是地图的路径,其中一个城市的Path路径如下:
path
android:fillColor="#CCCCCC"
android:strokeColor="#ffffff"
android:strokeWidth="0.5"
android:pathData="M573.31,330.07L570.81,329.19L570.81,329.19L569.19,331.78L567.43,336.57L566.91,338.87L567.08,339.84L565.09,340.51L563.01,342.21L561.38,344.46L560.72,346.75L560.25,349.77L559.08,351.52L559.03,351.58L565.55,354.4L577.39,358.16L581.64,360.42L584.68,363.43L586.13,366.31L585.37,369.2L583.52,370.26L579.45,370.95L574.88,372.83L572.17,376.22L570.4,380.67L568.44,383.05L571.93,391.75L574.96,395.25L579.68,398.76L589.24,403.27L592.25,405.9L592.87,410.03L592.59,413.09L591.97,415.79L591.2,418.26L591.97,420.54L592.72,423.64L591.75,427.3L591.03,431.27L594.24,432.05L600.28,430.39L605.63,430.05L609.36,430.42L619.23,427.92L623.16,431.05L624.19,434.52L626.8,437.05L630.36,443.18L630.86,447.01L631.47,446.58L639.77,445.8L641.99,446.4L645.4,438.18L656.39,427.05L659.34,422.48L658.89,419.04L659.95,415.32L661.77,410.22L658.01,408.53L652.3,405.28L649.14,403.02L644.78,402.27L639.92,399.36L635.91,396.26L635.46,392.85L639.09,387.74L640.56,381.67L639.32,375.96L639.04,372.02L636.36,369.95L632.94,368.26L630.89,364.68L628.22,361.93L625.21,362.18L622.21,361.61L619.74,359.17L616.47,356.79L611.32,355.15L608.61,352.24L609.5,347.63L606.68,344.37L601.31,342.61L598.09,341.08L594.7,340.1L590.77,338.57L587.64,331.75L584.01,329.31L573.31,330.07z" />
台湾地图的SVG文件是一个XML文件,把他放到res/raw/taiwanhigh.xml,在MainActivity中可以通过getResources().openRawResource(R.raw.taiwanhigh)获取到该XML文件,进行XML解析并通过工具类PathParser拿到里面的PathData,将路径数据作为一个成员变量赋值给自定义的地图控件TaiWan。通过for循环遍历整个XML把所有城市的数据获取到。通过定义一个City类来城市的PathData。我们需要的不仅仅是Path数据,更需要一整块PathData所围绕封闭的Region(区域),才能知道手指是否点在这个城市上面,这里要new一个Region,并使用path_svg.computeBounds(rectF, true)计算出PathData所占的区域。
private TaiWan taiWan;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
taiWan = (TaiWan) findViewById(R.id.taiwan);
ParseSVG();
}
private void ParseSVG() {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
List<City> cities = new ArrayList<>();
DocumentBuilder builder = factory.newDocumentBuilder();
InputStream is = getResources().openRawResource(R.raw.taiwanhigh);
Document document = builder.parse(is);
NodeList svgNodeList = document.getElementsByTagName("path");
for (int i = 0; i < svgNodeList.getLength(); i++) {
Element element = (Element) svgNodeList.item(i);
String path = element.getAttribute("android:pathData");
City city = new City(this);
Path path_svg = PathParser.createPathFromPathData(path);
city.setPath(path_svg);
RectF rectF = new RectF();
path_svg.computeBounds(rectF, true);
Region region = new Region();
region.setPath(path_svg, new Region((int) (rectF.left), (int) (rectF.top), (int) (rectF.right), (int) (rectF.bottom)));
city.setRegion(region);
cities.add(city);
}
taiWan.setCities(cities);
} catch (Exception e) {
e.printStackTrace();
}
}
显而易见地是,我们自定义了Taiwan作为地图控件,并在Taiwan里有一个集合cities,cities里存放了所有城市City的PathData,在onDraw中只要绘制出PathData,就能够显示出整个台湾地图。为了使图片好看一点,我把画布在X,Y坐标上都扩大了1.2倍。 接下来只要处理好手指点击在地图上的变色效果就好了。在Touch事件里面,获取触摸点event.getX()和event.getY(),遍历所有的City并使用region.contains((int) (x / 1.2f), (int) (y / 1.2f))判断该点是否处在Region中,如果在则表示手指按在该城市上面,在City中定义了布尔值isTouch,当手指按下时为True,其他状态为False。之所以在 x 和 y都要 除以1.2f 是由于之前我把画布在X,Y坐标上都扩大了1.2倍,现在需要调整坐标系来要正确计算Region。最后别忘记调用invalidate,通知系统重绘。
public class TaiWan extends View {
List<City> cities;
public TaiWan(Context context) {
super(context);
}
public TaiWan(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
if (null != cities && cities.size() > 0) {
canvas.scale(1.2f, 1.2f);
for (City city : cities) {
city.draw(canvas);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
int x = (int) event.getX();
int y = (int) event.getY();
for (City city : cities) {
Region region = city.getRegion();
boolean isContain = region.contains((int) (x / 1.2f), (int) (y / 1.2f));
if (isContain) {
city.setTouch(true);
} else {
city.setTouch(false);
}
}
invalidate();
}
return true;
}
public List<City> getCities() {
return cities;
}
public void setCities(List<City> cities) {
this.cities = cities;
}
}
City的代码如下,isTouch为True,按下时绘制实心Path并附加阴影,isTouch为False时绘制空心区域,so easy!
public class City {
private Context context;
private Path path;
private Paint paint;
private boolean isTouch;
private Region region;
public City(Context context) {
this.context = context;
paint = new Paint();
paint.setStrokeWidth(3);
paint.setAntiAlias(true);
}
public boolean isTouch() {
return isTouch;
}
public void setTouch(boolean touch) {
isTouch = touch;
}
public Path getPath() {
return path;
}
public void setPath(Path path) {
this.path = path;
}
public void draw(Canvas canvas) {
if (isTouch()) {
int color = getRanColor();
paint.setColor(color);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setShadowLayer(8, 3, 3, Color.DKGRAY);
} else {
paint.setColor(this.context.getResources().getColor(R.color.color1));
paint.setStyle(Paint.Style.STROKE);
}
canvas.drawPath(path, paint);
}
private int getRanColor() {
int[] colors = {this.context.getResources().getColor(R.color.color2),
this.context.getResources().getColor(R.color.color3), this.context.getResources().getColor(R.color.color4)};
return colors[(int) (Math.random() * 3)];
}
public Region getRegion() {
return region;
}
public void setRegion(Region region) {
this.region = region;
}
}
完整代码可见 : https://github.com/pengzee/SVG_TW