天天看點

Android 使用svg構造互動式中國地圖概念

概念

什麼是svg

即Scalable Vector Graphics 可伸縮矢量圖形

SVG的W3C的解釋: http://www.w3school.com.cn/svg/svg_intro.asp

什麼是矢量圖像,什麼是位圖圖像?

1、矢量圖像:SVG是W3C 推出的一種開放标準的文本式矢量圖形描述語言,他是基于XML的、專門為網絡而設計的圖像格式,

SVG是一種采用XML來描述二維圖形的語言,是以它可以直接打開xml檔案來修改和編輯。

2、位圖圖像:位圖圖像的存儲機關是圖像上每一點的像素值,因而檔案會比較大,像GIF、JPEG、PNG等都是位圖圖像格式。

Vector

在Android中指的是Vector Drawable,也就是Android中的矢量圖,

可以說Vector就是Android中的SVG實作(并不是支援全部的SVG文法,現已支援的完全足夠用了)

補充:Vector圖像剛釋出的時候,是隻支援Android 5.0+的,自從AppCompat 23.2之後,Vector可以使用于Android 2.1以上的所有系統,
            隻需要引用com.android.support:appcompat-v7:23.2.0以上的版本就可以了。(所謂的相容也是個坑爹的相容,即低版本非真實使用SVG,而是生成PNG圖檔)
           
1) Vector 文法簡介
        通過使用它的Path标簽,幾乎可以實作SVG中的其它所有标簽,雖然可能會複雜一點,
        但這些東西都是可以通過工具來完成的,是以,不用擔心寫起來會很複雜。
        (1)Path指令解析如下所示:
            M = moveto(M X,Y) :将畫筆移動到指定的坐标位置,相當于 android Path 裡的moveTo()
            L = lineto(L X,Y) :畫直線到指定的坐标位置,相當于 android Path 裡的lineTo()
            H = horizontal lineto(H X):畫水準線到指定的X坐标位置 
            V = vertical lineto(V Y):畫垂直線到指定的Y坐标位置 
            C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三次貝賽曲線 
            S = smooth curveto(S X2,Y2,ENDX,ENDY) 同樣三次貝塞爾曲線,更平滑 
            Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY):二次貝賽曲線 
            T = smooth quadratic Belzier curveto(T ENDX,ENDY):映射 同樣二次貝塞爾曲線,更平滑 
            A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧線 ,相當于arcTo()
            Z = closepath():關閉路徑(會自動繪制連結起點和終點)

            注意,’M’處理時,隻是移動了畫筆, 沒有畫任何東西。

    注意:1.關于這些文法,開發者不需要全部精通,而是能夠看懂即可,這些path标簽及資料生成都可以交給工具來實作。
        (一般美工來幫你搞定!PS、Illustrator等等都支援導出SVG圖檔)

            2.程式員:沒必要去學習使用這些設計工具,開發者可以利用一些工具,自己轉換一些比較基礎的圖像,
                如:http://inloop.github.io/svg2android/ 
            3.還可以使用SVG的編輯器來進行SVG圖像的編寫,例如:http://editor.method.ac/
            (絕配!可以先用http://editor.method.ac/ 生成SVG圖檔,然後用http://inloop.github.io/svg2android/ 生成 VectorDrawable xml代碼)
            4.使用AndroidStudio插件完成SVG添加(Vector Asset Studio)
                詳細:http://www.jianshu.com/p/d6c39f2dd5e7
                AS會自動生成相容性圖檔(高版本會生成xxx.xml的SVG圖檔;低版本會自動生成xxx.png圖檔)
            5.有些網站可以找到SVG資源
                SVG下載下傳位址: http://www.shejidaren.com/8000-flat-icons.html
                              http://www.flaticon.com/
                              
                              http://www.iconfont.cn/plus --- 阿裡巴巴
                              
                圖檔轉成SVG https://vectormagic.com/
           

 總結如下:

Android 使用svg構造互動式中國地圖概念

SVG的demo實作

類似下面的效果:

Android 使用svg構造互動式中國地圖概念

實作思路:

第一步 下載下傳含有中國地圖的 SVG

第二步 用http://inloop.github.io/svg2android/ 網站 将svg資源轉換成相應的 Android代碼

第三步 利用Xml解析SVG的代碼 封裝成javaBean 最重要的得到Path

第四步 重寫OnDraw方法 利用Path繪制中國地圖

第五步 重寫OnTouchEvent方法,記錄手指觸摸位置,判斷這個位置是否坐落在某個省份上。代碼如下:

package com.xifei.svgmapdemo;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import androidx.core.graphics.PathParser;

public class MapView extends View {
    //上下文
    private Context context;
    //畫筆
    private Paint paint;
    //适配比例
    private float scale = 1.0f;
    //矩形對象
    private RectF totalRect;
    //定義一個裝載所有省份的集合
    List<ProviceItem> itemList;
    //繪制地圖的顔色
    private int[] colorArray = new int[]{0xFF239BD7, 0xFF30A9E5, 0xFF80CBF1, 0xFFFFFFFF};
    //是否XML已經解析完畢
    private boolean isEnd = false;
    //目前選中的省份
    private ProviceItem select;


    public MapView(Context context) {
        super(context);
    }

    public MapView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    private void init(Context context) {
        this.context = context;
        paint = new Paint();
        paint.setAntiAlias(true);
        loadThread.start();

    }

    public MapView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private Thread loadThread = new Thread(new Runnable() {
        @Override
        public void run() {
            //定義一個輸入流對象去加載xml檔案
            InputStream inputStream = context.getResources().openRawResource(R.raw.china);
            //定義一個裝載所有省份的集合
            List<ProviceItem> list = new ArrayList<>();
            try {
                //擷取到解析器的工廠類
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = factory.newDocumentBuilder();
                //擷取到SVG圖形的XML對象
                Document parse = builder.parse(inputStream);
                //擷取都節點目錄
                Element documentElement = parse.getDocumentElement();
                //通過Element擷取到所有path節點的集合
                NodeList items = documentElement.getElementsByTagName("path");
                //定義四個點
                float left= -1;
                float bottom= -1;
                float top= -1;
                float right = -1;
                //周遊所有的path節點
                for(int x=0;x<items.getLength();x++){
                    //擷取到每一個path節點
                    Element element = (Element) items.item(x);
                    String pathData = element.getAttribute("android:pathData");
                    Path path = PathParser.createPathFromPathData(pathData);
                    ProviceItem proviceItem = new ProviceItem(path);
                    list.add(proviceItem);

                    //初始化每個省份的矩形
                    RectF rect = new RectF();
                    //擷取到每個省份的邊界
                    path.computeBounds(rect,true);

                    //擷取到left最小的值
                    left = left == -1?rect.left: Math.min(left,rect.left);
                    //擷取right最大值
                    right = right==-1?rect.right: Math.max(right,rect.right);
                    //周遊取出每個path中的top取所有的最小值
                    top = top == -1 ? rect.top : Math.min(top, rect.top);
                    //周遊取出每個path中的bottom取所有的最大值
                    bottom = bottom == -1 ? rect.bottom : Math.max(bottom, rect.bottom);
                }
                //封裝地圖的矩形
                totalRect = new RectF(left,top,right,bottom);
                itemList = list;
                handler.sendEmptyMessage(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });


    /**
     * 設定省份的顔色
     */
    Handler handler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            if(itemList == null || itemList.size()<=0){
                return;
            }
            int size = itemList.size();
            for(int x=0;x<size;x++){
                int color = Color.WHITE;
                int flag = x % 4;
                switch (flag){
                    case 1:
                        color = colorArray[0];
                        break;
                    case 2:
                        color = colorArray[1];
                        break;
                    case 3:
                        color = colorArray[2];
                        break;
                    default:
                        color = Color.CYAN;
                        break;
                }
                itemList.get(x).setDrawColor(color);
            }
            isEnd = true;
            measure(getMeasuredWidth(),getMeasuredHeight());
            //調用繪制
            postInvalidate();
        }
    };

    @Override
    protected void onDraw(Canvas canvas) {
        if(!isEnd){
            return;
        }
        if(itemList !=null && itemList.size()>0){
            canvas.save();
            canvas.scale(scale,scale);
            for (ProviceItem proviceItem : itemList) {
                if(select == proviceItem){
                    proviceItem.drawItem(canvas,paint,true);
                }else{
                    proviceItem.drawItem(canvas,paint,false);
                }
            }
        }
        super.onDraw(canvas);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //擷取到目前控件寬高值
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if(!isEnd){
            setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(height , MeasureSpec.EXACTLY));
            return;
        }
        if(totalRect !=null){
            //擷取到地圖的矩形的寬度
            double mapWidth = totalRect.width();
            //擷取到比例值
            scale = (float) (width/mapWidth);
        }
        setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height , MeasureSpec.EXACTLY));
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将目前手指觸摸到位置傳過去  判斷目前點選的區域
        handlerTouch(event.getX(),event.getY());
        return super.onTouchEvent(event);
    }

    /**
     * 判斷區域
     * @param x
     * @param y
     */
    private void handlerTouch(float x, float y) {
        //判空
        if(itemList ==null || itemList.size() ==0){
            return;
        }
        //定義一個空的被選中的省份
        ProviceItem selectItem =null;
        for (ProviceItem proviceItem : itemList) {
            //入股點選的是這個省份的範圍之内 就把目前省份的封裝對象繪制的方法 傳一個true
            if(proviceItem.isTouch(x/scale,y/scale)){
                selectItem = proviceItem;
            }
        }
        if(selectItem !=null){
            select = selectItem;
            postInvalidate();
        }
    }
}
           

JavaBean代碼如下:

package com.xifei.svgmapdemo;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Region;

/**
 * 省份的封裝類
 */
public class ProviceItem {
    //path對象
    private Path path;
    //繪制的顔色
    private int drawColor;

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

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

    void drawItem(Canvas canvas, Paint paint, boolean isSelect){
        if(isSelect){
            //選中的時候
            paint.clearShadowLayer();
            paint.setStrokeWidth(2);
            paint.setStyle(Paint.Style.FILL);
            paint.setColor(drawColor);
            paint.setShadowLayer(0,0,0,0xffffff);
            canvas.drawPath(path,paint);

            paint.setStyle(Paint.Style.STROKE);
            paint.setColor(Color.BLACK);
            canvas.drawPath(path,paint);
        }else{
            //未選中的時候
            paint.setStrokeWidth(1);
            paint.setStyle(Paint.Style.FILL);
            paint.setColor(drawColor);
            canvas.drawPath(path,paint);


            paint.setStyle(Paint.Style.STROKE);
            paint.setColor(Color.BLACK);
            canvas.drawPath(path,paint);
        }
    }

    public boolean isTouch(float x,float y){
        //建立一個矩形
        RectF rectF = new RectF();
        //擷取到目前省份的矩形邊界
        path.computeBounds(rectF, true);
        //建立一個區域對象
        Region region = new Region();
        //将path對象放入到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);
    }

}
           

demo下載下傳位址 : https://download.csdn.net/download/xifei66/13107152