美文网首页
Android 我们的矢量地图,放大不失真

Android 我们的矢量地图,放大不失真

作者: as_pixar | 来源:发表于2020-04-28 23:00 被阅读0次

    地图的轮廓看起来是不规则的,不规则轮廓就是地图的路线。路线在手,说走就走。我们一起来写一个矢量地图。

    先来了解矢量图形,XML 文件中定义为一组点、一组线条和一组曲线及其相关颜色信息。

    • 矢量图形 svg 用 XML语言来编写,文件是纯粹的 XML
    • SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失
    • SVG 是W3C的标准
    • 可在图像质量不下降的情况下被放大

    使用矢量图形的主要优势在于图片可缩放。可以在不降低显示质量的情况下缩放图片。也就是说,可以针对不同的屏幕密度调整同一文件的大小,而不会降低图片质量。不仅能缩减 APK 文件大小,还能减少开发者维护工作。您还可以对动画使用矢量图片,使用多个 XML 文件,而不是多张图片。

    Android 5.0(API 级别 21)是第一个使用矢量图像的。使用兼容看可以支持更低的版本。

    下载svg 地图网站

    绘制地图的轮廓

    我们是怎么绘制地图轮廓的呢?

    新建了一个属性文件 attrs.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <!--    是否仅仅绘制轮廓-->
        <attr name="isOnlyProfile" format="boolean" />
    
        <declare-styleable name="MapView">
            <attr name="isOnlyProfile" />
        </declare-styleable>
    
    </resources>
    

    我们在布局main布局文件中 activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <com.wwj.ourmap.MapView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:isOnlyProfile="true" />
    
    </RelativeLayout>
    

    在MapView.java文件中,获取是否仅仅显示轮廓的属性值

     /**
         * 是否仅仅绘制轮廓
         */
        private boolean isOnlyProfile;
    
        public MapView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public MapView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context, attrs);
        }
    
    
        private void init(Context context, AttributeSet attrs) {
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MapView);
            isOnlyProfile = typedArray.getBoolean(R.styleable.MapView_isOnlyProfile, false);
    
    
            typedArray.recycle();
    
            this.mContext = context;
            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            loadPathData();
        }
    

    我们接下来获取控件的宽度,我们以屏幕宽度为主

    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //        map 的宽度  和高度
            if (mMapRect != null) {
                double mapWidth = mMapRect.width();
    
                //控件宽度
                int width = MeasureSpec.getSize(widthMeasureSpec);
                int height = MeasureSpec.getSize(heightMeasureSpec);
    
                int finalWidth = Math.min(width, height);
    
                //计算控件宽度和地图宽度的比例  比如
                //以屏幕宽度为主
                mScale = (float) (finalWidth / mapWidth);
                Log.d("tag", "-------scale=" + mScale);
            }
        }
    

    这部分代码很简单

    我们的地图路线是怎么生成的呢?

    地图路线是美工或者UI设计师已经画好了的,是一个china.svg 文件,用一个xml文件描述,比如我们画一个圆圈,用svg怎么做呢?
    svg 菜鸟教程
    有很多的学习教程

    <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
       <circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
    </svg> 
    

    加载地图路线数据

    /**
         * 解析svg 矢量图数据
         */
        private void loadPathData() {
            //获取矢量图输入流
            InputStream inputStream = mContext.getResources().openRawResource(R.raw.china);
            List<ProvinceItem> list = new ArrayList<>();
            try {
                //文档施工队工厂
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                //文档施工队的某位师傅
                DocumentBuilder builder = factory.newDocumentBuilder();
                //w3c 的Document
                Document document = builder.parse(inputStream);
    
                //文档中的根元素
                Element rootElement = document.getDocumentElement();
    
                //获取名称为path 的节点列表
                NodeList items = rootElement.getElementsByTagName("path");
    //                中国地图的  矩形
                float left = -1;
                float right = -1;
                float top = -1;
                float bottom = -1;
                RectF rect = new RectF();
                for (int i = 0; i < items.getLength(); i++) {
                    //获取一个节点
                    Element element = (Element) items.item(i);
    
                    //获取节点中的值
                    String pathData = element.getAttribute("android:pathData");
    
                    //地图路径
                    Path path = PathParser.createPathFromPathData(pathData);
    
                    //要绘制的地图颜色,随机生成要绘制的地图颜色
                    int color;
                    int flag = i % 4;
                    switch (flag) {
                        case 1:
                            color = ContextCompat.getColor(mContext, R.color.deep_sky_blue);
                            break;
                        case 2:
                            color = ContextCompat.getColor(mContext, R.color.cerulean_blue);
                            break;
                        case 3:
                            color = ContextCompat.getColor(mContext, R.color.light_sky_blue);
                            break;
                        default:
                            color = ContextCompat.getColor(mContext, R.color.cyan);
                            break;
                    }
                    // 一条省份路线数据
                    ProvinceItem provinceItem = new ProvinceItem(path, color, mContext);
                    list.add(provinceItem);
    
                    //获取宽高
                    path.computeBounds(rect, true);
    
                    //获取地图大小,也就是地图的边界
                    left = left == -1 ? rect.left : Math.min(left, rect.left);
                    right = right == -1 ? rect.right : Math.max(right, rect.right);
                    top = top == -1 ? rect.top : Math.min(top, rect.top);
                    bottom = bottom == -1 ? rect.bottom : Math.max(bottom, rect.bottom);
                    mMapRect.set(left, top, right, bottom);
                }
                mProvinceItemList = list;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    获取矢量图输入流,创建了一个文档施工队工厂,创建一个文档施工队师傅解析矢量图输入流,接着获取文档根元素,获取根元素中的path节点列表,通过android:pathData获取每个节点值。这个值就是一个省的路线。我们通过PathParser类解析为一个android的Path对象 Path path = PathParser.createPathFromPathData(pathData); 有了路线,说走就走,接下来我们看看怎么绘制地图轮廓。 PathParse类在android.util下,我们通过Everything搜索某个类。

    @Override
        protected void onDraw(Canvas canvas) {
            if (mProvinceItemList != null) {
                canvas.save();
                canvas.scale(mScale, mScale);
                for (ProvinceItem provinceItem : mProvinceItemList) {
                    if (provinceItem != this.mSelectProvinceItem) {
                        provinceItem.drawItem(canvas, mPaint, false,isOnlyProfile);
                    } else {
                        provinceItem.drawItem(canvas, mPaint, true,isOnlyProfile);
                    }
                }
            }
        }
    
    /**
         * 绘制某一个省的地图路线
         *
         * @param canvas
         * @param paint
         * @param isSelect      是否选中
         * @param isOnlyProfile 是否仅仅绘制轮廓
         */
        void drawItem(Canvas canvas, Paint paint, boolean isSelect, boolean isOnlyProfile) {
            if (isSelect) {
                //清除阴影层
                paint.clearShadowLayer();
    
                //设置边界
                paint.setStyle(Paint.Style.STROKE);
                paint.setStrokeWidth(2);
                paint.setColor(Color.MAGENTA);
                paint.setShadowLayer(6, 0, 0, Color.BLUE);
    //            radius:模糊半径,radius越大越模糊,越小越清晰,但是如果radius设置为0,则阴影消失不见
    //            dx:阴影的横向偏移距离,正值向右偏移,负值向左偏移
    //            dy:阴影的纵向偏移距离,正值向下偏移,负值向上偏移
    //            color: 绘制阴影的画笔颜色,即阴影的颜色(对图片阴影无效)
    
                canvas.drawPath(mPath, paint);
    
                if (isOnlyProfile) {
                    return;
                }
    
                //选中时,绘制描边效果
                paint.setStyle(Paint.Style.FILL);
                paint.setColor(mDrawColor);
                paint.setStrokeWidth(1);
                canvas.drawPath(mPath, paint);
            } else {
                //设置边界
                paint.setStyle(Paint.Style.STROKE);
                paint.setStrokeWidth(1);
                paint.setColor(Color.BLACK);
                paint.setShadowLayer(8, 0, 0, Color.WHITE);
                canvas.drawPath(mPath, paint);
    
                if (isOnlyProfile) {
                    return;
                }
                //后面是填充
                paint.clearShadowLayer();
                paint.setColor(mDrawColor);
                paint.setStyle(Paint.Style.FILL);
                paint.setStrokeWidth(2);
                canvas.drawPath(mPath, paint);
            }
        }
    

    onDraw方法中通过遍历每一个省份,绘制每一个省的路线图。isOnlyProfile 如果为真仅仅绘制轮廓,绘制轮廓主要就是canvas.drawPath()方法,第一个参数传递的是要绘制路线,第二个是画笔。就这样我们就绘制了地图轮廓图。如果仅仅绘制轮廓为假,绘制五彩斑斓的地图。

    我们点击地图,会有选中效果,这部分是怎么做的呢?

    判断点击哪个省份

     @Override
        public boolean onTouchEvent(MotionEvent event) {
            handleTouch(event.getX(), event.getY());
            return super.onTouchEvent(event);
        }
    
        /**
         * 点击某个省份进行绘制
         *
         * @param x 点击x坐标
         * @param y 点击y坐标
         */
        private void handleTouch(float x, float y) {
            if (mProvinceItemList == null) {
                return;
            }
            for (ProvinceItem provinceItem : mProvinceItemList) {
                if (provinceItem.isTouch(x / mScale, y / mScale)) {
                    mSelectProvinceItem = provinceItem;
                    postInvalidate();
                    break;
                }
            }
        }
    
    
    /**
         * true 包含点击的x y 左边
         *
         * @param x 坐标
         * @param y 坐标
         * @return
         */
        public boolean isTouch(float x, float y) {
            RectF rectF = new RectF();
            /**
             * 获取路径的举行
             */
            mPath.computeBounds(rectF, true);
    //        rectF   矩形  包含了Path
            Region region = new Region();
    
            //设置路径范围
            region.setPath(mPath, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
            //x y 是否在这个坐标中
            return region.contains((int) x, (int) y);
        }
    

    在onTouchEvent方法中获取手指点击屏幕的x,y坐标,判断是在哪个省份中。 region.setPath是取路线和矩形的较劲,最后判断是否在这个区域中。

    下一节我们来聊聊svg矢量动画。

    项目下载地址

    相关文章

      网友评论

          本文标题:Android 我们的矢量地图,放大不失真

          本文链接:https://www.haomeiwen.com/subject/gawbwhtx.html