Android SVG图片解析Demo

SVG

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

Demo

本文通过解析SVG实现了一张中国地图,先看下效果
Android SVG图片解析Demo_第1张图片
这就是我们要实现的效果,画出一份中国地图,并且点击某个省份时候将该省份加粗,并在下面显示出省份名。
在写代码前需要先看一个SVG文件内容:
Android SVG图片解析Demo_第2张图片
密密麻麻的是不是觉得有点眼花,我们找一条最短的看一下:

<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下载

你可能感兴趣的:(java,android,android,svg)