当前位置:   article > 正文

美工死不瞑目系列之SVG推锅技巧!_vectordrawable nodestopath

vectordrawable nodestopath

 

 

1.前言

SVG,即Scalable Vector Graphics 可伸缩矢量图形。这种图像格式在前端中已经使用的非常广泛了,而在移动端的开发中,遇到一些复杂的自定义控件或者动画效果,我们就可以考虑让美工出套SVG图,再按照固定的套路去解析即可。

2.Vector Drawable

2.1 矢量图与位图

先介绍下矢量图像和位图图像的区别

  1. 1.矢量图像:SVG是W3C 推出的一种开放标准的文本式矢量图形描述语言,他是基于XML的专门为网络而设计的图像格式
  2. SVG是一种采用XML来描述二维图形的语言,所以它可以直接打开xml文件来修改和编辑。
  3. 2.位图图像:位图图像的存储单位是图像上每一点的像素值,因而文件会比较大,像GIF、JPEG、PNG等都是位图图像格式。

也就是说,如果使用矢量图,就不需要针对不同dpi的设备展示不同精度的图片了,是不是很方便啊?

2.2 Vector Drawable简介

在Andoird中,SVG的实现方式就是Vector Drawable。这是个在5.0时增加的新类,所以对之前版本的兼容会有些问题,之后会单独拎出来讲。
相对于普通的Drawable来说,Vector Drawable有以下几个好处:

  1. 1)Vector图像可以自动进行适配,不需要通过分辨率来设置不同的图片。
  2. 2)Vector图像可以大幅减少图像的体积,同样一张图,用Vector来实现,可能只有PNG的几十分之一。
  3. 3)使用简单,很多设计工具,都可以直接导出SVG图像,从而转换成Vector图像 功能强大。
  4. 4)不用写很多代码就可以实现非常复杂的动画 成熟、稳定,前端已经非常广泛的进行使用了。

2.3 Vector Drawable基本语法

Vector Drawable实际上是一个XML文件,咱们先来看一个vector的例子

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <vector xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:width="400dp"
  4. android:height="400dp"
  5. android:viewportHeight="400"
  6. android:viewportWidth="400">
  7. <path
  8. android:pathData="M 100 100 L 300 100 L 200 300 z"
  9. android:strokeColor="#000000"
  10. android:strokeWidth="5"
  11. android:fillColor="#FF0000"
  12. />
  13. </vector>

这个vector画了一个三角形,对照着上面的代码,咱们来学习Vector Drawable的基本语法。首先说明一下,这些语法开发者不需要全部精通,只要能够看懂即可,这些path标签及数据生成都可以交给工具来实现。

2.3.1 pathData标签

先看pathData标签,这里定义了vector中path的绘制,也是最重要的一部分。语法如下,注意,’M’处理时,只是移动了画笔, 没有画任何东西。

  1. M = moveto(M X,Y) :将画笔移动到指定的坐标位置,相当于 android Path 里的moveTo()
  2. L = lineto(L X,Y) :画直线到指定的坐标位置,相当于 android Path 里的lineTo()
  3. H = horizontal lineto(H X):画水平线到指定的X坐标位置
  4. V = vertical lineto(V Y):画垂直线到指定的Y坐标位置
  5. C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三次贝赛曲线
  6. S = smooth curveto(S X2,Y2,ENDX,ENDY) 同样三次贝塞尔曲线,更平滑
  7. Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY):二次贝赛曲线
  8. T = smooth quadratic Belzier curveto(T ENDX,ENDY):映射 同样二次贝塞尔曲线,更平滑
  9. A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧线 ,相当于arcTo()
  10. Z = closepath():关闭路径(会自动绘制链接起点和终点)

2.3.2 path标签

接着看下path标签的内容。稍微有个印象即可,需要时再对照着去理解。

  1. android:name 定义该 path 的名字,这样在其他地方可以通过名字来引用这个路径
  2. android:pathData 和 SVG 中 d 元素一样的路径信息。
  3. android:fillColor 定义填充路径的颜色,如果没有定义则不填充路径
  4. android:strokeColor 定义如何绘制路径边框,如果没有定义则不显示边框
  5. android:strokeWidth 定义路径边框的粗细尺寸
  6. android:strokeAlpha 定义路径边框的透明度
  7. android:fillAlpha 定义填充路径颜色的透明度
  8. android:trimPathStart 从路径起始位置截断路径的比率,取值范围从 01
  9. android:trimPathEnd 从路径结束位置截断路径的比率,取值范围从 01
  10. android:trimPathOffset 设置路径截取的范围
  11. android:strokeLineCap 设置路径线帽的形状,取值为 butt, round, square.
  12. android:strokeLineJoin 设置路径交界处的连接方式,取值为 miter,round,bevel.
  13. android:strokeMiterLimit 设置斜角的上限

2.3.4 vector标签

根元素 vector标签是用来定义这个矢量图的,该元素包含如下属性:

  1. android:name 定义该drawable的名字
  2. android:width 定义该 drawable 的内部(intrinsic)宽度,支持所有 Android 系统支持的尺寸,通常使用 dp
  3. android:height 定义该 drawable 的内部(intrinsic)高度,支持所有 Android 系统支持的尺寸,通常使用 dp
  4. android:viewportWidth 定义矢量图视图的宽度,视图就是矢量图 path 路径数据所绘制的虚拟画布
  5. android:viewportHeight 定义矢量图视图的高度,视图就是矢量图 path 路径数据所绘制的虚拟画布
  6. android:tint 定义该 drawable 的 tint 颜色。默认是没有 tint 颜色的
  7. android:tintMode 定义 tint 颜色的 Porter-Duff blending 模式,默认值为 src_in
  8. android:autoMirrored 设置当系统为 RTL (right-to-left) 布局的时候,是否自动镜像该图片。比如 阿拉伯语。
  9. android:alpha 该图片的透明度属性

2.3.5 group标签

有时候我们需要对几个路径一起处理,这样就可以使用 group 元素来把多个 path 放到一起。 group 支持的属性如下:

  1. android:name 定义 group 的名字
  2. android:rotation 定义该 group 的路径旋转多少度
  3. android:pivotX 定义缩放和旋转该 group 时候的 X 参考点。该值相对于 vector 的 viewport 值来指定的。
  4. android:pivotY 定义缩放和旋转该 group 时候的 Y 参考点。该值相对于 vector 的 viewport 值来指定的。
  5. android:scaleX 定义 X 轴的缩放倍数
  6. android:scaleY 定义 Y 轴的缩放倍数
  7. android:translateX 定义移动 X 轴的位移。相对于 vector 的 viewport 值来指定的。
  8. 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资源的网站

  1. http://www.shejidaren.com/8000-flat-icons.html
  2. http://www.flaticon.com/
  3. http://www.iconfont.cn/plus

2.5 适配中的一些坑

在正式开始撸代码前,先解决适配问题。
由于vector drawable是5.0之后才出来的东西,所以我们需要对之前的版本进行兼容。假设大家都使用Android Studio 2.2以上的版本,并且gradle版本在2.0以上(应该没有原始人吧)。下面是配置的步骤:

  1. 1.1、添加
  2. · defaultConfig {
  3. vectorDrawables.useSupportLibrary = true
  4. }
  5. 1.2、添加
  6. compile 'com.android.support:appcompat-v7:25.3.1' //需要是23.2 版本以上的
  7. 1.3、Activity需要继承与AppCompatActivity
  8. 1.4、布局文件当中添加
  9. xmlns:app="http://schemas.android.com/apk/res-auto"
  10. 1.5、使用在Actvity前面添加一个flag设置
  11. static {
  12. AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
  13. }

Vector Drawable可以理解为一张图片,所以能设置到其他的控件之中。

  1. 1 ImageView、ImageButton
  2. XML app:srcCompat(5.0以上可以直接使用background)
  3. 代码里面使用无区别,直接setBackground即可。
  4. 2. Button
  5. 不支持app:srcCompat
  6. Xm使用在Button的selector中
  7. 3. RadioButton
  8. 直接使用
  9. 4. textview的drawable
  10. 直接使用

3.Vector Drawable的使用

3.1 Vector Drawable静态使用

结合上文内容,去阿里svg平台随便找张svg的图片,既可以通过svg2android也可以通过AS自带的插件将其转化为vector drawable,接着配置项目兼容环境,再将这个vector在资源xml中用app:srcCompat赋值给ImageView,最后的结果就是这样,无论怎样放大都不会失真。

考拉.svg

如果你在自己的安卓机上也实现了这样的效果,恭喜!关于Vector Drawable最基本的静态使用已经被你掌控了!

3.2 Vector Drawable动态使用

大声告诉我,android中有几种动画的实现方式?除了帧、补间、属性动画以外,vector drawable也可以用来完成动画效果,还记得之前讲的path标签吗,这里面的属性都可以作为动画的变化条件,我们再展示一下:

  1. android:name 定义该 path 的名字,这样在其他地方可以通过名字来引用这个路径
  2. android:pathData 和 SVG 中 d 元素一样的路径信息。
  3. android:fillColor 定义填充路径的颜色,如果没有定义则不填充路径
  4. android:strokeColor 定义如何绘制路径边框,如果没有定义则不显示边框
  5. android:strokeWidth 定义路径边框的粗细尺寸
  6. android:strokeAlpha 定义路径边框的透明度
  7. android:fillAlpha 定义填充路径颜色的透明度
  8. android:trimPathStart 从路径起始位置截断路径的比率,取值范围从 01
  9. android:trimPathEnd 从路径结束位置截断路径的比率,取值范围从 01
  10. android:trimPathOffset 设置路径截取的范围
  11. android:strokeLineCap 设置路径线帽的形状,取值为 butt, round, square.
  12. android:strokeLineJoin 设置路径交界处的连接方式,取值为 miter,round,bevel.
  13. android:strokeMiterLimit 设置斜角的上限

剩下就是满满的套路了。
首先,获取到一张vector图片,比如这次使用的是一个对勾。我们给path标签附上了name属性,这是为了之后在动画中找到这条path。

  1. <vector xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:width="24dp"
  3. android:height="24dp"
  4. android:viewportWidth="24.0"
  5. android:viewportHeight="24.0">
  6. <path
  7. android:name="path_check"
  8. android:fillColor="#FF000000"
  9. android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
  10. </vector>

接着,用动画vector包装原来的vector图片,其创建方式和vector相似,只不过最外层的标签为animated-vector。我们还要为target标签赋值,name属性是前面命名的、vector中需要变化的地方,而animation自然就是属性动画了。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:drawable="@drawable/ic_done_black_24dp">
  4. <target
  5. android:name="path_check"
  6. android:animation="@animator/check_animator"/>
  7. </animated-vector>

归根结底还是需要用到属性动画,我们通过xml的方式来完成它。注意要在res下创建animator文件夹,再将xml放入其中。这里变化的属性是path标签中trimPathEnd属性。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <set xmlns:android="http://schemas.android.com/apk/res/android">
  3. <objectAnimator
  4. android:duration="500"
  5. android:propertyName="trimPathEnd"
  6. android:valueFrom="0"
  7. android:valueTo="1"
  8. android:valueType="floatType"/>
  9. </set>

最后,只要在代码中将Drawable转化为Animatable,并调用其start()方法开启动画即可。

  1. <ImageView
  2. android:id="@+id/iv"
  3. android:layout_width="240dp"
  4. android:layout_height="240dp"
  5. app:srcCompat="@drawable/check_animator"
  6. />
  1. imageView.setOnClickListener(new View.OnClickListener() {
  2. @Override
  3. public void onClick(View v) {
  4. Animatable animatable = (Animatable) imageView.getDrawable();
  5. animatable.start();
  6. }
  7. });

来看看效果图吧

动态vector.gif

4.交互式中国地图

我们已经掌握了静态与动态的SVG使用,接下来要学习更具挑战性的交互式应用。原图长这样:

中国地图.svg

我们要实现的效果就是每个省份都能被点击并凸显出来。很显然,这么一个复杂的图形是android中其他的知识所不能解决的。先看看效果图

2017-10-27_12_13_27.gif

下面分析思路。首先,解析SVG图片,由于每个省份都是一个path,因此可以获取到34个Path。又因为每个省份都有不同的颜色、被点击时有不同的绘制方式,所以可以创建provinceItem对象来封装这些参数和方法。最后是点击事件的控制与判断,如果当前触摸点在某个省份内,就将其轮廓突出。

4.1 ProvinceItem

我们以小博大,先从ProvinceItem对象开始介绍。该对象有2个参数,分别是从SVG中解析出来的path以及该path需要填充的颜色color。每个“省”都提供了绘制方法,用来让外部的地图控件调用,以此绘制普通状态或者选中状态。

  1. /**
  2. * 是否被选择
  3. *
  4. * @param canvas
  5. * @param paint
  6. * @param isSelected
  7. */
  8. public void draw(Canvas canvas, Paint paint, boolean isSelected) {
  9. if (isSelected) {
  10. paint.setStrokeWidth(3);
  11. paint.setColor(Color.BLACK);
  12. }else {
  13. paint.setStrokeWidth(1);
  14. paint.setColor(0xFFD0E8F4);
  15. }
  16. paint.setStyle(Paint.Style.STROKE);
  17. canvas.drawPath(path,paint);
  18. paint.setColor(drawColor);
  19. paint.setStyle(Paint.Style.FILL);
  20. canvas.drawPath(path, paint);
  21. }

普通状态和选中状态的区别只是外部轮廓的颜色和粗细,使用paint分别绘制其轮廓和填充颜色即可。

重点在于判断触摸点是否在某个省的范围内。每个省都是不规则的图形,说到不规则,是否想起之前讲Canvas时介绍的Region?Region代表一块区域,其面积的计算是使用微积分的原理,正好在此派上用场。

  1. public boolean isTouch(int x, int y) {
  2. RectF rectF=new RectF();
  3. path.computeBounds(rectF,true);
  4. Region region=new Region();
  5. region.setPath(path,new Region((int)rectF.left,(int)rectF.top,(int)rectF.right,(int)rectF.bottom));
  6. return region.contains(x,y);
  7. }

无论是多么不规则的图形,总会有顶点的上下左右,我们通过path.computeBounds()计算出这个上下左右的边界,再通过region.setPath()将path和上下左右传入,即可获取path所对应的那块region。

4.2 MapView

下面介绍外层的地图控件MapView,在初始化方法中,loadThread用来从SVG中加载数据,GestureDetectorCompat用来代理onTounch()中的触摸事件,没什么多余的意思,就是简单不用谢swich语句而已……

  1. private void init(Context context) {
  2. this.mContext = context;
  3. mProvinceItems = new ArrayList<>();
  4. mPaint = new Paint();
  5. mPaint.setAntiAlias(true);
  6. loadThread.start();
  7. mGestureDetectorCompat = new GestureDetectorCompat(mContext, new GestureDetector.SimpleOnGestureListener() {
  8. @Override
  9. public boolean onDown(MotionEvent e) {
  10. handleTouch(e.getX(), e.getY());
  11. return true;
  12. }
  13. });
  14. }

先看在子线程中加载数据的操作,由于svg是以xml的形式展现的,所以先要解析xml。这里使用了dom解析,当然你喜欢sax或者pull或者别的什么都无所谓。

  1. Thread loadThread = new Thread() {
  2. @Override
  3. public void run() {
  4. List<ProvinceItem> items = new ArrayList<>();//用新的list防止加载时冲突导致crash
  5. InputStream inputStream = mContext.getResources().openRawResource(R.raw.map_china);
  6. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
  7. try {
  8. DocumentBuilder builder = factory.newDocumentBuilder();
  9. Document doc = builder.parse(inputStream);
  10. Element root = doc.getDocumentElement();
  11. NodeList list = root.getElementsByTagName("path");
  12. for (int i = 0; i < list.getLength(); i++) {
  13. Element element = (Element) list.item(i);
  14. String pathData = element.getAttribute("android:pathData");
  15. Path path = PathParser.createPathFromPathData(pathData);
  16. ProvinceItem item = new ProvinceItem(path);
  17. items.add(item);
  18. }
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. }
  22. mProvinceItems = items;
  23. mHandler.sendEmptyMessage(1);
  24. }
  25. };

这段代码的重点其实在PathParser.createPathFromPathData(pathData)这行,其作用是将vector drawable中的path语法转化为android中的Path类。这不是一件简单的差事,但这又是一件需求很广泛的差事,所以我选择使用开源的类比如CSDN上就有的下载,这里限于篇幅我只把这个方法单独拉出来溜溜,有兴趣的同学去找个工具类自己学习吧:

  1. public static Path createPathFromPathData(String pathData) {
  2. Path path = new Path();
  3. PathDataNode[] nodes = createNodesFromPathData(pathData);
  4. if (nodes != null) {
  5. try {
  6. PathDataNode.nodesToPath(nodes, path);
  7. } catch (RuntimeException e) {
  8. throw new RuntimeException("Error in parsing " + pathData, e);
  9. }
  10. return path;
  11. }
  12. return null;
  13. }

回到我们的代码中,loadThread在最后给handler发送了消息,handler作用很简单,只是给不同的path随机赋予颜色值

  1. private Handler mHandler = new Handler() {
  2. @Override
  3. public void handleMessage(Message msg) {
  4. super.handleMessage(msg);
  5. if (mProvinceItems == null) {
  6. return;
  7. }
  8. int totalNumber = mProvinceItems.size();
  9. for (int i = 0; i < totalNumber; i++) {
  10. int color;
  11. int flag = i % 4;
  12. switch (flag) {
  13. case 1:
  14. color = colorArray[1];
  15. break;
  16. case 2:
  17. color = colorArray[2];
  18. break;
  19. case 3:
  20. color = colorArray[3];
  21. break;
  22. default:
  23. color = colorArray[0];
  24. break;
  25. }
  26. mProvinceItems.get(i).setDrawColor(color);
  27. }
  28. postInvalidate();
  29. }
  30. };

handler的最后,postInvalidate()会导致重绘,进而调用onDraw()方法。这里又涉及到scale放大倍数,由于svg本身的优点就是随便拉伸,因此给予MapView控件这个scale属性是理所当然的。剩下就是分别绘制普通省份和被选中省份。

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. super.onDraw(canvas);
  4. if (mProvinceItems != null) {
  5. //放大倍数
  6. canvas.scale(scale, scale);
  7. for (ProvinceItem item : mProvinceItems) {
  8. if (item != selectedItem) {
  9. item.draw(canvas, mPaint, false);
  10. }
  11. }
  12. if (selectedItem != null) {
  13. selectedItem.draw(canvas, mPaint, true);
  14. }
  15. }
  16. }

最后来看看触摸事件,onTouchEvent中直接回调mGestureDetectorCompat的方法。

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. return mGestureDetectorCompat.onTouchEvent(event);
  4. }

这里为了好看封装了下

  1. mGestureDetectorCompat = new GestureDetectorCompat(mContext, new GestureDetector.SimpleOnGestureListener() {
  2. @Override
  3. public boolean onDown(MotionEvent e) {
  4. handleTouch(e.getX(), e.getY());
  5. return true;
  6. }
  7. });

最后一个重点,由于之前的canvas拉伸过,所以在处理点击位置时需要还原。

  1. private void handleTouch(float x, float y) {
  2. if (mProvinceItems == null) {
  3. return;
  4. }
  5. ProvinceItem tmpItem = null;
  6. for (ProvinceItem item : mProvinceItems) {
  7. if (item.isTouch((int) (x / scale), (int) (y / scale))) {
  8. tmpItem = item;
  9. break;
  10. }
  11. }
  12. if (tmpItem != null) {
  13. selectedItem = tmpItem;
  14. postInvalidate();
  15. }
  16. }

5.总结

代码分析完毕,是不是还挺简单的?其实最难的部分美工已经帮我们解决了,我们只要解析SVG获取到相应的属性,在通过path啊,paint啊之流去处理这些属性,就可以轻松的完成一些复杂的自定义控件了。

最后祝大家不会被美工分而食之!

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/581084
推荐阅读
相关标签
  

闽ICP备14008679号