SVG是一种图像文件格式,它的英文全称为Scalable Vector Graphics,意思为可缩放的矢量图形。它是基于XML(Extensible Markup Language),由World Wide Web Consortium(W3C)联盟进行开发的。严格来说应该是一种开放标准的矢量图形语言,可让你设计激动人心的、高分辨率的Web图形页面。用户可以直接用代码来描绘图像,可以用任何文字处理工具打开SVG图像,通过改变部分代码来使图像具有交互功能,并可以随时插入到HTML中通过浏览器来观看。
对于一些不规则的图形使用svg图片还是挺方便的,尤其涉及到交互部分。看完本文你就再也不用怕美工丢一张svg过来了。
本文通过解析SVG实现了一张中国地图,先看下效果
这就是我们要实现的效果,画出一份中国地图,并且点击某个省份时候将该省份加粗,并在下面显示出省份名。
在写代码前需要先看一个SVG文件内容:
密密麻麻的是不是觉得有点眼花,我们找一条最短的看一下:
<path id="820000" title="澳门" class="land" d="M505.56,515.13l0.35,0.51l-0.43,0.26L505.56,515.13z"/>
其实跟android布局文件差不多,只是后面的路径可能有点长而已,可以把d标签里的内容理解成路径Path。比如:
首先我们需要先封装一个省份的类,省份包含路径、颜色、是否选中、名字。别忘了把svg图片拷贝到res/raw路径下面。
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Region;
public class Province {
//省份的path
private Path path;
//背景颜色
private int backgroundColor;
//是否选中
private boolean isSelect = false;
//省份名字 比如河南、广东
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setSelect(boolean select) {
isSelect = select;
}
public void setBackgroundColor(int backgroundColor) {
this.backgroundColor = backgroundColor;
}
public void setPath(Path path) {
this.path = path;
}
//绘制
public void drawProvince(Canvas canvas, Paint paint){
paint.clearShadowLayer();
paint.setStrokeWidth(1);
paint.setColor(backgroundColor);
paint.setShadowLayer(0,0,0,0xffffff);
paint.setStyle(Paint.Style.FILL);
canvas.drawPath(path,paint);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(0xFF000000);
//选中的话就加粗边界
if (isSelect){
paint.setStrokeWidth(3);
}
canvas.drawPath(path,paint);
}
//判断点击位置是否在省份的区域内
public boolean isSelect(float x,float y){
RectF rectF = new RectF();
path.computeBounds(rectF,true);
Region region = new Region();
region.setPath(path,new Region((int)rectF.left,(int)rectF.top,(int)rectF.right,(int)rectF.bottom));
return region.contains((int)x,(int)y);
}
}
然后写一个自定义view,来进行解析绘制,这里踩了个坑,设置颜色时候忘了给颜色加上透明度,断点调试好久才发现画的地图是透明的所以看不到。代码这东西总是在你意想不到的地方出问题。
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.PathParser;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
public class MapView extends View {
//上下文
Context context;
//存放整个地图的RectF,用来计算地图宽高
private RectF mapRectF;
//所有省份的集合
private List<Province> provinces;
//画笔
private Paint paint;
//缩放因子,因为地图宽度大概率超过屏幕宽度,所以需要缩放
private float scale = 1.0f;
//异步解析图片是否完成标志位
boolean finishParse = false;
//颜色数组,每个省份的颜色,记得颜色要写成不透明的(FF),
private int[] colorArray = {0xFF03DAC5,0xFFE68133,0xFF5AE633,0xFFF32C5B,0xFFC820F6,0xFF4657EF,0xFFE2EA1B,
0xFFFF9800,0xFFE89872,0xFF009688};
//记录点击的省份
private Province clickProvince = null;
public MapView(Context context) {
this(context,null);
}
public MapView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MapView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init();
}
private void init() {
//启动一个子线程解析svg文件
parseThread.start();
//初始化画笔
paint = new Paint();
//抗锯齿
paint.setAntiAlias(true);
//画笔宽度
paint.setStrokeWidth(1);
}
Handler handler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
//请求重新测量绘制
requestLayout();
//measure(getMeasuredWidth(),getMeasuredHeight());
invalidate();
}
};
//解析svg
Thread parseThread = new Thread(){
@Override
public void run() {
provinces = new LinkedList<>();
//打开raw目录下的china2.svg
InputStream inputStream = context.getResources().openRawResource(R.raw.china2);
//使用工厂模式创建一个DocumentBuilder
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
//得到对应的xml对象
Document parse = builder.parse(inputStream);
//得到svg文件中所有节点
Element documentElement = parse.getDocumentElement();
//获取到path节点的集合,注意这里是获取到了所有省份的path,所以需要遍历
NodeList pathList = documentElement.getElementsByTagName("path");
float left = -1;
float right = -1;
float top = -1;
float bottom = -1;
//遍历path集合
for (int i=0;i<pathList.getLength();i++){
//得到具体的一项,也就是一个省份
Element item = (Element) pathList.item(i);
//得到d标签里的内容,就是path路径
String attribute = item.getAttribute("d");
//得到title内容,就是省份名字
String name = item.getAttribute("title");
//通过PathParser将得到的路径字符串转成Path对象
Path pathFromPathData = PathParser.createPathFromPathData(attribute);
//new 一个省份,并设置路径,颜色,名字
Province province = new Province();
province.setPath(pathFromPathData);
//从数组中取出颜色
province.setBackgroundColor(colorArray[i%(colorArray.length-1)]);
province.setName(name);
//添加到地图省份集合中
provinces.add(province);
//得到上下左右的边界值
RectF rectF = new RectF();
pathFromPathData.computeBounds(rectF,true);
left = (left==-1)?rectF.left:Math.min(rectF.left,left);
right = right==-1?rectF.right:Math.max(right,rectF.right);
top = top==-1?rectF.top:Math.min(top,rectF.top);
bottom = bottom==-1?rectF.bottom:Math.max(bottom,rectF.bottom);
}
mapRectF = new RectF(left,top,right,bottom);
//解析完成
finishParse = true;
//回到主线程
handler.sendEmptyMessage(-1);
}catch (ParserConfigurationException e){
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
};
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
//如果测量完成 计算缩放因子
if (finishParse){
scale = width/mapRectF.width();
height = (int) mapRectF.height();
}
//这里高度就当成wrap_content处理了
setMeasuredDimension(width,height);
}
@Override
protected void onDraw(Canvas canvas) {
if(provinces == null || provinces.size() < 1 || !finishParse){
return;
}
//保存画布
canvas.save();
//设置缩放
canvas.scale(scale,scale);
//遍历省份集合
for(Province item:provinces){
//设置当前省份是否选中
if(clickProvince == item){
item.setSelect(true);
}else{
item.setSelect(false);
}
//开始绘制
item.drawProvince(canvas,paint);
}
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//处理点击事件
handlerTouchEvent(event.getX(),event.getY());
return true;
}
private void handlerTouchEvent(float x, float y) {
if(!finishParse){
return;
}
//遍历每个省份 看点击的坐标是否在省份中
for (Province province:provinces){
if(province.isSelect(x/scale,y/scale)){
//记录下点击的省份
clickProvince = province;
//重绘 加粗选中省份的边界
invalidate();
//找到点击省份 进行回调
provinceSelectListener.onProvinceSelect(clickProvince.getName());
return;
}
}
}
//省份点击事件的接口
public interface ProvinceSelectListener{
void onProvinceSelect(String name);
}
private ProvinceSelectListener provinceSelectListener;
//设置点击事件监听
public void setProvinceSelectListener(ProvinceSelectListener provinceSelectListener){
this.provinceSelectListener = provinceSelectListener;
}
}
MainActivity代码贴出来:
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MainActivity extends AppCompatActivity implements MapView.ProvinceSelectListener{
@BindView(R.id.tv_province)
TextView tv_province;
@BindView(R.id.china_map)
MapView chinaMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
//设置listener
chinaMap.setProvinceSelectListener(this);
}
//设置点击的响应
@Override
public void onProvinceSelect(String name) {
tv_province.setText(name);
}
}
布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.honeywell.chinasvg.MapView
android:id="@+id/china_map"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_province"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Hello World!"
/>
</LinearLayout>
其实一点也不复杂,调用系统的api就可以了,以前还以为会很复杂。基本上注释写的比较清楚了,相信大家都能看的懂。解析别的svg文件的话把解析时候的标签名换一下就行,如果需要源码的我再把源码上传。
Demo中的中国地图svg下载