高级UI

Android高级UI--四色填充地图

2019-12-06  本文已影响0人  谢俊烽

先上效果图


image.png

明显是一个自定义view,先解析svg资源(该资源不严谨,请勿在正规),获取每个省的path,再用四色算法设置每个省的颜色
先列举主要方法解析svg文件

 InputStream inputStream = context.getResources().openRawResource(R.raw.china);
            proviceItems = new ArrayList<>();
            try {
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = null;
                builder = factory.newDocumentBuilder();
                Document document = builder.parse(inputStream);
                Element rootElement = document.getDocumentElement();
                NodeList items = rootElement.getElementsByTagName("path");

items就是每个省份的边框了,遍历全部省份确定地图的最左最右最上最下,从而确定地图的真正宽高,然后再对比自定义View的宽度,确定画图的缩放比例,再定义自定义View的高度

           @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        if (totalRect != null && width != 0) {
            //获取到地图的矩形的宽度
            double mapWidth = totalRect.width();
            //获取到比例值
            scale = (float) (width / mapWidth);
            //用宽度重新定义高度
            heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (totalRect.height() * scale), MeasureSpec.EXACTLY);
        }
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);

    }

重写onDraw方法,把每个省依次华进去,如果有点击事件,被点击有变化的话,多数情况下都是要最后一个话

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (proviceItems != null) {
            int tatalNum = proviceItems.size();
            canvas.save();
            canvas.scale(scale, scale);
            ProviceItem selsetProviceItem = null;
            // 先画没被选中的
            for (int i = 0; i < tatalNum; i++) {
                if (!proviceItems.get(i).isSelect()) {
                    proviceItems.get(i).drawItem(canvas, paint);
                } else {
                    selsetProviceItem = proviceItems.get(i);
                }
            }
            //被选中的最后画,因为被选中的有阴影
            if (selsetProviceItem != null) {
                selsetProviceItem.drawItem(canvas, paint);
            }
        }
    }

把每个省都画到地图上的方法

 paint.setStrokeWidth(1);
        paint.setColor(drawColor);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        canvas.drawPath(path, paint);
        if (isSelect) {
            //被选择设置一下阴影
            paint.setShadowLayer(20, 0, 0, Color.WHITE);
        } else {
            //没选中去掉阴影
            paint.clearShadowLayer();
        }
        canvas.drawPath(path, paint);

接下来就是关于颜色的选择问题了,写一个获得颜色的工具类,主要参数和构造方法

    //存放颜色种类,并非真正的颜色
    private int[] colorTypes;
    //板块接壤矩阵,1为接壤
    private int[][] isBorder;
    //准备填充的颜色列表
    private int[] colors;
    //颜色多少种类
    private int TYPE_SIZE ;
    //总共有几个板块
    private int plateCount;

    public ColorFillUtil(int[][] isBorder, int[] colors) throws Exception{
        plateCount = isBorder.length;
        if (plateCount != isBorder[0].length) {//板块相邻关系必须是方阵,不能是矩阵
            throw new Exception("colors's length must be equal to isBorder's length!");
        }
        this.colors = colors;
        TYPE_SIZE = colors.length;
        this.isBorder = isBorder;
    }

思路就是从第一个省份开始慢慢尝试填充颜色,尝试方法就是从可选的颜色种类中,依次填充进去,然后再判断是否和已经填充的身份,是否有接壤并且同个颜色的,如果有就换一个颜色,如果最后每个颜色都尝试了还是不行就说明上一个板块填充有误,要回退到上个板块,如果上板块还是不行再回退,最后直到每个板块都设置好颜色,颜色种类如果小于4可能会填充失败。详细见后面代码

以下是自定义省份的been

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Region;
import android.util.Log;


/**
 * Create by XieJunFeng on 2019/12/4.
 */
public class ProviceItem {
    private int index;
    private Path path;
    //省份颜色
    private int drawColor;
    //是否被点击
    private boolean isSelect;

    public ProviceItem(Path path) {
        this.path = path;
    }

    public void setDrawColor(int drawColor) {
        this.drawColor = drawColor;
    }


    public void setIndex(int index) {
        this.index = index;
    }

    public boolean isSelect() {
        return isSelect;
    }

    public void setSelect(boolean select) {
        isSelect = select;
    }

    public void drawItem(Canvas canvas, Paint paint) {
        paint.setStrokeWidth(1);
        paint.setColor(drawColor);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        canvas.drawPath(path, paint);
        if (isSelect) {
            //被选择设置一下阴影
            paint.setShadowLayer(20, 0, 0, Color.WHITE);
        } else {
            //没选中去掉阴影
            paint.clearShadowLayer();
        }
        canvas.drawPath(path, paint);
    }

    public boolean isTouch(float x, float y) {
        //创建一个矩形
        RectF rectF = new RectF();
        //获取到当前省份的矩形边界
        path.computeBounds(rectF, true);
        //创建一个区域对象
        Region region = new Region();
        //将path对象放入到Region区域对象中
        region.setPath(path, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
        //返回是否这个区域包含传进来的坐标
        boolean resule = region.contains((int) x, (int) y);
        //无法通过代码确定两个省份是否接壤所以只能获取下标,人工构造省份相邻矩阵,如果
      //  if (result) {
      //      Log.d("ProviceItemIndex-----", index + "");
       // }
        return result;
    }

}

自定义view的代码

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * Create by XieJunFeng on 2019/12/4.
 */
public class MapView extends View {
    private Paint paint;
    private Context context;
    //整个地图所占用的矩形,在重新设配之前
    private RectF totalRect;
    private List<ProviceItem> proviceItems;
    //绘制地图的颜色
    private int[] colorArray = new int[]{0xFF1383f2, 0xFFFFDC00, 0xFFFF3D33, 0xFF4ADE8C};
    //适配比例
    private float scale = 0;
    int[] colors;
    //中国省份接壤关系矩阵,划分34个省份,自治区,市和特别行政区等,但是多一个颜色表示国外的颜色项目中最后没有用到
    int[][] isBorder = {
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1},
            {0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},

            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1},

            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},

            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},

            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0},

            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},

            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},


    };

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

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


    public MapView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private void init(Context context) {
        this.context = context;
        paint = new Paint();
        paint.setAntiAlias(true);
        //开线程解析数据
        loadThread.start();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        if (totalRect != null && width != 0) {
            //获取到地图的矩形的宽度
            double mapWidth = totalRect.width();
            //获取到比例值
            scale = (float) (width / mapWidth);
            //用宽度重新定义高度
            heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (totalRect.height() * scale), MeasureSpec.EXACTLY);
        }
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        //如果省份数据还没加载出来,实际缩放比例没定义出来啥都不用干
        if (proviceItems != null||scale==0) {
            super.onDraw(canvas);
            int tatalNum = proviceItems.size();
            canvas.save();
            canvas.scale(scale, scale);
            ProviceItem selsetProviceItem = null;
            // 先画没被选中的
            for (int i = 0; i < tatalNum; i++) {
                if (!proviceItems.get(i).isSelect()) {
                    proviceItems.get(i).drawItem(canvas, paint);
                } else {
                    selsetProviceItem = proviceItems.get(i);
                }
            }
            //被选中的最后画,因为被选中的有阴影
            if (selsetProviceItem != null) {
                selsetProviceItem.drawItem(canvas, paint);
            }
        }
    }

    private Thread loadThread = new Thread(new Runnable() {
        @Override
        public void run() {
            InputStream inputStream = context.getResources().openRawResource(R.raw.china);
            proviceItems = new ArrayList<>();
            try {
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = null;
                builder = factory.newDocumentBuilder();
                Document document = builder.parse(inputStream);
                Element rootElement = document.getDocumentElement();
                NodeList items = rootElement.getElementsByTagName("path");
                //定义一个不可能存在屏幕上的很右边的点Integer.MAX_VALUE作为最左边,同理定义一个不能存在屏幕上的很左边的点-1作为为最左边
                //因为循环每个省份的最左最右最上最下,左边下标只会越来越小
                float left = Integer.MAX_VALUE;
                float right = -1;
                float top = Integer.MAX_VALUE;
                float bottom = -1;
                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);
                    ProviceItem proviceItem = new ProviceItem(path);
                    //设置省份下标
                    //proviceItem.setIndex(i);
                    proviceItems.add(proviceItem);
                    RectF rectF = new RectF();
                    path.computeBounds(rectF, true);
                    left = Math.min(left, rectF.left);
                    right = Math.max(right, rectF.right);
                    top = Math.min(top, rectF.top);
                    bottom = Math.max(bottom, rectF.bottom);
                }
                //创建整个地图
                totalRect = new RectF(left, top, right, bottom);
                try {
                    if (colors == null) {
                        colors = new ColorFillUtil(isBorder, colorArray).getColors();
                        int totalNumber = proviceItems.size();
                        for (int i = 0; i < totalNumber; i++) {
                            proviceItems.get(i).setDrawColor(colors[i]);
                        }
                        handler.sendEmptyMessage(0);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

            } catch (ParserConfigurationException e) {
                e.printStackTrace();
            } catch (SAXException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });


    private Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            //返回主线程调用以下方法
            //重新测量,调用onMeasure
            requestLayout();
            //重新绘图,系统调用onDraw
            invalidate();
        }
    };

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将当前手指触摸到位置传过去  判断当前点击的区域
        handlerTouch(event.getX(), event.getY());
        return super.onTouchEvent(event);
    }

    /**
     * 判断区域
     *
     * @param x
     * @param y
     */
    private void handlerTouch(float x, float y) {
        //判空
        if (proviceItems == null || proviceItems.size() == 0) {
            return;
        }
        for (ProviceItem proviceItem : proviceItems) {
            //入股点击的是这个省份的范围之内 就把当前省份的封装对象绘制的方法 传一个true
            proviceItem.setSelect(proviceItem.isTouch(x / scale, y / scale));
        }
        postInvalidate();
    }

颜色选择工具类

package com.dxt.mapapplication;

/**
 * 板块颜色填充工具
 * Create by XieJunFeng on 2019/12/6.
 */
public class ColorFillUtil {
    //存放颜色种类,并非真正的颜色
    private int[] colorTypes;
    //板块接壤矩阵,1为接壤
    private int[][] isBorder;
    //准备填充的颜色列表
    private int[] colors;
    //颜色多少种类
    private int TYPE_SIZE ;
    //总共有几个板块
    private int plateCount;

    public ColorFillUtil(int[][] isBorder, int[] colors) throws Exception{
        plateCount = isBorder.length;
        if (plateCount != isBorder[0].length) {//板块相邻关系必须是方阵,不能是矩阵
            throw new Exception("colors's length must be equal to isBorder's length!");
        }
        this.colors = colors;
        TYPE_SIZE = colors.length;
        this.isBorder = isBorder;
    }


    /**
     * 获取最后的结果
     * @return
     */
    public int[] getColors()  {

        colorTypes = new int[plateCount];
        int index = 0;
        int colorType = 0;
        while (index < plateCount) {

            if (setColor(index, colorType)) {
                //设置颜色种类暂时成功,接着下一个,直到全部颜色设置完成,设置颜色种类从0开始尝试
                index++;
                colorType = 0;
            } else {
                //找不到合适的颜色要回退,上一个板块修改颜色
                index--;
                colorType = colorTypes[index] + 1;
                if (index == 0)
                    //无法求解,可能是是颜色种类太少
                    return null;
            }

        }
        return getRealColors();
    }

    /**
     * 返回真正的颜色列表
     * @return
     */
    private int[] getRealColors() {
        int[] result = new int[plateCount];
        for (int i = 0; i < plateCount; i++) {
            result[i] = colors[colorTypes[i]];
        }
        return result;
    }

    /**
     * 尝试填充颜色 填充成功返回true
     * @param index 准备填充的板块下标
     * @param colorType 准备填充的颜色种类
     * @return
     */
    private boolean setColor(int index, int colorType) {
        if (colorType >= TYPE_SIZE) return false;
        while (colorType < TYPE_SIZE) {
            //是否可以设置颜色种类
            boolean canSet = true;
            //循环判断准备填充的颜色与之前的颜色是否冲突
            for (int i = 0; i < index; i++) {
                //isBorder[i][index] == 1 表示之前已经填充的第i个板块和准备填充的板块是接壤的
                //colorType == colorTypes[i] 同时准备填充的颜色种类又是一样的,则准备填充的颜色要改变,再重新尝试填充
                if (isBorder[i][index] == 1 && colorType == colorTypes[i]) {

                    ++colorType;
                    canSet = false;
                    break;
                }
            }
            if (canSet) {
                colorTypes[index] = colorType;
                return true;
            }
        }
        //找不到合适的颜色,要回退
        return false;
    }
}

部分资源文件
https://pan.baidu.com/s/1Tgq84epnaFhmiEBotGeBgw

上一篇下一篇

猜你喜欢

热点阅读