SVG

SVG是一种图像文件格式,它的英文全称为Scalable Vector Graphics,意思为可缩放的矢量图形。它是基于XML(Extensible Markup Language),由World Wide Web Consortium(W3C)联盟进行开发的。严格来说应该是一种开放标准的矢量图形语言,可让你设计激动人心的、高分辨率的Web图形页面。用户可以直接用代码来描绘图像,可以用任何文字处理工具打开SVG图像,通过改变部分代码来使图像具有交互功能,并可以随时插入到HTML中通过浏览器来观看。
对于一些不规则的图形使用svg图片还是挺方便的,尤其涉及到交互部分。看完本文你就再也不用怕美工丢一张svg过来了。

Demo

本文通过解析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。比如:

  • M就是moveTo方法,将画笔移动到指定坐标。
  • L -->lineTo,画直线。
  • H -->horizeontal LineTo,画水平线
  • V --> vertical lineTo,画垂直线
  • C -->curveto,三次贝塞尔曲线
  • Q -->quadratic Belzier curve,二次贝塞尔曲线
  • Z --> closePath,关闭路径
    其实知道个大概意思就够了,没必要把路径完全看懂。

代码

首先我们需要先封装一个省份的类,省份包含路径、颜色、是否选中、名字。别忘了把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下载

更多相关文章

  1. Android初学者教程
  2. Android(安卓)NDK系列三(Android(安卓)Studio cmke 编译多个个.so
  3. Android(安卓)Material Design 详解(使用support v7兼容5.0以下系
  4. 转:关于android中图片裁剪以及PorterDuffXfermode的使用经验小结
  5. Android(安卓)实现颜色渐变的一个小 tip
  6. Android(安卓)java.lang.NoClassDefFoundError:*报错的处理
  7. android保存文件到SD卡中
  8. Android(安卓)Studio:依赖包的版本号
  9. Android(安卓)Studio 报错 ERROR: A problem occurred configuri

随机推荐

  1. 《世界前200名顶尖计量经济学家》,计量经
  2. 2020年要做的几件大事
  3. 六成开发者日编程不足4小时,半数认为学习
  4. 汉森的研究反省:充分利用数据来对经济建模
  5. 中国首个官方推出的开源协议:木兰宽松许可
  6. 开源分布式跟踪方案概览
  7. 百万年薪挖了个P8程序员,难道是“水货”?
  8. 想要改变世界的 Rust 语言
  9. “Python太慢了、Golang糟透了、MongoDB
  10. 周末分享 | 努力只是成功的一小部分