话说,今天应该写协程相关的坑的,但是下午两个开始写,写到3点半还没有写完,然后就出去骑自行车去了。sorry啊,但是这个写笔记又是一个不能断更的工程,所以找一个简单的写,估计晚上11点开写,然后12点可以卡一个最后时间吧。
不废话了,开整。
正文
我们还是先来分析一下业务诉求,我们要自定义一个基于path 的view,然后还要自定义点击事件,那么最好的例子是什么呢?那就是中国地图,比如点击某省高亮变颜色啥的。
因为中国版图更新了海域相关位置,但是我这地图path代码还是前年的,所以就不贴效果图了。sorry啊
我们还是先按照惯例,分析一下具体实现的需要处理的逻辑。
-
中国地图的svg 中的path 绘制区域一定是和我们自定义view的高宽不一致的,所以我们需要一个缩放,而缩放就两个方向,一个canvas 进行缩放,一个是通过设置matrix 进行缩放,对单纯的缩放而言没有多少区别。
- 这个逻辑里面就涉及到了view的高宽的确定,就是 onMeasure 对吧。但是我们在这里是需要一个缩放倍数的啊,缩放倍数来源于地图的实际大小,但是我们调用postInvalidate之前,手动调用一下onMeasure重新测量,所以可以通过一些逻辑搞定onMeasure。
-
然后就是文件IO了,同时还有一个xml 解析。
-
解析出来的xml 中的path 字符串 如何转化为path 对象
-
如何基于path原生对象同时结合onMeasure 中获取到的宽高算出缩放比例。
-
如何绘制这些path
-
如何处理点击事件,及其判断用户点击的是哪个省
当我们搞清楚了这些东西之后,我们就可以直接梭哈,开始撸代码了。先说明一下:
画中国地图的爱国青年数不胜数,所以,这种类型的笔记或者blog 也到处都是,因为我也是抄别人的blog,然后自己写了一遍,然后自己再做一遍笔记,仅此而已,所以代码的重复率特别高,思路都是这些思路,只是在于先看到谁的而已。
设定并获取自定义view的宽高
既然是写demo,通常就是全屏了啊。那么代码就可以是这个样子的。我们在这里主要是为了获得缩放倍数,那么可以假定我们已经知道了地图的原始宽高,并且定义成了一个RectF。
arduino
private RectF viewRectF;
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (viewRectF!=null){
int width = MeasureSpec.getSize(widthMeasureSpec);
scale=(float)width/(viewRectF.width());
}
}
svg读取并解析
读取并不难,就是io,解析也只是xml,但是我们一个地图svg是多个path ,一个省一个path,所以我们就需要考虑,如何存储和想要存储的数据。
定义模型存储省所在的path
arduino
public class MapChildCanvas {
Path path;// 路径
int drawColor;// 绘制颜色
String id;
String cityName;
}
我们还是按照怎么简单怎么来,存储了省path,id ,名称,然后定义了一个省地图的填充颜色。
io及其解析
数据模型定义好了,那么我们就开整,把svg解析成我们需要的MapChildCanvas 列表。
先定义类成员变量:
ini
List<MapChildCanvas> childCanvas=new ArrayList<>();
int [] colors={Color.BLUE,Color.RED,Color.GREEN,Color.YELLOW,Color.BLACK};
然后是文件IO 及其解析
ini
InputStream inputStream = getResources().openRawResource(R.raw.china);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = null;
childCanvas.clear();
try {
builder = factory.newDocumentBuilder();
Document document = builder.parse(inputStream);
//获取节点
Element element = document.getDocumentElement();
NodeList pathElements = element.getElementsByTagName("path");
// 获取节点
for (int i = 0; i < pathElements.getLength(); i++) {
// 获取path 节点
Element item = (Element) pathElements.item(i);
String path = item.getAttribute("d");
String title = item.getAttribute("title");
String id = item.getAttribute("id");
Log.d(TAG, "run: " + title + " " + id);
//转换为显示 路径
Path pathData = PathParser.createPathFromPathData(path);
MapChildCanvas canvas=new MapChildCanvas(pathData);
canvas.setId(id);
canvas.setCityName(title);
canvas.setDrawColor(colors[i%colors.length]);
childCanvas.add(canvas);
}
} catch (Exception e) {
e.printStackTrace();
}
计算出svg原始的尺寸
可以看到,上面代码,我们把每个省的数据都拿到了,但是我们并没有将所有的path 所需要的矩形空间计算出来,为什么需要计算这个,因为我们要地图完整的显示出来,这也是我们计算缩放倍数的原因。
ini
builder = factory.newDocumentBuilder();
Document document = builder.parse(inputStream);
//获取节点
Element element = document.getDocumentElement();
NodeList pathElements = element.getElementsByTagName("path");
float left=-1;
float bottom=-1;
float right=-1;
float top=-1;
// 获取节点
for (int i = 0; i < pathElements.getLength(); i++) {
// 获取path 节点
Element item = (Element) pathElements.item(i);
String path = item.getAttribute("d");
String title = item.getAttribute("title");
String id = item.getAttribute("id");
Log.d(TAG, "run: " + title + " " + id);
//转换为显示 路径
Path pathData = PathParser.createPathFromPathData(path);
// 获取到路径的边界,这个主要是获取需要绘制的区域。
RectF rectF=new RectF();
pathData.computeBounds(rectF,true);
// 将值扩展到最外层 view
left=left==-1?rectF.left:Math.min(rectF.left,left);
bottom=bottom==-1?rectF.bottom:Math.max(rectF.bottom,bottom);
right=right==-1?rectF.right:Math.max(rectF.right,right);
top=top==-1?rectF.top:Math.min(rectF.top,top);
// 子类 赋值
MapChildCanvas canvas=new MapChildCanvas(pathData);
canvas.setId(id);
canvas.setCityName(title);
canvas.setDrawColor(colors[i%colors.length]);
childCanvas.add(canvas);
}
// 发送重新测量
viewRectF = new RectF(left,top,right,bottom);
measure(getMeasuredWidth(),getMeasuredHeight());
postInvalidate();
可以看到,我们将viewRectF已经创建出来了。这个属性矢量图的同学就很容易理解上面的获取代码,整体就是基于将path的上下左右的极端值作为矩形,同时svg 是严格的按照路径的绘制点,所以他的点就是真实的点,这就是具体的效果,没有中间计算环节。
开始绘制
我们知道绘制path,这个显示效果全靠画笔对象,但是我们点击了和未点击还是有点区别,而且不同的省我们定义的颜色不一样,那么画笔在Demo阶段就在画的时候配置。那么就开始绘制。因为我们上面已经有一个 childCanvas 了,所以需要遍历这个列表,遍历代码就补贴了,直接上绘制省的代码:
scss
if (select){
paint.clearShadowLayer();
paint.setStrokeWidth(2);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE);
paint.setShadowLayer(0,0,0,0xffffff);
canvas.drawPath(path,paint);
// 描边
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.BLACK);
canvas.drawPath(path,paint);
}else {
//未选中的时候
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.FILL);
paint.setColor(drawColor);
canvas.drawPath(path,paint);
// 描边
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.BLACK);
canvas.drawPath(path,paint);
}
点击处理
我们先来看计算点击的x,y 是否在path 里面的处理代码:
java
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));
//返回是否这个区域包含传进来的坐标
return region.contains((int)x,(int)y);
}
这个很单纯,直接复制即可。所以我们只需要拿到这个点击事件的event.getX(),event.getY(),然后循环刚刚的childCanvas 判断是否在区域内部就行了。
结束
代码地址。其实写到很简单,比如很多知识点也没有详细的挖掘。但是基于path去自定义出来的view 还是要比自己画要好看不少。