Android 使用svg實現可縮放的地圖控件

語言: CN / TW / HK

序言

閒來無事寫了個地圖控件,基於SVG。可以縮放,可拖動,可點擊。SVG具有體積小,不失真的優點。而且由於保存的是路徑信息,可以做到複雜圖形的點擊判斷功能。還是很香的。

效果

在這裏插入圖片描述

實現

原理,SVG 意為可縮放矢量圖形(Scalable Vector Graphics)。 SVG 使用 XML 格式定義圖像。在xml中定義了路徑,只需要將路徑解析保存到path中。再繪製出來就行了。

svg地圖的獲取

使用如下地址

java String url="https://pixelmap.amcharts.com/";

下載需要的地圖在這裏插入圖片描述下載以後的地圖內容是這樣的。在這裏插入圖片描述 這種xml格式需要轉換為Android支持的格式,很簡單。new一個Vector Asset 在這裏插入圖片描述 在這裏插入圖片描述

控件實現

svg解析

轉換以後的svg圖片也只有125kb。而且怎麼放大也不會失真。svg真香。 在這裏插入圖片描述

轉換為android的svg格式以後。其中每個path保存的就是每個省的地圖數據,而其中的pathData就是具體的路徑。

在這裏插入圖片描述

svg解析是放在單獨的線程中進行的,避免造成UI卡頓,其原理就是解析XML文件。最後通過Android官方的。PathParser 將svg的路徑數據解析成對應的path。

java Path path = PathParser.createPathFromPathData(pathData); 還有一點就是定義了一個 MapItem用來保存下一級對象的路徑,是否被點擊等信息。其中的繪製功能,和判斷是否被點擊也是由該類完成。

```java 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);

}

} ```

縮放

關於縮放使用的是系統自帶的GestureDetectorScaleGestureDetector,其中GestureDetector用來實現拖動,滑動,ScaleGestureDetector用來實現雙指縮放。具體用法可以自行百度。我講一下其中需要注意的點。在SVG剛解析出來的時候需要,解析出其中的android:width 在這裏插入圖片描述 去掉其中的dp。比如上圖的1920dp去掉以後就是1920 。這個就行svg中路徑的繪製座標系中的寬度。通過它和我們控件的寬度就行縮放就可以將svg圖片完整的顯示在控件裏面。 在這裏插入圖片描述 上面的vectorWidth 就是記錄的svg中的初始寬度,在onDraw中就行計算。其中的viewScale代表的就是將svg完整展示到view中的需要的縮放比,這個值初始化以後是不會改變的。

用户手指縮放改變的是變量userScale。 用户拖動改變的是offsetX,offsetY 手指縮放的中心點用變量focusXfocusY

這些變量最後都會作用到一個matrix中。再繪製之前調用

java canvas.setMatrix(matrix); 就可以實現圖形的縮放,拖動。

invertMatrixmatrix的逆矩陣。用於將手勢的座標映射為svg中的座標。所有手勢操作之前都需要調用以下代碼進行座標轉換。

java invertMatrix.mapPoints(points);

還有一點需要注意。用户滾動和滑動都需要對距離和速度進行縮放。

在這裏插入圖片描述

源碼

一共只有319行,直接粘貼過來了。

```java 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 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.0f; 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() {
    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.0f;
        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.0f;
    offsetY=0;
    offsetX=0;
    focusX=0;
    focusY=0;
    new Thread(new DecodeRunnable()).start();
}

private class  DecodeRunnable implements Runnable {
    @Override
    public void run() {
        //Dom 解析 SVG文件

        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() {
    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.0f / 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);

}

}

```

Demo

最後想看效果的可以下載demo運行。 java String url="https://github.com/zhuguohui/MapView";

總結

做技術總是需要厚積薄發,這樣工作才能遊刃有餘。項目中雖然不需要,但是學習的腳步不能停止。提高自己解決問題的廣度和深度,才是程序員的核心價值。