1.前言
SVG,即Scalable Vector Graphics 可伸缩矢量图形。这种图像格式在前端中已经使用的非常广泛了,而在移动端的开发中,遇到一些复杂的自定义控件或者动画效果,我们就可以考虑让美工出套SVG图,再按照固定的套路去解析即可。
2.Vector Drawable
2.1 矢量图与位图
先介绍下矢量图像和位图图像的区别
1.矢量图像:SVG是W3C 推出的一种开放标准的文本式矢量图形描述语言,他是基于XML的专门为网络而设计的图像格式
SVG是一种采用XML来描述二维图形的语言,所以它可以直接打开xml文件来修改和编辑。
2.位图图像:位图图像的存储单位是图像上每一点的像素值,因而文件会比较大,像GIF、JPEG、PNG等都是位图图像格式。
也就是说,如果使用矢量图,就不需要针对不同dpi的设备展示不同精度的图片了,是不是很方便啊?
2.2 Vector Drawable简介
在Andoird中,SVG的实现方式就是Vector Drawable。这是个在5.0时增加的新类,所以对之前版本的兼容会有些问题,之后会单独拎出来讲。
相对于普通的Drawable来说,Vector Drawable有以下几个好处:
(1)Vector图像可以自动进行适配,不需要通过分辨率来设置不同的图片。
(2)Vector图像可以大幅减少图像的体积,同样一张图,用Vector来实现,可能只有PNG的几十分之一。
(3)使用简单,很多设计工具,都可以直接导出SVG图像,从而转换成Vector图像 功能强大。
(4)不用写很多代码就可以实现非常复杂的动画 成熟、稳定,前端已经非常广泛的进行使用了。
2.3 Vector Drawable基本语法
Vector Drawable实际上是一个XML文件,咱们先来看一个vector的例子
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="400dp"
android:height="400dp"
android:viewportHeight="400"
android:viewportWidth="400">
<path
android:pathData="M 100 100 L 300 100 L 200 300 z"
android:strokeColor="#000000"
android:strokeWidth="5"
android:fillColor="#FF0000"
/>
</vector>
这个vector画了一个三角形,对照着上面的代码,咱们来学习Vector Drawable的基本语法。首先说明一下,这些语法开发者不需要全部精通,只要能够看懂即可,这些path标签及数据生成都可以交给工具来实现。
2.3.1 pathData标签
先看pathData
标签,这里定义了vector中path的绘制,也是最重要的一部分。语法如下,注意,’M’处理时,只是移动了画笔, 没有画任何东西。
M = moveto(M X,Y) :将画笔移动到指定的坐标位置,相当于 android Path 里的moveTo()
L = lineto(L X,Y) :画直线到指定的坐标位置,相当于 android Path 里的lineTo()
H = horizontal lineto(H X):画水平线到指定的X坐标位置
V = vertical lineto(V Y):画垂直线到指定的Y坐标位置
C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三次贝赛曲线
S = smooth curveto(S X2,Y2,ENDX,ENDY) 同样三次贝塞尔曲线,更平滑
Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY):二次贝赛曲线
T = smooth quadratic Belzier curveto(T ENDX,ENDY):映射 同样二次贝塞尔曲线,更平滑
A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧线 ,相当于arcTo()
Z = closepath():关闭路径(会自动绘制链接起点和终点)
2.3.2 path标签
接着看下path
标签的内容。稍微有个印象即可,需要时再对照着去理解。
android:name 定义该 path 的名字,这样在其他地方可以通过名字来引用这个路径
android:pathData 和 SVG 中 d 元素一样的路径信息。
android:fillColor 定义填充路径的颜色,如果没有定义则不填充路径
android:strokeColor 定义如何绘制路径边框,如果没有定义则不显示边框
android:strokeWidth 定义路径边框的粗细尺寸
android:strokeAlpha 定义路径边框的透明度
android:fillAlpha 定义填充路径颜色的透明度
android:trimPathStart 从路径起始位置截断路径的比率,取值范围从 0 到1
android:trimPathEnd 从路径结束位置截断路径的比率,取值范围从 0 到1
android:trimPathOffset 设置路径截取的范围
android:strokeLineCap 设置路径线帽的形状,取值为 butt, round, square.
android:strokeLineJoin 设置路径交界处的连接方式,取值为 miter,round,bevel.
android:strokeMiterLimit 设置斜角的上限
2.3.4 vector标签
根元素 vector
标签是用来定义这个矢量图的,该元素包含如下属性:
android:name 定义该drawable的名字
android:width 定义该 drawable 的内部(intrinsic)宽度,支持所有 Android 系统支持的尺寸,通常使用 dp
android:height 定义该 drawable 的内部(intrinsic)高度,支持所有 Android 系统支持的尺寸,通常使用 dp
android:viewportWidth 定义矢量图视图的宽度,视图就是矢量图 path 路径数据所绘制的虚拟画布
android:viewportHeight 定义矢量图视图的高度,视图就是矢量图 path 路径数据所绘制的虚拟画布
android:tint 定义该 drawable 的 tint 颜色。默认是没有 tint 颜色的
android:tintMode 定义 tint 颜色的 Porter-Duff blending 模式,默认值为 src_in
android:autoMirrored 设置当系统为 RTL (right-to-left) 布局的时候,是否自动镜像该图片。比如 阿拉伯语。
android:alpha 该图片的透明度属性
2.3.5 group标签
有时候我们需要对几个路径一起处理,这样就可以使用 group 元素来把多个 path 放到一起。 group 支持的属性如下:
android:name 定义 group 的名字
android:rotation 定义该 group 的路径旋转多少度
android:pivotX 定义缩放和旋转该 group 时候的 X 参考点。该值相对于 vector 的 viewport 值来指定的。
android:pivotY 定义缩放和旋转该 group 时候的 Y 参考点。该值相对于 vector 的 viewport 值来指定的。
android:scaleX 定义 X 轴的缩放倍数
android:scaleY 定义 Y 轴的缩放倍数
android:translateX 定义移动 X 轴的位移。相对于 vector 的 viewport 值来指定的。
android:translateY 定义移动 Y 轴的位移。相对于 vector 的 viewport 值来指定的。
通过上面的属性可以看出, group 主要是用来设置路径做动画的关键属性的。
2.4 一些常用的工具
上面的这些语法只要能看懂就可以了。我们会用一些成熟的工具来辅助SVG在移动端的开发。
1.先说美工这个最好的工具,SVG图一般直接让美工来帮你搞定就行了!像PS、Illustrator等等都支持导出SVG图片
2.获取到SVG后,我们要将其转换为vector drawable对象,svg2android这个网站可以帮你轻松完成。
3.如果没有SVG图片怎么办?可以使用SVG的编辑器来进行SVG图像的创作和编写。
4.获取到资源后,使用AndroidStudio插件完成SVG添加,AS会自动生成兼容性图片(高版本会生成xxx.xml的SVG图片;低版本会自动生成xxx.png图片)。具体过程看Vector Asset Studio的使用
5.最后介绍几个可以获取SVG资源的网站
http://www.shejidaren.com/8000-flat-icons.html
http://www.flaticon.com/
http://www.iconfont.cn/plus
2.5 适配中的一些坑
在正式开始撸代码前,先解决适配问题。
由于vector drawable是5.0之后才出来的东西,所以我们需要对之前的版本进行兼容。假设大家都使用Android Studio 2.2以上的版本,并且gradle版本在2.0以上(应该没有原始人吧)。下面是配置的步骤:
1.1、添加
· defaultConfig {
vectorDrawables.useSupportLibrary = true
}
1.2、添加
compile 'com.android.support:appcompat-v7:25.3.1' //需要是23.2 版本以上的
1.3、Activity需要继承与AppCompatActivity
1.4、布局文件当中添加
xmlns:app="http://schemas.android.com/apk/res-auto"
1.5、使用在Actvity前面添加一个flag设置
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
Vector Drawable可以理解为一张图片,所以能设置到其他的控件之中。
1 ImageView、ImageButton
XML app:srcCompat(5.0以上可以直接使用background)
代码里面使用无区别,直接setBackground即可。
2. Button
不支持app:srcCompat
Xm使用在Button的selector中
3. RadioButton
直接使用
4. textview的drawable
直接使用
3.Vector Drawable的使用
3.1 Vector Drawable静态使用
结合上文内容,去阿里svg平台随便找张svg的图片,既可以通过svg2android也可以通过AS自带的插件将其转化为vector drawable,接着配置项目兼容环境,再将这个vector在资源xml中用app:srcCompat赋值给ImageView,最后的结果就是这样,无论怎样放大都不会失真。
如果你在自己的安卓机上也实现了这样的效果,恭喜!关于Vector Drawable最基本的静态使用已经被你掌控了!
3.2 Vector Drawable动态使用
大声告诉我,android中有几种动画的实现方式?除了帧、补间、属性动画以外,vector drawable也可以用来完成动画效果,还记得之前讲的path标签吗,这里面的属性都可以作为动画的变化条件,我们再展示一下:
android:name 定义该 path 的名字,这样在其他地方可以通过名字来引用这个路径
android:pathData 和 SVG 中 d 元素一样的路径信息。
android:fillColor 定义填充路径的颜色,如果没有定义则不填充路径
android:strokeColor 定义如何绘制路径边框,如果没有定义则不显示边框
android:strokeWidth 定义路径边框的粗细尺寸
android:strokeAlpha 定义路径边框的透明度
android:fillAlpha 定义填充路径颜色的透明度
android:trimPathStart 从路径起始位置截断路径的比率,取值范围从 0 到1
android:trimPathEnd 从路径结束位置截断路径的比率,取值范围从 0 到1
android:trimPathOffset 设置路径截取的范围
android:strokeLineCap 设置路径线帽的形状,取值为 butt, round, square.
android:strokeLineJoin 设置路径交界处的连接方式,取值为 miter,round,bevel.
android:strokeMiterLimit 设置斜角的上限
剩下就是满满的套路了。
首先,获取到一张vector图片,比如这次使用的是一个对勾。我们给path标签附上了name属性,这是为了之后在动画中找到这条path。
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:name="path_check"
android:fillColor="#FF000000"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>
接着,用动画vector包装原来的vector图片,其创建方式和vector相似,只不过最外层的标签为animated-vector。我们还要为target标签赋值,name属性是前面命名的、vector中需要变化的地方,而animation自然就是属性动画了。
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_done_black_24dp">
<target
android:name="path_check"
android:animation="@animator/check_animator"/>
</animated-vector>
归根结底还是需要用到属性动画,我们通过xml的方式来完成它。注意要在res下创建animator文件夹,再将xml放入其中。这里变化的属性是path标签中trimPathEnd属性。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="500"
android:propertyName="trimPathEnd"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"/>
</set>
最后,只要在代码中将Drawable转化为Animatable,并调用其start()
方法开启动画即可。
<ImageView
android:id="@+id/iv"
android:layout_width="240dp"
android:layout_height="240dp"
app:srcCompat="@drawable/check_animator"
/>
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Animatable animatable = (Animatable) imageView.getDrawable();
animatable.start();
}
});
来看看效果图吧
4.交互式中国地图
我们已经掌握了静态与动态的SVG使用,接下来要学习更具挑战性的交互式应用。原图长这样:
我们要实现的效果就是每个省份都能被点击并凸显出来。很显然,这么一个复杂的图形是android中其他的知识所不能解决的。先看看效果图
下面分析思路。首先,解析SVG图片,由于每个省份都是一个path,因此可以获取到34个Path。又因为每个省份都有不同的颜色、被点击时有不同的绘制方式,所以可以创建provinceItem对象来封装这些参数和方法。最后是点击事件的控制与判断,如果当前触摸点在某个省份内,就将其轮廓突出。
4.1 ProvinceItem
我们以小博大,先从ProvinceItem对象开始介绍。该对象有2个参数,分别是从SVG中解析出来的path以及该path需要填充的颜色color。每个“省”都提供了绘制方法,用来让外部的地图控件调用,以此绘制普通状态或者选中状态。
/**
* 是否被选择
*
* @param canvas
* @param paint
* @param isSelected
*/
public void draw(Canvas canvas, Paint paint, boolean isSelected) {
if (isSelected) {
paint.setStrokeWidth(3);
paint.setColor(Color.BLACK);
}else {
paint.setStrokeWidth(1);
paint.setColor(0xFFD0E8F4);
}
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path,paint);
paint.setColor(drawColor);
paint.setStyle(Paint.Style.FILL);
canvas.drawPath(path, paint);
}
普通状态和选中状态的区别只是外部轮廓的颜色和粗细,使用paint分别绘制其轮廓和填充颜色即可。
重点在于判断触摸点是否在某个省的范围内。每个省都是不规则的图形,说到不规则,是否想起之前讲Canvas时介绍的Region?Region代表一块区域,其面积的计算是使用微积分的原理,正好在此派上用场。
public boolean isTouch(int x, int 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(x,y);
}
无论是多么不规则的图形,总会有顶点的上下左右,我们通过path.computeBounds()
计算出这个上下左右的边界,再通过region.setPath()
将path和上下左右传入,即可获取path所对应的那块region。
4.2 MapView
下面介绍外层的地图控件MapView,在初始化方法中,loadThread用来从SVG中加载数据,GestureDetectorCompat用来代理onTounch()
中的触摸事件,没什么多余的意思,就是简单不用谢swich语句而已……
private void init(Context context) {
this.mContext = context;
mProvinceItems = new ArrayList<>();
mPaint = new Paint();
mPaint.setAntiAlias(true);
loadThread.start();
mGestureDetectorCompat = new GestureDetectorCompat(mContext, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
handleTouch(e.getX(), e.getY());
return true;
}
});
}
先看在子线程中加载数据的操作,由于svg是以xml的形式展现的,所以先要解析xml。这里使用了dom解析,当然你喜欢sax或者pull或者别的什么都无所谓。
Thread loadThread = new Thread() {
@Override
public void run() {
List<ProvinceItem> items = new ArrayList<>();//用新的list防止加载时冲突导致crash
InputStream inputStream = mContext.getResources().openRawResource(R.raw.map_china);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(inputStream);
Element root = doc.getDocumentElement();
NodeList list = root.getElementsByTagName("path");
for (int i = 0; i < list.getLength(); i++) {
Element element = (Element) list.item(i);
String pathData = element.getAttribute("android:pathData");
Path path = PathParser.createPathFromPathData(pathData);
ProvinceItem item = new ProvinceItem(path);
items.add(item);
}
} catch (Exception e) {
e.printStackTrace();
}
mProvinceItems = items;
mHandler.sendEmptyMessage(1);
}
};
这段代码的重点其实在PathParser.createPathFromPathData(pathData)
这行,其作用是将vector drawable中的path语法转化为android中的Path类。这不是一件简单的差事,但这又是一件需求很广泛的差事,所以我选择使用开源的类比如CSDN上就有的下载,这里限于篇幅我只把这个方法单独拉出来溜溜,有兴趣的同学去找个工具类自己学习吧:
public static Path createPathFromPathData(String pathData) {
Path path = new Path();
PathDataNode[] nodes = createNodesFromPathData(pathData);
if (nodes != null) {
try {
PathDataNode.nodesToPath(nodes, path);
} catch (RuntimeException e) {
throw new RuntimeException("Error in parsing " + pathData, e);
}
return path;
}
return null;
}
回到我们的代码中,loadThread在最后给handler发送了消息,handler作用很简单,只是给不同的path随机赋予颜色值
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (mProvinceItems == null) {
return;
}
int totalNumber = mProvinceItems.size();
for (int i = 0; i < totalNumber; i++) {
int color;
int flag = i % 4;
switch (flag) {
case 1:
color = colorArray[1];
break;
case 2:
color = colorArray[2];
break;
case 3:
color = colorArray[3];
break;
default:
color = colorArray[0];
break;
}
mProvinceItems.get(i).setDrawColor(color);
}
postInvalidate();
}
};
handler的最后,postInvalidate()
会导致重绘,进而调用onDraw()
方法。这里又涉及到scale放大倍数,由于svg本身的优点就是随便拉伸,因此给予MapView控件这个scale属性是理所当然的。剩下就是分别绘制普通省份和被选中省份。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mProvinceItems != null) {
//放大倍数
canvas.scale(scale, scale);
for (ProvinceItem item : mProvinceItems) {
if (item != selectedItem) {
item.draw(canvas, mPaint, false);
}
}
if (selectedItem != null) {
selectedItem.draw(canvas, mPaint, true);
}
}
}
最后来看看触摸事件,onTouchEvent
中直接回调mGestureDetectorCompat的方法。
@Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetectorCompat.onTouchEvent(event);
}
这里为了好看封装了下
mGestureDetectorCompat = new GestureDetectorCompat(mContext, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
handleTouch(e.getX(), e.getY());
return true;
}
});
最后一个重点,由于之前的canvas拉伸过,所以在处理点击位置时需要还原。
private void handleTouch(float x, float y) {
if (mProvinceItems == null) {
return;
}
ProvinceItem tmpItem = null;
for (ProvinceItem item : mProvinceItems) {
if (item.isTouch((int) (x / scale), (int) (y / scale))) {
tmpItem = item;
break;
}
}
if (tmpItem != null) {
selectedItem = tmpItem;
postInvalidate();
}
}
5.总结
代码分析完毕,是不是还挺简单的?其实最难的部分美工已经帮我们解决了,我们只要解析SVG获取到相应的属性,在通过path啊,paint啊之流去处理这些属性,就可以轻松的完成一些复杂的自定义控件了。
最后祝大家不会被美工分而食之!