地图的轮廓看起来是不规则的,不规则轮廓就是地图的路线。路线在手,说走就走。我们一起来写一个矢量地图。
先来了解矢量图形,XML 文件中定义为一组点、一组线条和一组曲线及其相关颜色信息。
- 矢量图形 svg 用 XML语言来编写,文件是纯粹的 XML
- SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失
- SVG 是W3C的标准
- 可在图像质量不下降的情况下被放大
使用矢量图形的主要优势在于图片可缩放。可以在不降低显示质量的情况下缩放图片。也就是说,可以针对不同的屏幕密度调整同一文件的大小,而不会降低图片质量。不仅能缩减 APK 文件大小,还能减少开发者维护工作。您还可以对动画使用矢量图片,使用多个 XML 文件,而不是多张图片。
Android 5.0(API 级别 21)是第一个使用矢量图像的。使用兼容看可以支持更低的版本。
绘制地图的轮廓
我们是怎么绘制地图轮廓的呢?
新建了一个属性文件 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矢量动画。
网友评论