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

0.06字数 2539阅读 3717

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,最后的结果就是这样,无论怎样放大都不会失真。

考拉.svg

如果你在自己的安卓机上也实现了这样的效果,恭喜!关于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();
            }
        });

来看看效果图吧

动态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。每个“省”都提供了绘制方法,用来让外部的地图控件调用,以此绘制普通状态或者选中状态。

  /**
     * 是否被选择
     *
     * @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啊之流去处理这些属性,就可以轻松的完成一些复杂的自定义控件了。

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

推荐阅读更多精彩内容