使用SVG打造可交互的地图控件

2017-06-14  本文已影响1666人  Veneto_2022

无图无真相,先上图

demo.gif

这个效果看上去很高大上,通过常规手段来实现的难度很大,使用SVG技术来实现非常合适。
下面分析一下实现思路:
1)不规则的区域需要使用SVG Path
2)地图控件肯定是Canvas来绘制的自定义控件
3)点击变色使用Android原生的触摸事件即可解决

地图的SVG矢量图可以通过网络找到资源,再通过处理使之变成Android系统可以使用的矢量图(本质上是把SVG的标签进行简化),下面以台湾地图为例:fillColor定义Path填充的颜色,strokeColor定义Path边界的颜色,strokeWidth定义Path边界的宽度,pathData定义Path实际的路径,在本文中也就是地图的路径,其中一个城市的Path路径如下:

path
        android:fillColor="#CCCCCC"
        android:strokeColor="#ffffff"
        android:strokeWidth="0.5"
        android:pathData="M573.31,330.07L570.81,329.19L570.81,329.19L569.19,331.78L567.43,336.57L566.91,338.87L567.08,339.84L565.09,340.51L563.01,342.21L561.38,344.46L560.72,346.75L560.25,349.77L559.08,351.52L559.03,351.58L565.55,354.4L577.39,358.16L581.64,360.42L584.68,363.43L586.13,366.31L585.37,369.2L583.52,370.26L579.45,370.95L574.88,372.83L572.17,376.22L570.4,380.67L568.44,383.05L571.93,391.75L574.96,395.25L579.68,398.76L589.24,403.27L592.25,405.9L592.87,410.03L592.59,413.09L591.97,415.79L591.2,418.26L591.97,420.54L592.72,423.64L591.75,427.3L591.03,431.27L594.24,432.05L600.28,430.39L605.63,430.05L609.36,430.42L619.23,427.92L623.16,431.05L624.19,434.52L626.8,437.05L630.36,443.18L630.86,447.01L631.47,446.58L639.77,445.8L641.99,446.4L645.4,438.18L656.39,427.05L659.34,422.48L658.89,419.04L659.95,415.32L661.77,410.22L658.01,408.53L652.3,405.28L649.14,403.02L644.78,402.27L639.92,399.36L635.91,396.26L635.46,392.85L639.09,387.74L640.56,381.67L639.32,375.96L639.04,372.02L636.36,369.95L632.94,368.26L630.89,364.68L628.22,361.93L625.21,362.18L622.21,361.61L619.74,359.17L616.47,356.79L611.32,355.15L608.61,352.24L609.5,347.63L606.68,344.37L601.31,342.61L598.09,341.08L594.7,340.1L590.77,338.57L587.64,331.75L584.01,329.31L573.31,330.07z" />
    

台湾地图的SVG文件是一个XML文件,把他放到res/raw/taiwanhigh.xml,在MainActivity中可以通过getResources().openRawResource(R.raw.taiwanhigh)获取到该XML文件,进行XML解析并通过工具类PathParser拿到里面的PathData,将路径数据作为一个成员变量赋值给自定义的地图控件TaiWan。通过for循环遍历整个XML把所有城市的数据获取到。通过定义一个City类来城市的PathData。我们需要的不仅仅是Path数据,更需要一整块PathData所围绕封闭的Region(区域),才能知道手指是否点在这个城市上面,这里要new一个Region,并使用path_svg.computeBounds(rectF, true)计算出PathData所占的区域。

private TaiWan taiWan;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        taiWan = (TaiWan) findViewById(R.id.taiwan);
        ParseSVG();
    }

    private void ParseSVG() {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            List<City> cities = new ArrayList<>();
            DocumentBuilder builder = factory.newDocumentBuilder();
            InputStream is = getResources().openRawResource(R.raw.taiwanhigh);
            Document document = builder.parse(is);
            NodeList svgNodeList = document.getElementsByTagName("path");
            for (int i = 0; i < svgNodeList.getLength(); i++) {
                Element element = (Element) svgNodeList.item(i);
                String path = element.getAttribute("android:pathData");
                City city = new City(this);
                Path path_svg = PathParser.createPathFromPathData(path);
                city.setPath(path_svg);
                RectF rectF = new RectF();
                path_svg.computeBounds(rectF, true);
                Region region = new Region();
                region.setPath(path_svg, new Region((int) (rectF.left), (int) (rectF.top), (int) (rectF.right), (int) (rectF.bottom)));
                city.setRegion(region);
                cities.add(city);
            }
            taiWan.setCities(cities);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

显而易见地是,我们自定义了Taiwan作为地图控件,并在Taiwan里有一个集合cities,cities里存放了所有城市City的PathData,在onDraw中只要绘制出PathData,就能够显示出整个台湾地图。为了使图片好看一点,我把画布在X,Y坐标上都扩大了1.2倍。 接下来只要处理好手指点击在地图上的变色效果就好了。在Touch事件里面,获取触摸点event.getX()和event.getY(),遍历所有的City并使用region.contains((int) (x / 1.2f), (int) (y / 1.2f))判断该点是否处在Region中,如果在则表示手指按在该城市上面,在City中定义了布尔值isTouch,当手指按下时为True,其他状态为False。之所以在 x 和 y都要 除以1.2f 是由于之前我把画布在X,Y坐标上都扩大了1.2倍,现在需要调整坐标系来要正确计算Region。最后别忘记调用invalidate,通知系统重绘。

public class TaiWan extends View {

    List<City> cities;

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

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

    @Override
    protected void onDraw(Canvas canvas) {
        if (null != cities && cities.size() > 0) {
            canvas.scale(1.2f, 1.2f);
            for (City city : cities) {
                city.draw(canvas);
            }
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                int x = (int) event.getX();
                int y = (int) event.getY();
                for (City city : cities) {
                    Region region = city.getRegion();
                    boolean isContain = region.contains((int) (x / 1.2f), (int) (y / 1.2f));
                    if (isContain) {
                        city.setTouch(true);
                    } else {
                        city.setTouch(false);
                    }
                }
                invalidate();
        }
        return true;
    }


    public List<City> getCities() {
        return cities;

    }

    public void setCities(List<City> cities) {
        this.cities = cities;
    }
}

City的代码如下,isTouch为True,按下时绘制实心Path并附加阴影,isTouch为False时绘制空心区域,so easy!

public class City {

    private Context context;
    private Path path;
    private Paint paint;
    private boolean isTouch;
    private Region region;

    public City(Context context) {
        this.context = context;
        paint = new Paint();
        paint.setStrokeWidth(3);
        paint.setAntiAlias(true);
    }

    public boolean isTouch() {
        return isTouch;
    }

    public void setTouch(boolean touch) {
        isTouch = touch;
    }

    public Path getPath() {
        return path;
    }

    public void setPath(Path path) {
        this.path = path;
    }

    public void draw(Canvas canvas) {
        if (isTouch()) {
            int color = getRanColor();
            paint.setColor(color);
            paint.setStyle(Paint.Style.FILL_AND_STROKE);
            paint.setShadowLayer(8, 3, 3, Color.DKGRAY);

        } else {
            paint.setColor(this.context.getResources().getColor(R.color.color1));
            paint.setStyle(Paint.Style.STROKE);
        }
        canvas.drawPath(path, paint);
    }

    private int getRanColor() {
        int[] colors = {this.context.getResources().getColor(R.color.color2),
                this.context.getResources().getColor(R.color.color3), this.context.getResources().getColor(R.color.color4)};
        return colors[(int) (Math.random() * 3)];
    }

    public Region getRegion() {
        return region;
    }

    public void setRegion(Region region) {
        this.region = region;
    }
}

完整代码可见 : https://github.com/pengzee/SVG_TW

上一篇 下一篇

猜你喜欢

热点阅读