SVG地图绘制和点击效果实现

在一个小项目中用到了地图的展示,在此做个笔记,先上一个效果图

上图用到的SVG资源是从Free SVG Maps上下载的

然后将下载好的SVG资源转成XML文件格式,转化工具地址

解析好的XML文件

在res目录下,创建raw文件夹

android 如何开发3维地图 androidstudio做一个地图_android 如何开发3维地图


将解析好的xml数据复制进去

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="-1dp"
    android:height="-1dp"
    android:viewportWidth="-1"
    android:viewportHeight="-1">
	
	//因为整个数据太长,仅展示部分 
    <path
        android:name="CN-11"
        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
        android:name="CN-50"
        android:fillColor="#CCCCCC"
        android:strokeColor="#ffffff"
        android:strokeWidth="0.5"
        android:pathData="M441.77,372.89L443.66,374.38L444.63,374.41L444.78,375L446.44,376.09L448.29,376.53L450.21,378.04L450.34,378.61L451.33,378.94L451.14,379.42L451.5,380.2L455.19,379.99L455.19,379.99L457.11,380.46L457.36,381.73L457.05,382.61L458.18,382.41L458.99,382.98L460.2,383.16L459.82,383.71L462.29,384.97L462.95,388.58L462.01,389.34L462.06,390.23L462.62,390.98L461.5,393.66L460.76,393.21L460.54,392.23L460.08,392.64L459.2,392.38L457.68,392.95L456.24,394.2L456.03,394.91L455.29,395.18L455.18,395.81L454.67,395.53L453.63,396.3L452.22,398.17L452.27,397.14L451.71,396.13L449.61,397.59L448.96,397.13L449.33,395.92L448.36,395.81L447.35,396.31L445.63,398.13L444.48,398.09L443.95,396.8L443.22,396.85L443.39,397.48L442.46,398.45L441.7,398.03L440.59,398.26L440.29,399.7L442.47,401.76L442.33,403.29L441.57,404.6L441.89,404.52L442.02,405.51L441.8,407.26L440.72,407.05L440.14,407.32L440.05,408.02L440.26,407.75L440.53,408.06L440.75,409.2L441.64,409.55L442.02,409.18L441.92,408.45L442.7,407.43L443.53,407.5L443.8,409.82L444.32,409.83L444.8,410.48L445.17,410.11L445.21,410.67L445.95,410.43L446.37,410.79L446.2,411.24L446.63,411.22L446.13,413.01L447.06,413.79L446.76,415.14L447.54,415.06L448.73,414.13L449.23,414.58L449.22,415.57L449.56,415.99L449.27,416.9L450.8,418.06L450.8,418.06L450.75,418.48L451.9,419.15L450.84,421.56L450.91,422.98L451.7,423.65L451.06,424.21L451.25,424.58L450.18,425.23L450.38,425.53L451.53,425.27L451.9,425.65L451.32,426.94L451.32,426.94L450.3,427.46L449.8,428.24L449.51,429.71L448.92,430.59L449.02,431.36L448.2,431.08L447.79,431.79L446.84,431.04L445.95,431.23L445.46,430.62L444.83,431.27L444.38,430.29L444.98,429.63L445.02,427.91L444.28,426.98L443.36,427.48L443.93,428.36L443.49,428.9L443.73,429.24L443.07,429.41L442.52,428.97L442.87,428.1L442.47,426.47L442.96,426.35L443.25,424.97L442.5,424.63L441.21,425.13L439.53,424.46L440.11,422.81L439.57,421.95L439.75,421.38L439.21,420.24L439.1,418.62L438.69,418.67L438.23,419.37L437.7,418.81L437.13,419.08L436.07,418.6L435.39,419.47L434.6,419.32L433.29,420.37L432.56,418.96L432.75,417.82L432.18,417.14L431.24,417.77L430.78,417.31L430.07,417.41L429.74,416.67L428.75,417.32L428.32,416.89L427.78,417.16L427.94,418.15L427.34,418.35L427.54,419.25L427.24,419.68L428.15,420.59L427.49,421.88L426.94,421.98L426.72,422.44L425.6,423.08L425.27,422.23L424.2,421.39L423.47,421.7L422.77,421.44L422.34,421.91L422.44,422.98L421.99,423.08L421.3,422.45L420.51,423.08L420.33,423.54L421.12,424.35L420.36,425.64L419.74,425.19L419.79,426.02L419.14,426.46L419.43,427.12L419.15,427.56L418.85,427.62L418.63,427.06L418.32,427.23L417.52,426.8L417.16,427.12L418.25,424.66L417.29,424.25L417.46,423.84L415.93,422.16L415.58,422.31L415.69,422.91L416.55,424.48L415.87,425.76L416.27,426.41L415.04,425.91L414.72,426.56L414.72,426.56L414.16,426.17L414.22,425.02L413.82,424.91L413.15,422.8L413.23,422L412.7,421.31L410.57,421.13L410.54,420.52L409.78,420.07L408.73,421.22L408.47,420.82L407.47,420.67L406.76,419.15L406.94,418.8L406.42,418.33L406.68,417.91L406.1,415.66L405.58,416.18L405.37,415.87L403.09,415.63L403.17,415.2L402.71,415.23L402.6,414.85L402.81,413.97L402.14,413.7L402.19,413.33L401.4,413.4L401,412.03L401.29,411.09L401.63,411.29L402.57,409.98L403.29,410.06L403.4,409.44L403.91,409.66L404.47,409.24L405.03,407.77L406.52,407.46L406.32,406.43L406.71,404.97L406.05,404.76L404.12,402.85L405.61,402.33L405.17,401.36L406.53,401.64L406.35,400.95L406.69,400.13L407.78,399.06L408.01,399.66L408.59,399.41L411.76,400.73L412.98,402.66L413.65,402.41L413.78,401.94L415.38,401.74L415.6,401.07L417.08,400.92L418.06,401.45L417.75,402.13L419.42,404.94L421.64,404.87L422.26,404.15L422.84,404.83L423.5,404.51L425.51,402.07L426.13,400.58L427.14,399.4L427.44,398.15L429.03,396.06L427.94,394.53L429.05,393.11L429.96,392.94L430.66,393.51L431.68,392.45L432.26,393.49L433.62,393.59L434.99,392.14L434.43,391.87L434.53,391.04L435.94,390L435.38,389.14L436.34,387.67L435.56,387.52L435.57,387.1L437.53,385.87L437.2,385.24L437.9,384.75L438.09,383.93L437.7,383.47L439.38,383.25L440.68,381.44L441.3,381.59L441.27,381.26L442.02,380.82L441.68,379.97L441.93,379.55L441.51,379.13L440.95,379.18L441.03,378.8L439.67,378.05L438.57,376.47L439.02,376.07L439.84,376.19L439.42,375.68L439.74,375.29L439.59,374.88L440.88,374.99L440.46,373.95L439.95,373.76L439.99,373.24L440.39,372.99L441.36,373.22z" />
 
</vector>

直接上代码,自定义View

关于省份、国家等名称信息,网上找不到明确的样例
这里是通过SVG里的name信息,查找省份期刊号等进行识别对应省份,世界地图以各国家的英文缩写为name进行识别对应国家

public class MapView extends View {

    private final Map<String, String> provinceId = new HashMap<String,String>(){
        {
             put("11", "北京"); put("12", "天津"); put("13", "河北");
             put("14", "山西"); put("15", "内蒙古"); put("21", "辽宁");
             put("22", "吉林"); put("23", "黑龙江"); put("31", "上海");
             put("32", "江苏"); put("33", "浙江"); put("34", "安徽");
             put("35", "福建"); put("36", "江西"); put("37", "山东");
             put("41", "河南"); put("42", "湖北"); put("43", "湖南");
             put("44", "广东"); put("45", "广西"); put("46", "海南");
             put("50", "重庆"); put("51", "四川"); put("52", "贵州");
             put("53", "云南"); put("54", "西藏"); put("61", "陕西");
             put("62", "甘肃"); put("63", "青海"); put("64", "宁夏");
             put("65", "新疆"); put("91", "澳门"); put("92","香港");
             put("71","台湾");
        }
    };
    
    private ExecutorService mThreadPool;
    private Paint mPaint;
    private int mMapResId = -1;
    private List<PathItem> mItemList;
    private RectF mMaxRect;
    //现有确诊范围 1-9,10-99,100-999,1000-9999,10000
    private int[] mColorArray = new int[] { Color.parseColor("#FFFFFF"),Color.parseColor("#cc6633"),Color.parseColor("#cc3300"), Color.parseColor("#cc0000"),Color.parseColor("#990000"),Color.parseColor("#660000") };
    private PathItem mSelectItem;
    private float mScale = 1f;
    private Paint rectPaint;

    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);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
        mPaint = new Paint();
        // 设置抗锯齿
        mPaint.setAntiAlias(true);
        // 初始化线程池
        initThreadPool();
        // 解析自定义属性
        getMapResource(context, attrs, defStyleAttr);
        rectPaint = new Paint();
        rectPaint.setStyle(Paint.Style.FILL);   // 设置样式
    }

    private void initThreadPool() {
        ThreadFactory threadFactory = new ThreadFactory() {
            @Override
            public Thread newThread(@NonNull Runnable r) {
                Thread thread = new Thread(r);
                thread.setPriority(Thread.MAX_PRIORITY);
                return thread;
            }
        };
        mThreadPool = new ThreadPoolExecutor(1, 1, 10L, TimeUnit.MINUTES,
                new LinkedBlockingQueue<Runnable>(10), threadFactory,
                new ThreadPoolExecutor.AbortPolicy());
    }

    private void getMapResource(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MapView, defStyleAttr, 0);
        int resId = a.getResourceId(R.styleable.MapView_map, -1);
        a.recycle();
        setMapResId(resId);
    }

    /**
     * 设置地图资源Id
     */
    public void setMapResId(int resId) {
        mMapResId = resId;
        executeLoad();
    }

    private void executeLoad() {
        if (mMapResId <= 0) {
            return;
        }

        mThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                // 获取xml文件输入流
                InputStream inputStream = getResources().openRawResource(mMapResId);
                // 创建解析实例
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder;
                try {
                    builder = factory.newDocumentBuilder();
                    // 解析输入流,得到Document实例
                    Document doc = builder.parse(inputStream);
                    // 获取根节点,即vector节点
                    Element rootElement = doc.getDocumentElement();
                    // 获取所有的path节点
                    NodeList items = rootElement.getElementsByTagName("path");

                    // 以下四个变量用来保存地图四个边界,用于确定缩放比例(适配屏幕)
                    float left = -1;
                    float right = -1;
                    float top = -1;
                    float bottom = -1;

                    // 解析path节点
                    List<PathItem> list = new ArrayList<>();
                    for (int i = 0; i < items.getLength(); ++i) {
                        Element element = (Element) items.item(i);
                        // 获取pathData内容
                        String pathData = element.getAttribute("android:pathData");
                        String name = element.getAttribute("android:name");
                        // 将pathData转换为path
                        Path path = PathParser.createPathFromPathData(pathData);
                        // 封装成PathItem对象
                        PathItem pathItem = new PathItem(path,getName(name));
                        pathItem.setDrawColor(mColorArray[getColor(name)]);
                        RectF rectF = new RectF();
                        // 计算当前path区域的矩形边界
                        path.computeBounds(rectF, true);
                        // 判断边界,最终获得的就是整个地图的最大矩形边界
                        left = left < 0 ? rectF.left : Math.min(left, rectF.left);
                        right = Math.max(right, rectF.right);
                        top = top < 0 ? rectF.top : Math.min(top, rectF.top);
                        bottom = Math.max(bottom, rectF.bottom);

                        list.add(pathItem);
                    }
                    // 解析完成,保存节点列表和最大边界
                    mItemList = list;
                    mMaxRect = new RectF(left, top, right, bottom);
                    // 通知重新布局和绘制
                    post(new Runnable() {
                        @Override
                        public void run() {
                            requestLayout();
                            invalidate();
                        }
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mThreadPool != null) {
            // 释放线程池
            mThreadPool.shutdown();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        if (mMaxRect != null) {
            // 获取缩放比例
            double mapWidth = mMaxRect.width();
            mScale = (float) (width / mapWidth);
        }

        // 应用测量数据
        setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mItemList != null) {
            // 使地图从画布左上角开始绘制(图片本身可能存在边距)
            canvas.translate(-mMaxRect.left, -mMaxRect.top);
            // 设置画布缩放,以(mMaxRect.left, mMaxRect.top)为基准进行缩放
            // 因为当前该点对应屏幕左上(0, 0)点
            canvas.scale(mScale, mScale, mMaxRect.left, mMaxRect.top);
            // 绘制所有省份区域,并设置是否选中状态
            for (PathItem pathItem : mItemList) {
                pathItem.drawItem(canvas, mPaint, mSelectItem == pathItem );
            }
            //现有确诊范围 1-9,10-99,100-999,1000-9999,10000
            rectPaint.setColor(mColorArray[1]);
            canvas.drawRect(20,MyApplication.dp2px(180),30,MyApplication.dp2px(180)+10,rectPaint);
            rectPaint.setColor(mColorArray[2]);
            canvas.drawRect(20, MyApplication.dp2px(190),30,MyApplication.dp2px(190)+10,rectPaint);
            rectPaint.setColor(mColorArray[3]);
            canvas.drawRect(20, MyApplication.dp2px(200),30,MyApplication.dp2px(200)+10,rectPaint);
            rectPaint.setColor(mColorArray[4]);
            canvas.drawRect(20, MyApplication.dp2px(210),30,MyApplication.dp2px(210)+10,rectPaint);
            rectPaint.setColor(mColorArray[5]);
            canvas.drawRect(20, MyApplication.dp2px(220),30,MyApplication.dp2px(220)+10,rectPaint);
            rectPaint.setColor(Color.BLACK);
            rectPaint.setTextSize(20f);
            canvas.drawText("1-9",35,MyApplication.dp2px(180)+10,rectPaint);
            canvas.drawText("10-99",35,MyApplication.dp2px(190)+10,rectPaint);
            canvas.drawText("100-999",35,MyApplication.dp2px(200)+10,rectPaint);
            canvas.drawText("1000-9999",35,MyApplication.dp2px(210)+10,rectPaint);
            canvas.drawText(">=10000",35,MyApplication.dp2px(220)+10,rectPaint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 将事件分发给所有的区块,如果事件未被消费,则调用View的onTouchEvent,这里会默认范围false
        if (handleTouch((int) (event.getX() / mScale + mMaxRect.left), (int) (event.getY() / mScale + mMaxRect.top), event)) {
            return true;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 派发触摸事件
     */
    private boolean handleTouch(int x, int y, MotionEvent event) {
        if (mItemList == null) {
            return false;
        }

        boolean isTouch = false;

        PathItem selectItem = null;
        for (PathItem pathItem : mItemList) {
            // 依次派发事件
            if (pathItem.isTouch(x, y, event)) {
                // 选中省份区块
                selectItem = pathItem;
                isTouch = true;
                break;
            }
        }

        if (selectItem != null && selectItem != mSelectItem) {
            mSelectItem = selectItem;

            // 通知重绘
            postInvalidate();
        }
        return isTouch;
    }

    private int getColor(String name){
        int num = 0;
        String[] arr = name.split("-");
        //中国省份
        if (arr.length == 2){
            Children children = RetrofitUtil.getInstance().getProvinceDetail(provinceId.get(arr[1]));
            if (children == null)
                return 0;
            num = children.getTotal().getConfirm() - children.getTotal().getDead() - children.getTotal().getHeal();
        }
        //国家
        else {
            AreaTree areaTree = RetrofitUtil.getInstance().getCountryDetail(countryId.get(name));
            if (areaTree == null)
                return 0;
            num = areaTree.getTotal().getConfirm() - areaTree.getTotal().getDead() - areaTree.getTotal().getHeal();
        }
        if (num == 0)
            return 0;
        if (num > 0 && num < 10)
            return 1;
        if (num >= 10 && num < 100)
            return 2;
        if (num >= 100 && num < 1000)
            return 3;
        if (num >= 1000 && num < 10000)
            return 4;
        if (num >= 10000)
            return 5;
        return 0;
    }

    private String getName(String name){
        String[] arr = name.split("-");
        if (arr.length == 2){
            Children children = RetrofitUtil.getInstance().getProvinceDetail(provinceId.get(arr[1]));
            if (children != null)
                return children.getName();
        }
        return "";
    }
}

初始化View

<com.example.watcher.data.MapView
      android:id="@+id/china_map"
      android:layout_width="match_parent"
      android:layout_height="300dp" />
viewBinding.chinaMap.setMapResId(R.raw.china_map);

快去试试吧!!!