赞
踩
SVG,即Scalable Vector Graphics 可伸缩矢量图形。这种图像格式在前端中已经使用的非常广泛了,而在移动端的开发中,遇到一些复杂的自定义控件或者动画效果,我们就可以考虑让美工出套SVG图,再按照固定的套路去解析即可。
先介绍下矢量图像和位图图像的区别
- 1.矢量图像:SVG是W3C 推出的一种开放标准的文本式矢量图形描述语言,他是基于XML的专门为网络而设计的图像格式
- SVG是一种采用XML来描述二维图形的语言,所以它可以直接打开xml文件来修改和编辑。
-
- 2.位图图像:位图图像的存储单位是图像上每一点的像素值,因而文件会比较大,像GIF、JPEG、PNG等都是位图图像格式。
也就是说,如果使用矢量图,就不需要针对不同dpi的设备展示不同精度的图片了,是不是很方便啊?
在Andoird中,SVG的实现方式就是Vector Drawable。这是个在5.0时增加的新类,所以对之前版本的兼容会有些问题,之后会单独拎出来讲。
相对于普通的Drawable来说,Vector Drawable有以下几个好处:
- (1)Vector图像可以自动进行适配,不需要通过分辨率来设置不同的图片。
- (2)Vector图像可以大幅减少图像的体积,同样一张图,用Vector来实现,可能只有PNG的几十分之一。
- (3)使用简单,很多设计工具,都可以直接导出SVG图像,从而转换成Vector图像 功能强大。
- (4)不用写很多代码就可以实现非常复杂的动画 成熟、稳定,前端已经非常广泛的进行使用了。
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 主要是用来设置路径做动画的关键属性的。
上面的这些语法只要能看懂就可以了。我们会用一些成熟的工具来辅助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
在正式开始撸代码前,先解决适配问题。
由于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
- 直接使用
结合上文内容,去阿里svg平台随便找张svg的图片,既可以通过svg2android也可以通过AS自带的插件将其转化为vector drawable,接着配置项目兼容环境,再将这个vector在资源xml中用app:srcCompat赋值给ImageView,最后的结果就是这样,无论怎样放大都不会失真。
考拉.svg
如果你在自己的安卓机上也实现了这样的效果,恭喜!关于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
我们已经掌握了静态与动态的SVG使用,接下来要学习更具挑战性的交互式应用。原图长这样:
中国地图.svg
我们要实现的效果就是每个省份都能被点击并凸显出来。很显然,这么一个复杂的图形是android中其他的知识所不能解决的。先看看效果图
2017-10-27_12_13_27.gif
下面分析思路。首先,解析SVG图片,由于每个省份都是一个path,因此可以获取到34个Path。又因为每个省份都有不同的颜色、被点击时有不同的绘制方式,所以可以创建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。
下面介绍外层的地图控件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();
- }
- }
代码分析完毕,是不是还挺简单的?其实最难的部分美工已经帮我们解决了,我们只要解析SVG获取到相应的属性,在通过path啊,paint啊之流去处理这些属性,就可以轻松的完成一些复杂的自定义控件了。
最后祝大家不会被美工分而食之!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。