Android实现四色填充地图功能
先上效果图:

明显是一个自定义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的高度
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方法,把每个省依次华进去,如果有点击事件,被点击有变化的话,多数情况下都是要最后一个话
@Overrideprotected 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
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的代码
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();}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);}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() {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()) {public void handleMessage(@NonNull Message msg) {//返回主线程调用以下方法//重新测量,调用onMeasurerequestLayout();//重新绘图,系统调用onDrawinvalidate();}};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) {//入股点击的是这个省份的范围之内 就把当前省份的封装对象绘制的方法 传一个trueproviceItem.setSelect(proviceItem.isTouch(x / scale, y / scale));}postInvalidate();}
颜色选择工具类
/*** 板块颜色填充工具*/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
评论
