android svg动画高级,放荡不羁SVG讲解与实战——Android高级UI

3、缺点

这个缺点,说的并不是SVG的缺点,而是在 Android 中使用SVG的缺点或局限。

(1) 动画兼容问题

前言中提到 SVG 是在5.0之后引入,虽然作为一个图标资源并不会有兼容问题。

但是如果对 SVG 进行使用动画时,则需要进行兼容性处理。否在 5.0 以下会闪退,毕竟 4.4 的占有率还 10.3%左右(如下图,图片来自 Android Studio 的统计)。

至于如何使用和兼容,我们在下一小节进行说明。

123e36cfb89c15ca4860405f4dfc8b68.png

(2) 动画限制问题

动画限制这一点其实准确来说,不属于缺点,小盆友认为是不够灵活。

因为SVG的动画是通过属性动画进行执行的,我们知道属性动画最终是反射调用到类的 setXxx(Xxx就是我们设置的属性名称),所以如果该类没有对应的方法则是没有作用的。

对 “属性动画” 源码兴趣的童鞋可以移步小盆友的另一篇博文,带有活力的属性动画源码分析与实战

https://juejin.im/post/5c595158f265da2d9710cb6e

接下来的一个问题就是,属性动画反射回调的类是哪个类呢?这里有两种情况,一种是针对 Group 标签,一种是针对 Path 标签。但在说明具体具体类之前,我们有必要说明 Group 和 Path 标签的层级关系。

如下图所示,叶子节点只能为Path标签,而 Group标签用于装载Path标签或Group标签。值得一提的是 Vector 可以直接包含一个或多个Path, 而不一定需要包含Group。

73347d7fdb256e9730d40845166f139b.png

接着我们来说说他们各自的具体反射类,Group标签 对应的是

VectorDrawableCompat$VGroup 类,其类的内部方法如下,带 set 开头的方法,已经用红框圈出,这代表着我们为Group标签设置的属性动画所作用的属性就只能局限于这几个方法中。

e3557f0a12139d783f7dfcd1e7f72be7.png

Path标签对应的是 VectorDrawableCompat$VFullPath,而 VectorDrawableCompat$VFullPath 继承于 VectorDrawableCompat$VPath,这两个类的内部方法如下,同样用红框圈出 set 开头的方法,所以我们通过属性动画对Path标签进行控制的只能这几个属性。

bddb1b2584886ac70c367f4201921f6b.png

c4d63b17c1c5c94b248dae1aaf6c43cc.png

小结一下,这些方法能满足我们一些简单的动画,但是设计师来了一个较为骚气的交互,这时我们比较尴尬了,因为我们没法进行扩展,没法设置我们自己想要的动画逻辑。

3

简单使用

我们先来阐述如何将SVG常规使用起来。但在这之前我们需要说明一下 SVG 中绘制 Path 的语法。

1、绘制语法

path 的 pathData属性内装载的就是路径数据,其语法如下

M= moveto(M X,Y) :将画笔移动到指定的坐标位置

L= lineto(L X,Y) :画直线到指定的坐标位置

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):弧线

Z= closepath():关闭路径

小盆友个人认为,这些语法作为一个了解即可,并不需要记忆,因为 SVG 的资源文件一般不需要我们程序猿自行绘制,只是偶尔需要修改一下,所以要求并不是很高。

现在有很多在线编辑SVG工具,可以通过绘制后,将路径数据拷贝下来稍作修改,便可使用。

“手写”掘金 的 SVG资源就是小盆友从掘金官网获取后,进行一些简单的修改,所以只需要了解,需要修改时会运用就行。

2、作为静态图片资源

在 Android 中的常使用的模版为

android:width="xxdp"

android:height="yydp"

android:viewportWidth="xx"

android:viewportHeight="yy">

android:fillColor="#006CFF"

android:pathData="xxxx"/>

....more path or group

....more path or group

在 vector 标签中的 android:width 和 android:height 表示的是 SVG的大小,而 android:viewportWidth 和 android:viewportHeight 表示的是将 android:width 和 android:height 划分成多少个等份,随后的 Group 和 Path 的坐标则是基于这一比例进行编写。

group 和 path 我们在前面已经提过了,就不再赘述。

我们举个简单的例子,用 SVG画出 如下图形,并将其使用

6cde8de8efd6ae69661e751672770ff2.png

具体的SVG代码如下

// ic_menu.xml

android:width="200dp"

android:height="200dp"

android:viewportWidth="100"

android:viewportHeight="100">

android:name="top"

android:pathData="

M 20,20

L 50,20 80,20"

android:strokeWidth="5"

android:strokeColor="#000000"

android:strokeLineCap="round"/>

android:name="middle"

android:pathData="

M 20,50

L 50,50 80,50"

android:strokeWidth="5"

android:strokeColor="#000000"

android:strokeLineCap="round"/>

android:name="bottom"

android:pathData="

M 20,80

L 50,80 80,80"

android:strokeWidth="5"

android:strokeColor="#000000"

android:strokeLineCap="round"/>

使用其实和普通的图片资源一样,ic_menu资源 便是我们的 SVG 图形

android:layout_width="50dp"

android:layout_height="50dp"

android:layout_marginTop="10dp"

android:src="@drawable/ic_menu"/>

这里不存在兼容问题,小盆友在4.4的机子上也有测试过。

3、作为动态图片资源

SVG 的动画是比较有趣的,但我们在 “动画限制问题” 小节中提到,存在着兼容问题,5.0之前的版本不能使用SVG动画。

所以我们需要新建一个 drawable-anydpi-v21 文件夹,来存放我们的动画资源,具体存放结构和代码如下

12a99f134f09a612ffe6e1ed63defcb4.png

animated-vector 起着扣接 SVG静态资源 和 属性动画的作用。

// menu.xml

android:drawable="@drawable/ic_menu">

android:name="top"

android:animation="@animator/top_anim"/>

android:name="bottom"

android:animation="@animator/bottom_anim"/>

// top_anim.xml

android:duration="500"

android:interpolator="@android:interpolator/accelerate_decelerate"

android:propertyName="pathData"

android:valueFrom="

M 20,20

L 50,20 80,20"

android:valueTo="

M 20,50

L 50,20 50,20"

android:valueType="pathType"/>

// bottom_anim.xml

android:duration="500"

android:interpolator="@android:interpolator/accelerate_decelerate"

android:propertyName="pathData"

android:valueFrom="

M 20,80

L 50,80 80,80"

android:valueTo="

M 20,50

L 50,80 50,80"

android:valueType="pathType"/>

值得一提的是,这里的 pathData 最终就是调用了 VectorDrawableCompat$VPath 中的 setPathData,而参数类型便为 pathType。忘记的童鞋可以回 “动画限制问题” 小节查看下。

如果只是把我们这里使用的 menu资源放在 drawable-anydpi-v21 文件夹下,运行于 4.4的机子时,会报找不到相应资源的错误。所以我们需要在 drawable 文件夹下,建一个相同名字的资源 menu资源,只是里面的内容不是 animated-vector 作为根标签,而是使用和 ic_menu资源 完全一样的内容。

最终在代码中进行兼容处理 5.0之后的版本开启动画,之前的版本切换图片资源

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

((Animatable) img1.getDrawable()).start();

} else{

img1.setImageDrawable(

ContextCompat.getDrawable(SvgUseActivity. this, R.drawable.ic_back));

}

5.0之后版本的效果如下。5.0之前版本就只是简单图片切换,就不上图了:

7fb252f89660ff2231d47993ea42677d.gif

4

实战

上一小节我们知道,对 SVG 添加动画,简单方便,但是也说明了使用系统自带的这一套操作无法实现较为复杂的交互,所以我们只能自己动手,才能丰衣足食了。

还记得小盆友在介绍优点时,说到SVG的格式是XML,这就是我们自己动手的切入点。因为格式为XML,所以可以自行解析,拿取其中的pathData数据转为Path路径,接下来就可以做很多有趣的事情。我们融入到实战中来体会这一趣事。

1、"手写"掘金

效果图

c8120df21953f5b1683f0f3bdd938c57.gif

Github入口:

https://github.com/zincPower/UI2018/blob/6b0799d92d15c106f0b27fc486551a846cc834c5/class21_svg/src/main/java/com/zinc/class21_svg/widget/JueJinLogoView.java

编码思路

(1)解析 SVG 文件首先需要将 “掘金”这一SVG进行XML解析,我们借助 DocumentBuilderFactory 类,为我们解析获取一棵DOM树。

// 从 XML文档 生成 DOM对象树

DocumentBuilderFactory factory= DocumentBuilderFactory.newInstance();

Document document= null;

try{

document= factory.newDocumentBuilder().parse(inputStream);

} catch(SAXException |

IOException |

ParserConfigurationException e) {

e.printStackTrace();

} finally{

try{

inputStream.close();

} catch(IOException e) {

e.printStackTrace();

}

}

(2)获取并保存Path的数据在上一步中获取到DOM树之后,进行遍历DOM节点获取到 Path 数据,保存其填充的颜色和将 pathData 的数据翻译成 Path对象进行保存起来。

这里需要借助 PathParser 类将 pathData 的数据翻译成 Path对象,但是PathParser类 被打上了注解 @hide,我们无法直接使用,所以只能是将其拷贝一份放置我们的目录下来使用。具体核心代码如下

// 遍历所有的 Path 节点

for(int i = 0; i < pathNodeList.getLength(); ++i) {

Element pathNode = (Element) pathNodeList.item(i);

// path 的 svg 路径

StringpathData = pathNode.getAttribute(PATH_DATA);

// path 的 颜色

StringcolorData = pathNode.getAttribute(FILL_COLOR);

// 解析 path

Path path = null;

try{

path = PathParser.createPathFromPathData(pathData);

} catch(Exception e) {

e.printStackTrace();

}

// path 解析出错,退出

if(path == null) {

mHandle.sendEmptyMessage(InnerHandler.ERROR);

return;

}

int color = Color.parseColor(colorData);

path.computeBounds(rect, true);

left = left == -1? rect.left : Math.min(left, rect.left);

right = right == -1? rect.right : Math.max(right, rect.right);

top = top == -1? rect.top : Math.min(top, rect.top);

bottom = bottom == -1? rect.bottom : Math.max(bottom, rect.bottom);

PathData item = newPathData();

item.path = path;

item.color = color;

pathDataList.add(item);

}

(3)进行缩放根据 SVG图像大小 和 画布大小,进行偏移和缩放,让SVG图像大小合适且居中显示于画布中。核心代码如下

floatmScale = calculateScale(mSvgRect.width(), mSvgRect.height(), getWidth(), getHeight());

// 移至中心

mCanvasMatrix.preTranslate(getWidth() / 2, getHeight() / 2);

mCanvasMatrix.preTranslate(-mSvgRect.width() / 2, -mSvgRect.height() / 2);

mCanvasMatrix.preScale(

mScale,

mScale,

mSvgRect.width() / 2,

mSvgRect.height() / 2);

canvas.setMatrix(mCanvasMatrix);

(4)借助 PathMeasure 和 属性动画,让其进行勾勒后填充属性动画开启后,每次刷新都通过 PathMeasure 对当前需要勾勒的Path进行裁剪绘制,达到一步步勾勒的效果。核心代码如下

PathData pathData = mPathDataList.get(index);

mPaint.setStyle(Paint.Style.STROKE);

mPaint.setColor(pathData.color);

mPaint.setStrokeWidth(mLineWidth / mScale);

mPathMeasure.setPath(pathData.path, false);

mPathMeasure.getSegment( 0,

mPathMeasure.getLength() * process,

mAnimPath,

true);

canvas.drawPath(mAnimPath, mPaint);

PathMeasure的使用,可以查看小盆友的另一篇博文:PathMeasure的API讲解与实战

https://juejin.im/post/5c3039356fb9a049c15f5c5b

2、地图查阅器

效果图

beb345e2e5ca51f9fd9324e3a32320a5.gif

Github入口:

https://github.com/zincPower/UI2018/blob/6b0799d92d15c106f0b27fc486551a846cc834c5/class21_svg/src/main/java/com/zinc/class21_svg/widget/SvgMapView.java

编码思路

(1)解析SVG数据与“手写”掘金的事例一样,第一步也是解析数据,通过 PathParser 类将svg的数据转为Path对象,而颜色填充则由我们设置的数组决定。

同时还要保存好svg图像的大小,具体核心代码如下:

// 用于记录整个 svg 的实际大小

floatleft = - 1;

floattop = - 1;

floatright = - 1;

floatbottom = - 1;

// 计算出 path 的 rect

RectF rect = newRectF();

// 遍历所有的 Path 节点

for( inti = 0; i < pathNodeList.getLength(); ++i) {

Element pathNode = (Element) pathNodeList.item(i);

// path 的 svg 路径

String pathData = pathNode.getAttribute(DATA);

// path 的 title

String title = pathNode.getAttribute(TITLE);

// 省略一些代码

path.computeBounds(rect, true);

left = left == - 1? rect.left : Math.min(left, rect.left);

right = right == - 1? rect.right : Math.max(right, rect.right);

top = top == - 1? rect.top : Math.min(top, rect.top);

bottom = bottom == - 1? rect.bottom : Math.max(bottom, rect.bottom);

ItemData itemData = newItemData(path,

ContextCompat.getColor(getContext(), mMapColor[i % colorSize]),

title);

mapDataList.add(itemData);

}

mSvgRect.left = left;

mSvgRect.top = top;

mSvgRect.right = right;

mSvgRect.bottom = bottom;

(2)缩放地图至View中心根据画布的大小 和 svg的大小,将我们的画布进行偏移和缩放,使我们的地图大小合适且居中放置(这里借助了矩阵,但最终会将该矩阵作用于我们的画布)

// 移至画布中心

mCanvasMatrix.preTranslate(getWidth() / 2, getHeight() / 2);

// 移外边

floatlastLeftMargin = mLastRectF.left - mSvgRect.left;

floatlastTopMargin = mLastRectF.top - mSvgRect.top;

mCanvasMatrix.preTranslate(-lastLeftMargin, -lastTopMargin);

// 移至中心

mCanvasMatrix.preTranslate(-mLastRectF.width() / 2, -mLastRectF.height() / 2);

// 进行缩放

if(!mLastRectF.isEmpty()) {

mScale = calculateScale(

mLastRectF.width(),

mLastRectF.height(),

getWidth(),

getHeight());

}

mCanvasMatrix.preScale(

mScale,

mScale,

lastLeftMargin + mLastRectF.width() / 2,

lastTopMargin + mLastRectF.height() / 2);

(3)如何交互至此我们的地图就已经能正常显示了,但还需要交互。交互最主要的问题是我们如何知道选中的是哪块区域。具体通过一下代码进行判断,便可知道我们是否触碰了 该Path所包含的区域

/**

* 是否在触碰的范围内

*

* @paramitem 地图的每个数据项

* @paramx 触碰点的x轴

* @paramy 触碰点的y轴

* @returntrue:在范围内;false:在范围外

*/

privatebooleanisTouch(ItemData item, floatx, floaty){

item.path.computeBounds(mTouchRectF, true);

mTouchRegion.setPath(

item.path,

newRegion(( int) mTouchRectF.left,

( int) mTouchRectF.top,

( int) mTouchRectF.right,

( int) mTouchRectF.bottom)

);

returnmTouchRegion.contains(( int) x, ( int) y);

}

(4)剩余操作获得了点击的区域,如何进行动画的过渡就是计算逻辑问题了。小盆友这里就不再展开讲这块的逻辑。这里用一句话概括,就是通过比较 上一次选中的Path区域 和 这次选中的Path区域 进行 中心坐标偏移和缩放。

五、写在最后

SVG 也是一把利器,挥舞得当可以让自己的App展现出别人所想不到的交互效果,希望这篇文章能让你体会到不一样的SVG。如果你有所收获就给我一个赞❤️并关注我吧,如果发现有那些欠妥的地方,请留言区与我讨论,我们共同进步。

高级UI系列的Github地址:请进入传送门,如果喜欢的话给我一个star吧😄

https://github.com/zincPower/UI2018返回搜狐,查看更多

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值