(十七)SVG 实例-可交互式中国地图

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、效果

地图效果只有各个省份的,按上级要求,地图的展示必须是完整的,这边效果没有南海那些小岛屿,无法展示效果吐了,见谅。原先发布成功了,最近修改后发布,被警告说违法违规,只能删除图片了。
二、分析

第一步 下载含有中国地图的 SVG
这里附一个地图资源: https://www.amcharts.com/dl/javascript-maps/ ,这里面包含世界各个国家的SVG地图,各个省份地图

第二步 将 SVG 资源转换成相应的 Android 代码
用http://inloop.github.io/svg2android/ 网站,将 SVG 转化成 Xml文件,放置在 /res/raw/ 下。

第三步 利用 Xml 解析 SVG 的代码 封装成 JavaBean 最重要的得到 Path
利用 Xml 解析,把中国地图的 Path 封装成一个个省的 JavaBean。

第四步 重写 OnDraw 方法 利用 Path 绘制中国地图

第五步 重写 OnTouchEvent 方法,记录手指触摸位置,判断这个位置是否坐落在某个省份上

三、省份 JavaBean

我们先来封装一个省份的 JavaBean,以便对各个省份进行绘制。

public class Provice {
    /**
     * 绘制路径
     */
    protected Path path;
    /**
     * 绘制颜色
     */
    private  int drawColor;

    public Provice(Path path) {
        this.path = path;
    }


    void draw (Canvas canvas, Paint paint, boolean isSelect) {
        if (isSelect) {
            //选中时,绘制描边效果
            paint.setStrokeWidth(2);
            paint.setColor(Color.BLACK);
            paint.setStyle(Paint.Style.FILL);
            paint.setShadowLayer(8,0,0,0xffffff);
            canvas.drawPath(path,paint);

            //选中时,绘制地图
            paint.clearShadowLayer();
            paint.setColor(drawColor);
            paint.setStyle(Paint.Style.FILL);
            paint.setStrokeWidth(2);
            canvas.drawPath(path, paint);
        }else {
            //非选中时,绘制描边效果
            paint.clearShadowLayer();
            paint.setStrokeWidth(1);
            paint.setStyle(Paint.Style.FILL);
            paint.setColor(drawColor);
            canvas.drawPath(path, paint);

            //非选中时,绘制地图
            paint.setStyle(Paint.Style.STROKE);
            int strokeColor = 0xFFD0E8F4;
            paint.setColor(strokeColor);
            canvas.drawPath(path, paint);
        }
    }

    /**
     * 是否被选中
     */
    public boolean isSelect(int x, int 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(x,y);
    }

    public Path getPath() {
        return path;
    }

    public void setPath(Path path) {
        this.path = path;
    }

    public int getDrawColor() {
        return drawColor;
    }

    public void setDrawColor(int drawColor) {
        this.drawColor = drawColor;
    }
}

这段代码相对还是比较简单,在 draw 中不论选中与否都进行两次绘制,是因为当边沿颜色设置与中间颜色不一样的话,是没办法直接进行绘制,只能分两次进行边沿和中间内容的绘制。

isSelect 就是对一个点是否在一个区域中进行判断,这个方法比较常见,就不具体介绍了。拿 Path 路径与该路径边界组成的矩形进行相裁剪,获取到的就是该 Path 所围成的区域。

四、Xml 数据封装成 JavaBean

这个主要是分两部,一个是读取 Xml 中 Path 的数据,即对 Xml 数据进行解析。二是将获取到的 Path 路径数据封装成 Provice 类。

1.读取 Xml 数据

	 InputStream inputStream = context.getResources().openRawResource(R.raw.china_svg);
	 try {
	     //取得 DocumentBuilderFactory 实例
	     DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
	     //从 factory 获取 DocumentBuilder 实例
	     DocumentBuilder builder = factory.newDocumentBuilder();
	     //解析输入流,得到 Document 实例
	     Document doc = builder.parse(inputStream);
	     Element rootElement = doc.getDocumentElement();
	     NodeList items = rootElement.getElementsByTagName("path");

	     for (int i = 0; i < items.getLength(); i ++) {

	         Element element= (Element) items.item(i);
             String pathData=element.getAttribute("android:pathData");
	    }
    } catch (Exception e) {
	    e.printStackTrace();
    }

这即是对一个 Xml 数据进行读取的方法,跟 File 读写一样,都是固定套路。

  1. 封装 JavaBean

由 Xml 解析器解析出来的 Path 数据是一个字符串,我们需要把这串字符串解析成对应的 Path ,然后调用构造函数创建对应的 Provice。

选取其中一个省份数据进行分析:

    <path
        android:fillColor="#CCCCCC"
        android:strokeColor="#ffffff"
        android:strokeWidth="0.5"
        android:pathData="M546.46,257.82L546.2,258.07L545.19,257.69L545.32,258.18L543.93,257.5L543.54,257.65L542.41,258.89L541.38,259.38L542.02,260.56L540.61,260.4L539.44,259.53L539.28,258.7L538.66,258.32L537.92,258.56L536.13,258.13L535.82,258.52L535.34,258.01L535.11,258.57L534.23,258.84L533.87,259.42L533.2,259.42L532.34,257.98L530.81,258.02L529.74,257.1L529.84,255.69L529,255.13L530.84,254.47L530.2,254L530.15,252.87L528.99,252.2L529.29,251L531.38,249.47L533.13,249.19L533.53,248.49L534.28,249.13L534.78,247.81L535.79,247.12L535.17,245.59L534.67,245.52L533.4,244.1L533.46,243.33L532.94,243.15L533.23,242.5L535.15,241.24L536.1,241.87L537.59,241.28L539.29,238.42L540,238.94L541.47,238.45L542.04,238.74L542.1,238.37L540.49,236.54L540.53,236.05L540.92,235.88L541.32,236.49L542.23,236.64L541.97,235.28L543.78,235.25L543.95,234.15L544.4,233.9L545.04,234.33L544.79,235.26L545.4,236.52L546.36,237.49L547.05,237.66L548.92,239.98L550.77,239.84L552.86,240.63L554.07,240.2L555.26,240.44L555.25,240.92L554.58,240.66L554.36,240.98L554.53,241.89L552.78,241.9L551.97,242.41L552.11,242.94L551.46,243.23L551.9,244.24L551.74,245.25L553.34,247.62L553.79,247.69L553.79,247.69L553.79,248.62L551.45,250.07L551.45,250.07L549.71,250.41L548.96,250.95L548.26,250.65L546.19,250.97L545.91,251.98L546.21,253.02L547.91,253.88L548.31,254.99L547.73,255.44L547.66,256.41L547.66,256.41L547.72,256.71L547.07,256.96z" />

地图的 Path 是由很多条短的直线连接而成的,我们解析 Xml 获取到数据是表示这些连接点的字符串,需要把这个字符串转化成对应的 Path。网络上有提供对应的的工具类,当然也可以自己进行代码编写解析,代码较长,但不是很难,这边也不展示。

3.另起线程

考虑到读取地图数据是一个耗时的操作,故另起一个线程进行数据的读取以及解析。
最终代码:

   private Thread loadThread=new Thread(){
        @Override
        public void run() {
            InputStream inputStream = context.getResources().openRawResource(R.raw.china_svg);
            try {
                //取得 DocumentBuilderFactory 实例
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                //从 factory 获取 DocumentBuilder 实例
                DocumentBuilder builder = factory.newDocumentBuilder();
                //解析输入流,得到 Document 实例
                Document doc = builder.parse(inputStream);
                Element rootElement = doc.getDocumentElement();
                NodeList items = rootElement.getElementsByTagName("path");

                for (int i = 0; i < items.getLength(); i ++) {

                    Element element= (Element) items.item(i);
                    String pathData=element.getAttribute("android:pathData");
                    Path path = PathParser.createPathFromPathData(pathData);
                    proviceList.add(new Provice(path));

                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            handler.sendEmptyMessage(0);
        }
    };
    
    private int[] colorArray = new int[]{0xFF239BD7, 0xFF30A9E5, 0xFF80CBF1, 0xFFB0D7F8};
 Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (proviceList == null) {
                return;
            }
            for (int i = 0; i < proviceList.size(); i++) {
                int color;
                int flag =  i %4;
                switch (flag) {
                    case 1:
                        color = colorArray[0];
                        break;
                    case 2:
                        color = colorArray[1];
                        break;
                    case 3:
                        color = colorArray[2];
                        break;
                    default:
                        color = colorArray[3];
                        break;
                }
                proviceList.get(i).setDrawColor(color);
            }
            postInvalidate();
        }
    };

PathParser 即是 Path 路径字符串转化为 Path 对象的工具类。在需要加载地图的时候,启动这个线程即可。

把数据解析成 JavaBean 之后,需要通知进行刷新操作。在这里,我们对各个省份的绘制颜色进行随机设置,实际中可以根据某一数据进行颜色绘制的选择。

五、onDraw()

onDraw()一直是核心重点。

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (proviceList != null) {
            //绘制没有被选择的省份
            for (Provice item : proviceList) {
                if (item != selectItem) {
                    item.draw(canvas, paint, false);
                }
            }
            //绘制选中的省份
            if (selectItem != null) {
                selectItem.draw(canvas, paint, true);
            }
        }
    }

onDraw()分别进行了选中与未选中省份的绘制,由于前面已经封装好 Provide 类的绘制方法 draw(),这边直接调用即可。
注:必须先绘制未选中的省份,否则在选中与未选中的边沿上,未选中的边沿颜色会把选中的边沿颜色给覆盖掉。

六、OnTouchEvent()

上面已经把 SVG 的地图数据转化为 Path 绘制出来,接下来需要对屏幕的触摸事件进行处理,选中对应区域的省份,重绘地图。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (proviceList != null) {
            Provice provice = null;
            for ( Provice item : proviceList) {
                if (item.isSelect((int) (event.getX()), (int) (event.getY()))) {
                    provice = item;
                    break;
                }
            }
            if (provice != null) {
                selectItem = provice;
                postInvalidate();
            }
        }
        return true;
    }

OnTouchEvent() 方法也比较简单,最开始的时候省份已经封装了判断点是否在该省份 Path 路径内的方法,所以这边只要遍历省份,调用该方法即可,最后进行重绘。

这时候基本实现了大体功能:把 SVG 地图数据绘制出来,实现了点击选中功能。

效果无法在这里展示了,见谅。

可以在 onDraw 方法前后记录时间去获取绘制地图的所需要的时间,是在0~1 毫秒之间,我们初始化的时候把 Path 保存在 List 集合里面,后面只需要再次绘制尽可,绘制的速度是特别快的。

这里,xml 的宽高都是设置为 match_parent,可以发现,屏幕宽度还是过小,导致地图无法完全显示,所以还需要重写 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 以及对地图进行缩放。

七、onMeasure()

首先,要确认一下过去的 SVG 中国地图的具体边界,在 Provice 类的 isSelect(int x, int y) 就有获取各个省份的边界,可以在这边进行一个测试,记录边界值,并取最值,获取中国地图的边界。这边用的边界值为:左:0 上:0 右:773 下:568。即中国地图宽度为 773,高度为 568。


    private static int Mleft=0, Mright=0, Mtop=0, Mbotton=0;

    public boolean isSelect(int x, int 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));

        if (rectF.right > Mright) {
            Mright = (int)rectF.right;
        }
        if (rectF.bottom > Mbotton) {
            Mbotton = (int)rectF.bottom;
        }
        System.out.println( Mright + " " + Mbotton);
        return region.contains(x,y);
    }

加的这些是为了计算中国地图大小,实际不需要。

在 onMeasure()方法里面计算缩放比例。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        scale = Math.min(width/mapWidth, height/mapHeight);

    }

onDraw()方法的时候要进行缩放后在进行绘制

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (proviceList != null) {
            canvas.scale(scale, scale);
            for (Provice item : proviceList) {
                if (item != selectItem) {
                    item.draw(canvas, paint, false);
                }
            }
            if (selectItem != null) {
                selectItem.draw(canvas, paint, true);
            }
        }
    }

onTouchEvent(MotionEvent event) 判定的时候也要添加对应的缩放

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (proviceList != null) {
            Provice provice = null;
            for ( Provice item : proviceList) {
                if (item.isSelect((int) (event.getX() / scale), (int) (event.getY() / scale))) {
                    provice = item;
                    break;
                }
            }
            if (provice != null) {
                selectItem = provice;
                postInvalidate();
            }
        }
        return true;
    }

八、附

代码连接:http://download.csdn.net/download/qq_18983205/9956046

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值