The preface

I wrote a map control based on SVG. You can zoom, you can drag, you can click. SVG has the advantage of being small and realistic. And because save is the path information, can achieve the complex graph click judgment function. It still smells good.

The effect

implementation

In principle, SVG stands for Scalable Vector Graphics. SVG uses XML format to define images. The path is defined in THE XML, and you only need to save the path resolution to the PATH. I’m just going to draw it.

Get the SVG map

Use the following address

String url="https://pixelmap.amcharts.com/";
Copy the code

Download the map you needAfter downloading the map, it looks like this.This XML format needs to be converted to an Android supported format, which is simple. New a Vector Asset

Control implementation

SVG parsing

The converted SVG image is only 125KB. And how to magnify without distortion. SVG is really sweet.

After converting to the Android SVG format. Each path saves map data of each province, and pathData is the specific path.

SVG parsing is done in a separate thread to avoid UI lag by parsing XML files. Finally through Android official. PathParser parses SVG’s path data into the corresponding path.

 Path path = PathParser.createPathFromPathData(pathData);
Copy the code

The other thing is that we define a MapItem that holds the path of the next level object, whether it’s clicked, etc. This class also does the drawing and determining whether or not it is clicked.

class MapItem {
    Path path;
    private final Region region;
    private boolean isSelected = false;
    private final RectF rectF;
    private final int index;

    public boolean onTouch(float x, float y) {
        if (region.contains((int) x, (int) y)) {
            isSelected = true;
            return true;
        }
        isSelected = false;
        return false;
    }

    public MapItem(Path path, int index) {
        this.path = path;
        rectF = new RectF();
        path.computeBounds(rectF, true);
        region = new Region();
        region.setPath(path, new Region(new Rect((int) rectF.left
                , (int) rectF.top, (int) rectF.right, (int) rectF.bottom)));
        this.index = index;
    }


    protected void onDraw(Canvas canvas, Paint paint) {
        paint.reset();
        paint.setColor(isSelected ? Color.YELLOW : Color.GRAY);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawPath(path, paint);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);
        canvas.drawPath(path, paint);
        paint.setColor(Color.GRAY);
        paint.setColor(Color.BLUE);
        // canvas.drawText(index+"",rectF.centerX(),rectF.centerY(),paint);}}Copy the code

The zoom

The use of scaling is system nativeGestureDetectorandScaleGestureDetector, includingGestureDetectorYou can drag, you can slide,ScaleGestureDetectorUsed for double finger scaling. Specific usage can be baidu. Let me tell you something about that. You need to parse out SVG when it’s first parsed outandroid:width Get rid of the DP. For example, the 1920 DP in the figure above is 1920 when removed. This is the width in the drawing coordinate system of the path in SVG. It can be scaled with the width of our control to display the SVG image fully inside the control.The abovevectorWidthIs the initial width recorded in SVG, which is computed in onDraw. One of theviewScaleRepresents the scaling ratio required to fully display SVG in the view, and this value does not change after initialization.

The user’s finger scaling changes the variable userScale. The user drags to change offsetX, and offsetY pinches the center with the variables focusX and focusY

All of these variables end up being applied to a matrix. Call before redrawing

 canvas.setMatrix(matrix);
Copy the code

You can scale and drag graphics.

And invertMatrix is the inverse of matrix. Use to map the coordinates of the gesture to coordinates in SVG. All gesture operations are preceded by the following coordinate transformations called.

invertMatrix.mapPoints(points);
Copy the code

There is one other thing to note. Both user scrolling and sliding require scaling for distance and speed.

The source code

It’s only 319 lines, so I just pasted it in.

package com.trs.app.learnview.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.Scroller;

import androidx.annotation.Nullable;
import androidx.core.graphics.PathParser;

import com.trs.app.learnview.R;

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;

/** * Created by zhuguohui * Date: 2021/12/28 * Time: 10:56 * Desc: */
public class MapView extends View {
    private List<MapItem> list = new ArrayList<>();
    private Paint paint;
    private int vectorWidth = -1;
    private Matrix matrix = new Matrix();
    private Matrix invertMatrix = new Matrix();
    private float viewScale = -1f;
    private float userScale = 1.0 f;
    private boolean initFinish = false;
    private int bgColor;
    private GestureDetector gestureDetector;
    private int offsetX, offsetY;
    private Scroller scroller;
    private float[] points;
    private float[] pointsFocusBefore;
    private float focusX, focusY;
    private ScaleGestureDetector scaleGestureDetector;
    private boolean showDebugInfo = false;
    private static final int MAX_SCROLL = 10000;
    private static final int MIN_SCROLL = -10000;
    private int mapId = R.raw.ic_african;

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

    private void init(a) {
        bgColor = Color.parseColor("#f5f5f5");
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.GRAY);
        scroller = new Scroller(getContext());
        gestureDetector = new GestureDetector(getContext(), onGestureListener);
        scaleGestureDetector = new ScaleGestureDetector(getContext(), scaleGestureListener);
    }

    private ScaleGestureDetector.OnScaleGestureListener scaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {

        float lastScaleFactor;
        boolean mapPoint = false;

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float scaleFactor = detector.getScaleFactor();
            float[] points = new float[]{detector.getFocusX(), detector.getFocusY()};
            pointsFocusBefore = new float[]{detector.getFocusX(), detector.getFocusY()};
            if (mapPoint) {
                mapPoint = false;
                invertMatrix.mapPoints(points);
                focusX = points[0];
                focusY = points[1];
            }
            float change = scaleFactor - lastScaleFactor;
            lastScaleFactor = scaleFactor;
            userScale += change;
            postInvalidate();
            return false;
        }

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            lastScaleFactor = 1.0 f;
            mapPoint = true;
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {}};private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        @Override
        public void onShowPress(MotionEvent e) {}@Override
        public boolean onSingleTapUp(MotionEvent event) {
            boolean result = false;
            float x = event.getX();
            float y = event.getY();
            points = new float[]{x, y};
            invertMatrix.mapPoints(points);
            for (MapItem item : list) {
                if (item.onTouch(points[0], points[1])) {
                    result = true;
                }
            }
            postInvalidate();
            return result;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            offsetX += -distanceX / userScale;
            offsetY += -distanceY / userScale;
            postInvalidate();
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {}@Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            scroller.fling(offsetX, offsetY, (int) ((int) velocityX / userScale), (int) ((int) velocityY / userScale), MIN_SCROLL,
                    MAX_SCROLL, MIN_SCROLL, MAX_SCROLL);
            postInvalidate();
            return true; }};@Override
    public boolean onTouchEvent(MotionEvent event) {
        gestureDetector.onTouchEvent(event);
        scaleGestureDetector.onTouchEvent(event);
        return true;
    }

    public void setMapId(int mapId) {
        this.mapId = mapId;
        userScale=1.0 f;
        offsetY=0;
        offsetX=0;
        focusX=0;
        focusY=0;
        new Thread(new DecodeRunnable()).start();
    }

    private class  DecodeRunnable implements Runnable {
        @Override
        public void run(a) {
            //Dom parses SVG files

            InputStream inputStream = getContext().getResources().openRawResource(mapId);
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

            try {
                DocumentBuilder builder = factory.newDocumentBuilder();

                Document doc = builder.parse(inputStream);

                Element rootElement = doc.getDocumentElement();
                String strWidth = rootElement.getAttribute("android:width");
                vectorWidth = Integer.parseInt(strWidth.replace("dp".""));
                NodeList items = rootElement.getElementsByTagName("path");
                list.clear();
                for (int i = 1; i < items.getLength(); i++) {
                    Element element = (Element) items.item(i);
                    String pathData = element.getAttribute("android:pathData");
                    @SuppressLint("RestrictedApi")
                    Path path = PathParser.createPathFromPathData(pathData);
                    MapItem item = new MapItem(path, i);
                    list.add(item);
                }
                initFinish = true;
                postInvalidate();
            } catch(Exception e) { e.printStackTrace(); }}};@Override
    public void computeScroll(a) {
        if(scroller.computeScrollOffset()) { offsetX = scroller.getCurrX(); offsetY = scroller.getCurrY(); invalidate(); }}@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        if(vectorWidth ! = -1 && viewScale == -1) {
            int width = getWidth();
            viewScale = width * 1.0 f / vectorWidth;
        }
        if(viewScale ! = -1) {
            float scale = viewScale * userScale;
            matrix.reset();
            matrix.postTranslate(offsetX, offsetY);
            matrix.postScale(scale, scale, focusX, focusY);

            invertMatrix.reset();
            matrix.invert(invertMatrix);
        }
        canvas.setMatrix(matrix);
        canvas.drawColor(bgColor);
        if (initFinish) {
            for (MapItem item : list) {
                item.onDraw(canvas, paint);
            }
        }

        showDebugInfo(canvas);
    }

    private void showDebugInfo(Canvas canvas) {
        if(! showDebugInfo) {return;
        }
        if(points ! =null) {
            paint.setColor(Color.GREEN);
            paint.setStyle(Paint.Style.FILL);
            canvas.drawCircle(points[0], points[1].20, paint);
        }
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(focusX, focusY, 20, paint);


        if(pointsFocusBefore ! =null) {
            paint.setColor(Color.RED);
            paint.setStyle(Paint.Style.FILL);
            canvas.drawCircle(pointsFocusBefore[0], pointsFocusBefore[1].20, paint); }}}class MapItem {
    Path path;
    private final Region region;
    private boolean isSelected = false;
    private final RectF rectF;
    private final int index;

    public boolean onTouch(float x, float y) {
        if (region.contains((int) x, (int) y)) {
            isSelected = true;
            return true;
        }
        isSelected = false;
        return false;
    }

    public MapItem(Path path, int index) {
        this.path = path;
        rectF = new RectF();
        path.computeBounds(rectF, true);
        region = new Region();
        region.setPath(path, new Region(new Rect((int) rectF.left
                , (int) rectF.top, (int) rectF.right, (int) rectF.bottom)));
        this.index = index;
    }


    protected void onDraw(Canvas canvas, Paint paint) {
        paint.reset();
        paint.setColor(isSelected ? Color.YELLOW : Color.GRAY);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawPath(path, paint);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);
        canvas.drawPath(path, paint);
        paint.setColor(Color.GRAY);
        paint.setColor(Color.BLUE);
        // canvas.drawText(index+"",rectF.centerX(),rectF.centerY(),paint);}}Copy the code

Demo

Finally want to see the effect can download demo run.

String url="https://github.com/zhuguohui/MapView";
Copy the code

conclusion

It takes a lot of skill to do the job well. Although not necessary in the project, but the pace of learning can not stop. Improving the breadth and depth of your problem solving is the core value of a programmer.