Android(安卓)SVG图片解析Demo
SVG
SVG是一种图像文件格式,它的英文全称为Scalable Vector Graphics,意思为可缩放的矢量图形。它是基于XML(Extensible Markup Language),由World Wide Web Consortium(W3C)联盟进行开发的。严格来说应该是一种开放标准的矢量图形语言,可让你设计激动人心的、高分辨率的Web图形页面。用户可以直接用代码来描绘图像,可以用任何文字处理工具打开SVG图像,通过改变部分代码来使图像具有交互功能,并可以随时插入到HTML中通过浏览器来观看。
对于一些不规则的图形使用svg图片还是挺方便的,尤其涉及到交互部分。看完本文你就再也不用怕美工丢一张svg过来了。
Demo
本文通过解析SVG实现了一张中国地图,先看下效果
这就是我们要实现的效果,画出一份中国地图,并且点击某个省份时候将该省份加粗,并在下面显示出省份名。
在写代码前需要先看一个SVG文件内容:
密密麻麻的是不是觉得有点眼花,我们找一条最短的看一下:
<path id="820000" title="澳门" class="land" d="M505.56,515.13l0.35,0.51l-0.43,0.26L505.56,515.13z"/>
其实跟android布局文件差不多,只是后面的路径可能有点长而已,可以把d标签里的内容理解成路径Path。比如:
- M就是moveTo方法,将画笔移动到指定坐标。
- L -->lineTo,画直线。
- H -->horizeontal LineTo,画水平线
- V --> vertical lineTo,画垂直线
- C -->curveto,三次贝塞尔曲线
- Q -->quadratic Belzier curve,二次贝塞尔曲线
- Z --> closePath,关闭路径
其实知道个大概意思就够了,没必要把路径完全看懂。
代码
首先我们需要先封装一个省份的类,省份包含路径、颜色、是否选中、名字。别忘了把svg图片拷贝到res/raw路径下面。
import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.Path;import android.graphics.RectF;import android.graphics.Region;public class Province { //省份的path private Path path; //背景颜色 private int backgroundColor; //是否选中 private boolean isSelect = false; //省份名字 比如河南、广东 private String name; public void setName(String name) { this.name = name; } public String getName() { return name; } public void setSelect(boolean select) { isSelect = select; } public void setBackgroundColor(int backgroundColor) { this.backgroundColor = backgroundColor; } public void setPath(Path path) { this.path = path; } //绘制 public void drawProvince(Canvas canvas, Paint paint){ paint.clearShadowLayer(); paint.setStrokeWidth(1); paint.setColor(backgroundColor); paint.setShadowLayer(0,0,0,0xffffff); paint.setStyle(Paint.Style.FILL); canvas.drawPath(path,paint); paint.setStyle(Paint.Style.STROKE); paint.setColor(0xFF000000); //选中的话就加粗边界 if (isSelect){ paint.setStrokeWidth(3); } canvas.drawPath(path,paint); } //判断点击位置是否在省份的区域内 public boolean isSelect(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); }}
然后写一个自定义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.Message;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import androidx.annotation.NonNull;import androidx.annotation.Nullable;import androidx.core.graphics.PathParser;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.LinkedList;import java.util.List;import javax.xml.parsers.DocumentBuilder;import javax.xml.parsers.DocumentBuilderFactory;import javax.xml.parsers.ParserConfigurationException;public class MapView extends View { //上下文 Context context; //存放整个地图的RectF,用来计算地图宽高 private RectF mapRectF; //所有省份的集合 private List<Province> provinces; //画笔 private Paint paint; //缩放因子,因为地图宽度大概率超过屏幕宽度,所以需要缩放 private float scale = 1.0f; //异步解析图片是否完成标志位 boolean finishParse = false; //颜色数组,每个省份的颜色,记得颜色要写成不透明的(FF), private int[] colorArray = {0xFF03DAC5,0xFFE68133,0xFF5AE633,0xFFF32C5B,0xFFC820F6,0xFF4657EF,0xFFE2EA1B, 0xFFFF9800,0xFFE89872,0xFF009688}; //记录点击的省份 private Province clickProvince = null; public MapView(Context context) { this(context,null); } public MapView(Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public MapView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; init(); } private void init() { //启动一个子线程解析svg文件 parseThread.start(); //初始化画笔 paint = new Paint(); //抗锯齿 paint.setAntiAlias(true); //画笔宽度 paint.setStrokeWidth(1); } Handler handler = new Handler(){ @Override public void handleMessage(@NonNull Message msg) { //请求重新测量绘制 requestLayout(); //measure(getMeasuredWidth(),getMeasuredHeight()); invalidate(); } }; //解析svg Thread parseThread = new Thread(){ @Override public void run() { provinces = new LinkedList<>(); //打开raw目录下的china2.svg InputStream inputStream = context.getResources().openRawResource(R.raw.china2); //使用工厂模式创建一个DocumentBuilder DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); try { DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder(); //得到对应的xml对象 Document parse = builder.parse(inputStream); //得到svg文件中所有节点 Element documentElement = parse.getDocumentElement(); //获取到path节点的集合,注意这里是获取到了所有省份的path,所以需要遍历 NodeList pathList = documentElement.getElementsByTagName("path"); float left = -1; float right = -1; float top = -1; float bottom = -1; //遍历path集合 for (int i=0;i<pathList.getLength();i++){ //得到具体的一项,也就是一个省份 Element item = (Element) pathList.item(i); //得到d标签里的内容,就是path路径 String attribute = item.getAttribute("d"); //得到title内容,就是省份名字 String name = item.getAttribute("title"); //通过PathParser将得到的路径字符串转成Path对象 Path pathFromPathData = PathParser.createPathFromPathData(attribute); //new 一个省份,并设置路径,颜色,名字 Province province = new Province(); province.setPath(pathFromPathData); //从数组中取出颜色 province.setBackgroundColor(colorArray[i%(colorArray.length-1)]); province.setName(name); //添加到地图省份集合中 provinces.add(province); //得到上下左右的边界值 RectF rectF = new RectF(); pathFromPathData.computeBounds(rectF,true); left = (left==-1)?rectF.left:Math.min(rectF.left,left); right = right==-1?rectF.right:Math.max(right,rectF.right); top = top==-1?rectF.top:Math.min(top,rectF.top); bottom = bottom==-1?rectF.bottom:Math.max(bottom,rectF.bottom); } mapRectF = new RectF(left,top,right,bottom); //解析完成 finishParse = true; //回到主线程 handler.sendEmptyMessage(-1); }catch (ParserConfigurationException e){ e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); //如果测量完成 计算缩放因子 if (finishParse){ scale = width/mapRectF.width(); height = (int) mapRectF.height(); } //这里高度就当成wrap_content处理了 setMeasuredDimension(width,height); } @Override protected void onDraw(Canvas canvas) { if(provinces == null || provinces.size() < 1 || !finishParse){ return; } //保存画布 canvas.save(); //设置缩放 canvas.scale(scale,scale); //遍历省份集合 for(Province item:provinces){ //设置当前省份是否选中 if(clickProvince == item){ item.setSelect(true); }else{ item.setSelect(false); } //开始绘制 item.drawProvince(canvas,paint); } super.onDraw(canvas); } @Override public boolean onTouchEvent(MotionEvent event) { //处理点击事件 handlerTouchEvent(event.getX(),event.getY()); return true; } private void handlerTouchEvent(float x, float y) { if(!finishParse){ return; } //遍历每个省份 看点击的坐标是否在省份中 for (Province province:provinces){ if(province.isSelect(x/scale,y/scale)){ //记录下点击的省份 clickProvince = province; //重绘 加粗选中省份的边界 invalidate(); //找到点击省份 进行回调 provinceSelectListener.onProvinceSelect(clickProvince.getName()); return; } } } //省份点击事件的接口 public interface ProvinceSelectListener{ void onProvinceSelect(String name); } private ProvinceSelectListener provinceSelectListener; //设置点击事件监听 public void setProvinceSelectListener(ProvinceSelectListener provinceSelectListener){ this.provinceSelectListener = provinceSelectListener; }}
MainActivity代码贴出来:
import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;import butterknife.BindView;import butterknife.ButterKnife;public class MainActivity extends AppCompatActivity implements MapView.ProvinceSelectListener{ @BindView(R.id.tv_province) TextView tv_province; @BindView(R.id.china_map) MapView chinaMap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); //设置listener chinaMap.setProvinceSelectListener(this); } //设置点击的响应 @Override public void onProvinceSelect(String name) { tv_province.setText(name); }}
布局文件如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout 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" android:orientation="vertical" tools:context=".MainActivity"> <com.honeywell.chinasvg.MapView android:id="@+id/china_map" android:layout_width="match_parent" android:layout_height="wrap_content"/> <TextView android:id="@+id/tv_province" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="Hello World!" /></LinearLayout>
总结
其实一点也不复杂,调用系统的api就可以了,以前还以为会很复杂。基本上注释写的比较清楚了,相信大家都能看的懂。解析别的svg文件的话把解析时候的标签名换一下就行,如果需要源码的我再把源码上传。
Demo中的中国地图svg下载
更多相关文章
- Android初学者教程
- Android(安卓)NDK系列三(Android(安卓)Studio cmke 编译多个个.so
- Android(安卓)Material Design 详解(使用support v7兼容5.0以下系
- 转:关于android中图片裁剪以及PorterDuffXfermode的使用经验小结
- Android(安卓)实现颜色渐变的一个小 tip
- Android(安卓)java.lang.NoClassDefFoundError:*报错的处理
- android保存文件到SD卡中
- Android(安卓)Studio:依赖包的版本号
- Android(安卓)Studio 报错 ERROR: A problem occurred configuri