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.