先上效果图
明显是一个自定义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;
}
}