实现效果如下:两张SVG图片,实现点击交互,被点击的区域显示特征色。
SVG图片点击交互
1)SVG是什么?
SVG 文件是纯粹的 XML文件。
栅格图
栅格图也称位图,它由像素点组成,每个像素点分配特定的色值和位置。我们平时生活和工作中遇到的图像大部分都是栅格图,它对图片在空间和亮度上都做了离散化。
矢量图
矢量的定义为既有大小又有方向的几何对象,那矢量图顾名思义为由矢量组成的图像。矢量图由矢量定义的直线和曲线组成,可以放在特定位置使用颜色填充,它基于数学公式计算获得,所以无论放大还是缩小都不会失真。
2)SVG优势是什么?
与其他图像格式相比,使用 SVG 的优势在于:
3)SVG工具——用以创建SVG图片
① PNG 转SVG图片:在线网站 https://www.pngtosvg.com/
上传一张平常的PNG图片,然后网站将其转换为SVG图片;
② SVG转vector标签:在线网站 https://inloop.github.io/svg2android/
通常情况下,可以直接使用一张SVG图片(标签为svg);但有时需要转为vector标签使用。
仅作了解,Android开发者使用上述两个工具即可实现正常使用。
坐标轴为以(0,0)为中心,X轴水平向右,Y轴水平向下;
所有指令大小写均可。大写绝对定位,参照全局坐标系;小写相对定位,参照父容器坐标系;
指令和数据间的空格可以省略;同一指令出现多次可以只用一个。
5)直接将SVG作为图片源需要的配置
// 增加配置信息
android {
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
}
// View中使用声明
1)下载分别代表China和USA的SVG图片;(文末资源中有给出)
https://www.amcharts.com/svg-maps/?map=china
2)将下载得到的SVG格式图片转为vector标签;(使用上面的网站)
3)自定义View继承View;
核心思路:读取SVG图片中的“pathData”信息,将其转为Android中的Path路径,最后借助Canvas绘制出来。
自定义View代码如下:
package com.seotm.coloring.china.view;
import android.annotation.SuppressLint;
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.support.v4.graphics.PathParser;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import com.seotm.coloring.china.bean.SvgItem;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
/**
* ① 不是直接使用svg图片,需要将svg图片转换为vector标签页才能使用 利用网站在线转换;
* ② svg图片本身要有足够的颜色信息,意味着初始的那张png图片必须是色彩丰富的,不能是黑白的;
*/
public class SvgView extends View {
private Context context;//上下文
private List itemList;//各省地图列表 各省地图颜色 与路径
private Paint paint; //初始化画笔
private SvgItem select; //选中的省份
private RectF totalRect;//中国地图的矩形范围
private float scale = 1.0f;//中国地图的缩放比例
private int resId;
public SvgView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SvgView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void init(Context context, int resId) {
this.context = context;
paint = new Paint();
paint.setAntiAlias(true);
itemList = new ArrayList<>();
loadThread.start();
this.resId = resId;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取当前控件的高度 让地图宽高适配当前控件
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (totalRect != null) {
double mapWidth = totalRect.width();
scale = (float) (width / mapWidth); //获取控件高度为了让地图能缩放到和控件宽高适配
}
setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
/*不是直接使用svg图片,需要将svg图片转换为vector标签页才能使用*/
private Thread loadThread = new Thread() {
@Override
public void run() {
final InputStream inputStream = context.getResources().openRawResource(resId);// 读取地图svg 把要解析的XML文档转化为输入流,以便 DOM 解析器解析它
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // 获取DocumentBuilderFactory实例,调用 DocumentBuilderFactory.newInstance() 方法得到创建 DOM 解析器的工厂
DocumentBuilder builder = null;
try {
builder = factory.newDocumentBuilder(); // 调用工厂对象的 newDocumentBuilder方法得到 DOM 解析器对象
Document doc = builder.parse(inputStream);// 解析svg的输入流 调用 DOM 解析器对象的 parse()方法解析XML文档,得到代表整个文档的Document对象,进行可以利用DOM特性对整个XML文档进行操作了
Element rootElement = doc.getDocumentElement(); // 得到 XML文档的根节点
NodeList items = rootElement.getElementsByTagName("path");
NodeList items2 = rootElement.getChildNodes(); // 得到节点的子节点
String str = rootElement.getAttribute("");
//获取地图的整个上下左右位置,
float left = -1;
float right = -1;
float top = -1;
float bottom = -1;
List list = new ArrayList<>();
for (int i = 0; i < items.getLength(); i++) {
Element element = (Element) items.item(i);
String pathData = element.getAttribute("android:pathData"); // 取得节点的属性值 可以读取path内的具体属性值
String idData = element.getAttribute("android:id"); // 具体属性值 — id
String colorData = element.getAttribute("android:fillColor"); // 具体属性值 — color
Log.d("HorseView", "run_000: " + pathData);
Log.d("HorseView", "run_111: " + colorData);
Log.d("HorseView", "run_222: " + idData);
@SuppressLint("RestrictedApi")
Path path = PathParser.createPathFromPathData(pathData);
SvgItem SvgItem = new SvgItem(path);//设置路径
SvgItem.setDrawColor(colorData);//设置颜色
//取每个省的上下左右 最后拿出最小或者最大的来充当 总地图的上下左右
RectF rect = new RectF();
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);
list.add(SvgItem);
}
itemList = list;
totalRect = new RectF(left, top, right, bottom);//设置地图的上下左右位置
//加载完以后刷新界面
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
requestLayout();
invalidate();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
};
@Override
public boolean onTouchEvent(MotionEvent event) {
handleTouch(event.getX() / scale, event.getY() / scale);
return super.onTouchEvent(event);
}
private void handleTouch(float x, float y) {
if (itemList == null) {
return;
}
SvgItem selectItem = null;
for (SvgItem SvgItem : itemList) {
if (SvgItem.isTouch(x, y)) {
selectItem = SvgItem;
}
}
if (selectItem != null) {
select = selectItem;
postInvalidate();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (itemList != null) {
canvas.save();
canvas.scale(scale, scale);//把画布缩放匹配到本控件的宽高
for (SvgItem SvgItem : itemList) {
if (SvgItem != select) {
SvgItem.drawItem(canvas, paint, false);
} else {
SvgItem.drawItem(canvas, paint, true);
}
}
}
}
}
下面开始重点解析实现的关键: DocumentBuilderFactory解析XML
① javax.xml.parsers 包中的DocumentBuilderFactory用于创建DOM模式的解析器对象 , DocumentBuilderFactory是一个抽象工厂类,它不能直接实例化,但该类提供了一个newInstance方法 ,这个方法会根据本地平台默认安装的解析器,自动创建一个工厂的对象并返回;
② 调用 DocumentBuilderFactory.newInstance() 方法得到创建 DOM 解析器的工厂;
DocumentBuilderFactory doc=DocumentBuilderFactory.newInstance();
③ 调用工厂对象的 newDocumentBuilder方法得到 DOM 解析器对象;
DocumentBuilder db=doc.newDocumentBuilder();
④ 把要解析的 XML 文档转化为输入流,以便 DOM 解析器解析它;
final InputStream inputStream = context.getResources().openRawResource(resId);
⑤ 调用 DOM 解析器对象的 parse() 方法解析 XML 文档,得到代表整个文档的 Document 对象,进行可以利用DOM特性对整个XML文档进行操作了;
Document doc = builder.parse(inputStream);
⑥ 得到 XML 文档的根节点;
Element rootElement = doc.getDocumentElement();
⑦ 得到节点的子节点;
NodeList items = rootElement.getElementsByTagName("path");
⑧ 得到子节点中的各个属性;
Element element = (Element) items.item(i);
String pathData = element.getAttribute("android:pathData");
String idData = element.getAttribute("android:id");
String colorData = element.getAttribute("android:fillColor");
⑨ 将得到的pathData转为Android的path类;
Path path = PathParser.createPathFromPathData(pathData);
至此,我们已成功从SVG图片中拿到path路径,然后借助Canvas绘制出来即可。这里有用到手指点击区域的判断,借助JavaBean实现,代码如下:
核心:一条path即为一个JavaBean,这样就可以控制各个path的绘制情况,可以绘制多个,也可以像Demo一样只绘制一个。
public class SvgItem {
private Path path;
/**
* 绘制颜色
* */
private String drawColor;
public void setDrawColor(String drawColor){
this.drawColor = drawColor;
}
public SvgItem(Path path) {
this.path = path;
}
public void drawItem(Canvas canvas, Paint paint, boolean isSelect){
if (isSelect){
//绘制内部颜色
paint.clearShadowLayer();
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.FILL);
paint.setColor(0xffffff00); // 填充的颜色为黄色
canvas.drawPath(path,paint);
//绘制边界
paint.setStyle(Paint.Style.STROKE);
paint.setColor(0xFFD0E8F4);
canvas.drawPath(path,paint);
}else {
paint.setStrokeWidth(2);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL);
paint.setShadowLayer(8,0,0,0xffffff);
canvas.drawPath(path,paint);
//绘制边界
paint.clearShadowLayer();
paint.setColor(Color.parseColor(drawColor));
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(2);
canvas.drawPath(path,paint);
}
}
/*判断手指点击区域是否落入*/
public boolean isTouch(float x,float y){
RectF rectF = new RectF();
path.computeBounds(rectF,true);
Region region = new 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);
}
}
4)Activity内使用,传入资源id即可;
public class MainActivity1 extends AppCompatActivity {
private SvgView svgView1, svgView2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_china);
svgView1 = findViewById(R.id.img1);
svgView1.init(this, R.raw.china);
svgView2 = findViewById(R.id.img2);
svgView2.init(this, R.raw.usa_low);
}
}
最后,在此自定义View提出几个延伸的方向:
① 如何实现上一步颜色绘制的保存? —— 用集合装载已经被点击的区域的JavaBean,onDraw内绘制前在做判断;
② 如何实现图片的缩放? —— 建议只采用双指缩放,避免与点击矛盾;
③ 如何绘制代表区域块的数字? —— 暂时还没有思绪,如何实现非规则path区域内的定位,还望不吝赐教。