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

无图无真相,先上图

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容

  • 使用SVG打造一个可以交互的地图 首先还是看看效果 交互感觉好像简单,但是这个地图怎么绘制呢? 思路:利用Xml解...
    laer_L阅读 10,236评论 5 11
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,077评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,094评论 4 62
  • 这个社会是个爆炸的时代,非常多的信息,极其多的信息通过手机,电脑等媒介进行传播,其中不乏一些能满足人们空虚的心灵,...
    neurall阅读 440评论 0 1
  • 在未来新世界,机器已经替代了绝大多数工作。因此很多人都不能再去工作,而无所事事,政府为了避免这么多无所事事的人会产...
    老祝读书阅读 316评论 0 0