一、利用PathMeasure实现路径动画
仅通过改变控件属性的方式实现一些复杂的动画效果是比较有难度的,比如Nexus的开机动画就根本实现不了。这里将展示如何利用PathMeasure和SVG动画来实现复杂的动画效果。
初始化:
PathMeasure类似一个计算器,可以计算出指定路径的一些信息,比如路径总长、指定长度所对应的坐标点等。
构造方式一:
PathMeasure pathMeasure = new PathMeasure();
setPath(Path path, boolean forceClosed);
构造方式二:
PathMeasure(Path path, boolean forceClosed);
● forceClosed:表示Path最终是否需要闭合。forceClosed只对PathMeasure的测量结果有影响,
例如一个折线段的Path,本身没有闭合,当forceClosed设置为true时,
PathMeasure的计算就会包含最后一段闭合的路径,与原来的Path不同。
简单函数使用:
1.getLength()函数
public float getLength() // 获取当前计算的路径长度(不是整个路径)
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(50, 50);
Path path = new Path();
path.moveTo(0, 0);
path.lineTo(0, 100);
path.lineTo(100, 100);
path.lineTo(100, 0);
PathMeasure measure1 = new PathMeasure(path, false);
PathMeasure measure2 = new PathMeasure(path, true);
Log.d("TAG", "forceClosed=false---->" + measure1.getLength());
Log.d("TAG", "forceClosed=true----->" + measure2.getLength());
Paint paint = new Paint();
paint.setColor(Color.BLACK);
paint.setStrokeWidth(8);
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, paint);
}
D/TAG:forceClosed=false---->300.0
D/TAG:forceClosed=true----->400.0
很明显,forceClosed=false时,则测量的是当前Path状态的长度;如果forceClosed=true,则不论Path是否闭合,测量的都是Path的闭合长度。
2.isClosed()函数
public boolean isClosed()
如果在关联Path的时候设置forceClosed为true,则这个函数的返回值一定为true。
3.nextContour()函数(contour:[ˈkɑːntʊr]外形; 轮廓)
Path可以由多条曲线构成,但不论是getLength()、getSegment()还是其他函数,都只会针对其中第一条线段进行计算。 而nextContour()就是用于跳转到下一条曲线的函数。如果跳转成功,则返回true;如果跳转失败,则返回false。
canvas.translate(150, 150);
Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
path.addRect(-100, -100, 100, 100, Path.Direction.CW);
path.addRect(-120, -120, 120, 120, Path.Direction.CW);
canvas.drawPath(path, paint);
PathMeasure measure = new PathMeasure(path, false);
do {
float len = measure.getLength();
Log.d("TAG", "len=" + len);
} while (measure.nextContour());
通过do...while循环和measure.nextContour()函数结合,逐个枚举出Path中的所有曲线。
通过这个例子可以得出以下结论:
● 通过PathMeasure.nextContour()函数得到的曲线的顺序与Path中添加的顺序相同。
● getLength()等函数针对的都是当前的曲线,而不是整个Path。
getSegment()函数:
1.基本用法
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
● startD:(D:distance)开始【截取】位置【距离Path起始点】的长度。
● stopD:(D:distance)结束【截取】位置【距离Path起始点】的长度。
● dst:截取的Path将会被添加到dst中。注意是添加,而不是替换。
● startWithMoveTo:是否调用Path.moveTo()函数将路径的起始点改为新添加路径的起始点。
即,如果draw了segment0,接着draw了segment1,如果startWithMoveTo=true,
则路径的起始点为segment1的起始点;因为调用了start with moveTo()函数;
startWithMoveTo=false路径的起始点不变,即为segment0的终点,保证路径连续。
Begin the segment with a moveTo if startWithMoveTo is true.
总之,true:各Path独立;false:各Path连续。
注:
如果startD、stopD数值不在取值范围[0, getLength]内,或者startD==stopD,则返回false,而且不会改变dst中的内容。
开启硬件加速功能后,绘图会出现问题,因此,在使用getSegment()函数时需要【禁用硬件加速】功能。
因为dst中保存的Path是被不断添加的,而不是每次被覆盖,设置为false,则新增的片段会从上一次Path终点开始计算,这样可以保存截取的Path片段数组连续起来。
示例一:
canvas.translate(100, 100);
Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
Path dst = new Path();
PathMeasure measure = new PathMeasure(path, false);
measure.getSegment(0, 150, dst, true);
canvas.drawPath(dst, paint);
通过measure.getSegment(0, 150, dst, true);截取长度为0~150。截取成功后,会把新的路径线段添加到dst路径中,最后画出dst路径:
结论一:路径截取是以路径的左上角为起始点开始的。
因为生成路径的方式指定的是Path.Direction.CW(顺时针方向),所以顺时针方向去截取。
结论二:路径的截取方向与路径的生成方向相同。
示例二:如果dst路径不为空
Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
Path dst = new Path();
dst.lineTo(10, 100);
PathMeasure measure = new PathMeasure(path, false);
measure.getSegment(0, 150, dst, true);
canvas.drawPath(dst, paint);
(dst中存在两条segment,都被draw出来了)
结论三:会将截取的Path片段添加到路径dst中,而不是替换dst中的内容。
示例三:如果startWithMoveTo参数为false
Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
Path dst = new Path();
dst.lineTo(10, 100);
PathMeasure measure = new PathMeasure(path, false);
measure.getSegment(0, 150, dst, false);
canvas.drawPath(dst, paint);
startWithMoveTo的意思就是在添加新的路径前,是否调用Path.MoveTo()函数将路径的起始点改为新添加路径的起始点。如果设置为true,就会将路径起始点移动到新添加路径的起始点,就可以保持当前被添加路径的形状;而如果设置为false,则不会调用Path.moveTo()函数,会将路径起始点位置改为上一条路径的终点,从而保持连续性。新添加的路径除起始点位置被更改以外,其他路径点是不会被更改的。
(drawPath()依次画添加进dst中的segment0、segment1、...segmentN)
结论四:如果startWithMoveTo为true,则被截取出来的Path片段保持原状;如果startWithMoveTo为false,则会将截取出来的Path片段的起始点移动到dst的最后一个点,以保证dst路径的连续性。
2.示例:路径加载动画
public GetSegmentView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);// 抗锯齿标志
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
mPaint.setColor(Color.BLACK);
mDstPath = new Path();
mCirclePath = new Path();
mCirclePath.addCircle(100, 100, 50, Path.Direction.CW);
mPathMeasure = new PathMeasure(mCirclePath, true);
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
mCurAnimValue = (Float) animation.getAnimatedValue();
invalidate();
}
});
animator.setDuration(2000);
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
float stopD = mPathMeasure.getLength() * mCurAnimValue;
mDestPath.reset();// 让性能更好!避免连续重画!
mPathMeasure.getSegment(0, stopD, mDstPath, true);
canvas.drawPath(mDstPath, mPaint);
}
由于每次调用getSegment()函数得到的路径都被添加到mDstPath中,所以要先调用mDstPath.reset()函数清空之前生成的路径。最后将本次生成的路径画出来即可。
在生成动画时,始终是从0位置开始的。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
float length = mPathMeasure.getLength();
float stopD = length * mCurAnimValue;
float startD = (float) (stopD - ((0.5 - Math.abs(mCurAnimValue - 0.5)) * length));
mDstPath.reset();
mPathMeasure.getSegment(startD, stopD, mDstPath, true);
canvas.drawPath(mDstPath, mPaint);
}
在这里只改变了起始点的计算方法:
float startD = (float) (stop - ((0.5 - Math.abs(mCurAnimValue - 0.5)) * length));
根据效果描述,在进度为0~0.5时,路径的起始点都是0;进度为0.5~1时,路径的起始点逐渐靠近终点;当进度为1时,两点重合。
在进度小于0.5时,start=0;进度大于0.5时,start=2*mCurAnimaValue-1。
用if...else..语句来计算start也是可以的,比如:
float start = 0;
if (start >= 0.5) {
start = 2 * mCurAnimValue -1;
}
...
mPathMeasure.getSegment(start, stop, mDstPath, true);
getPosTan()函数:
1.概述
getPosTan()函数用于得到路径上某一长度的位置以及该位置的正切值。
boolean getPosTan(float distance, float[] pos, float[] tan)
● distance:距离Path起始点的长度,取值范围为 {0, getLength}
● pos:该点的坐标值。当前点在画布上的位置有两个值,分别为x、y坐标。pos[0]表示x坐标,pos[1]表示y坐标
● tan:该点的正切值,也是个二维数组{cos, sin}
以上图中,B点坐标的正切值为,所以tan数组的返回值为
。整个计算过程为:
由于tan=sin/cos,所以tan数组值就是{cos, sin}。
在Math类中,有两个求反正切值的函数。
double atan(double d) // 参数是正切的结果值
double atan2(double y, double x) // 参数是正切的点坐标值
由于tan={cos, sin},tan=sin/cos,所以弧度值=atan2(tan[1], tan[0])。
下图中有一个沿圆形旋转的箭头,而当箭头围绕圆形旋转时,应该实时地旋转箭头的转向,以使它的头与圆形连线吻合。比如,从X轴开始移动,移动了а角度后的情形如下图:
在移动а角度后,三角形要与圆形连线吻合,那箭头就要一直沿着连线的方向。∠a=∠c,正切夹角是多少度就需要旋转多少度。
结论:如果想让移动点旋转至与切线重合,则旋转角度要与正切角度相同。
public class GetPosTanView extends View {
private Path mCirclePath, mDstPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private Float mCurAnimValue;
private Bitmap mArrawBmp;
public GetPosTanView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mArrawBmp = BitmapFactory.decodeResource(getResources(), R.drawable.arraw);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
mPaint.setColor(Color.BLACK);
mDstPath = new Path();
mCirclePath = new Path();
mCirclePath.addCircle(100, 100, 50, Path.Direction.CW);
mPathMeasure = new PathMeasure(mCirclePath, true);
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurAnimValue = (Float) animation.getAnimatedValue();
invalidate();
}
});
animator.setDuration(2000);
animator.start();
}
在将图片加载到内存中之后,在每次重绘时,先将图片旋转,然后再绘制到画布上。
private float[] pos = new float[2];
private float[] tan = new float[2];
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
float stopD = mPathMeasure.getLength() * mCurAnimValue;
mDestPath.reset();
mPathMeasure.getSegment(0, stopD, mDstPath, true);
canvas.drawPath(mDstPath, mPaint);
mPathMeasure.getPosTan(stopD, pos, tan);
float degrees = (float) (Math.atan2 (tan[1], tan[0]) * 180.0 / Math.PI);
Matrix matrix = new Matrix() ;
matrix.postRotate(degrees, mArrawBmp.getwidth() / 2, mArrawBmp.getHeight() / 2);
matrix.postTranslate(pos[0], Pos[1]);
canvas.drawBitmap(mArrawBmp, matrix, mPaint);
}
需要注意的是:
(1)pos、tan数组在使用时必须使用new关键词分配存储空间,而PathMeasure.getPosTan()函数只会向数组中的元素赋值。如果事先没有分配空间,则getPosTan()函数将不会获取成功。
(2)通过Math.atan2(tan[1], tan[0])函数得到的是弧度值而不是角度值,所以要通过(Math.atan2(tan[1],tan[0])*180.0/Math.PI)将弧度值转换为角度值。
先利用matrix.postRotate()函数将图片围绕中心点旋转指定角度,以便和切线重合。
然后利用matrix.postTranslate()函数将图片从默认的(0,0)移动到当前路径的最前端。
最后将图片绘制到画布上。
从效果图中可以看出,虽然箭头随着路径轨迹移动,但有点偏差。
在移动图片时,是以图片的左上角为起始点开始移动的,所以原来的(0,0)点移动(pos[0],pos[1])距离后,图片的左上角在(pos[0],pos[1])位置上。这说明我们移动过头了,少移动半个图片大小就可以了。
将移动图片的代码加以改造,少移动半个图片大小。
Matrix matrix = new Matrix();
matrix.postRotate(degrees, mArrawBmp.getwidth() / 2, mArrawBmp.getHeight() / 2);
matrix.postTranslate(pos[0] - mArrawBmp.getwidth() / 2,pos[1] - mArrawBmp.getHeight() / 2);
canvas.drawBitmap(mArrawBmp, matrix, mPaint);
getMatrix()函数:
这个函数用于得到路径上某一长度的位置以及该位置的正切值的矩阵。
boolean getMatrix(float distance, Matrix matrix, int flags)
● distance:距离Path起始点的长度
● matrix:根据flags封装好的matrix会根据flags的设置而存入不同的内容
● flags:用于指定哪些内容会存入matrix中。flags的值有两个:
PathMeasure.POSITION_MATRIX_FLAG:表示获取位置信息;
PathMeasure.TANGENT_MATRIX_FLAG:表示获取切边信息,使得图片按Path旋转。
可以只指定一个,也可以使用“|”(或运算符)同时指定。
很明显,getMatrix()函数只是PathMeasure.getPosTan()函数的另一种实现而已。getPosTan()函数把获取到的位置和切边信息分别保存在pos和tan数组中;而getMatrix()函数则直接将其保存到matrix数组中。
下面使用getMatrix()函数代替getPosTan()函数实现箭头加载动画。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制路径加载动画,与“路径加载动画”示例相同,在此省略
...
// 计算方位角
Matrix matrix = new Matrix();
mPathMeasure.getMatrix(stopD, matrix, PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
matrix.preTranslate(-mArrawBmp.getWidth()/2, -mArrawBmp.getHeight()/2);
canvas.drawBitmap(mArrawBmp, matrix, mPaint);
}
这里通过getMatrix()函数将获取的位置信息和切边信息保存到matrix中。由于matrix中已经保存了当前的位置信息,所以我们只需要再将图片移动半个图片大小就可以了,所以使用matrix.preTranslate(-mArrawBmp.getWidth()/2, -mArrawBmp.getHeight()/2);移动半个图片大小。至于这里为什么使用preTranslate()函数移动,而上面代码用postTranslate()函数移动,在后面Matrix章节会具体介绍。
示例:支付宝支付成功动画
从这张图中可以看出在圆心(mCenterX,mCenterY)的基础上,如何根据圆形半径mRadius计算出对钩各个点的过程。
private int mCenterX = mCenterY = 100;
private int mRadius = 50;
public AliPayView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
// mPaint初始化与“路径加载动画”示例相同,在此省略
...
mDstPath = new Path();
mCirclePath = new Path();
mCirclePath.addCircle(mCenterX, mCenterY, mRadius, Path.Direction.CW);
mCirclePath.moveTo(mCenterX-mRadius/2, mCenterY);
mCirclePath.lineTo(mCenterX, mCenterY+mRadius/2);
mCirclePath.lineTo(mCenterX+mRadius/2, mCenterY-mRadius/3);
mPathMeasure = new PathMeasure(mCirclePath, false);
ValueAnimator animator = ValueAnimator.ofFloat(0, 2);
animator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurAnimValue = (Float) animation.getAnimatedValue();
invalidate();
}
});
animator.setDuration(4000);
animator.start();
}
相比“路径加载动画”示例,这里主要有两点改动:第一,在构造mCirclePath时,除添加圆形路径以外,还添加了一条对钩路径;第二,在构造ValueAnimator动画时,构造的是ofFloat(0, 2)之间的动画,这是因为我们有两条路径,0~1之间时画第一条路径,在1~2之间时画第二条路径。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
if (mCurAnimValue < 1) {
float stopD = mPathMeasure.getLength() * mCurAnimValue;
mPathMeasure.getSegment(0, stopD, mDstPath, true);
} else if (mCurAnimValue == 1) {
mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDstPath, true);
mPathMeasure.nextContour();
} else {
float stopD = mPathMeasure.getLength() * (mCurAnimValue -1);
mPathMeasure.getSegment(0, stopD, mDstPath, true);
}
canvas.drawPath(mDstPath, mPaint);
}
注意动画细节,可以理解为一条路径有它的头部和尾部,是先慢慢出现半圈,接着尾部开始动起来,直到慢慢追上头部,最终合二为一。
在绘图时,当mCurAnimValue<1时,画外圈的圆形;当mCurAnimYalue=1时,说明圆已经画完,此时,先利用mPathMeasure.getSegment()函数将圆画完,然后调用mPathMeasure.nextContour()函数将路径转到对钩路径上mCurAnimValue>1时,说明已经在对钩路径上了。
如果在mCurAnimValue==1时,添加如下代码进去:
mCount = 1;
mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDstPath, true);
float len = mPathMeasure.getLength();
while(mPathMeasure.nextContour()){
mCount++;
}
Log.d("TAG", "len=" + len + ",count=" + mCount);
D/TAG: len=313.65173,2
D/TAG: len=83.37617,2
由日志可以看出,有且仅有两条路径:
用数学计算:圆周长=2 * π * 50 = 314.1592,对勾总长=√((25+50/3)^2 + 25^2) ) = 83.9466。与Log打印出的数值有点偏差。这是可以理解的,毕竟数学计算与像素拼成的圆本来就不一模一样。
❶在mCirclePath这一个Path中添加了两条路径,为什么就是两条segment的呢?
因为mPathMeasure = new PathMeasure(mCirclePath, false); forceClosed = false,即是两条独立的路径。
❷以上代码中肯定是多次调用mPathMeasure.getSegment(0, stopD, mDstPath, true);为什么nextContour()没有取出添加的数个片段路径?
Path可以由多条曲线构成,但不论是getLength()、getSegment()还是其他函数,都只会针对其中第一条线段进行计算。 而nextContour()就是用于跳转到下一条曲线的函数。所以mPathMeasure.nextContour()跳转到的曲线与mDstPath中添加的曲线无关。mPathMeasure中有两条路径,mDstPath中有数个片段路径。
出于便于理解的考虑,本节的例子中使用了很多纯数字,这是不可取的。因为固定的数字是无法适应多变的需求的,这里我们只有两条路径,如果要在对钩上面再加一条心形路径该怎么办呢?这里的代码就得重写,而不是只添加心形路径就可以解决的。而这里的关键在于如何得知有几条路径,在得知有几条路径后 ,就可以逐个对其枚举作画。所以,我们可以提供一个接口,在外部设置Path实例,并且指定当前Path实例中的路径条数;或者,我们可以克隆一个Path实例,使用PathMeasure.nextContour()函数进行遍历,当该函数返回false时,则表明遍历结束。解决方法有很多。
二、SVG动画
概述:
SVG的全称是Scalable Vector Graphics(可缩放失量图形),由此可知SVG是失量图,而且是专门用于网络的失量图形标准。失量图由一个个点组成,经过数学计算利用直线和曲线绘制而成,无论如何放大,都不会出现马赛克现象,Illustrator就是常用的失量图绘图软件。
SVG与Bitmap相比有哪些好处:
● SVG使用XML格式定义图形,可被非常多的工具读取和修改。
● SVG由点来存储,由计算机根据点信息绘图,不会失真,无须根据分辨率适配多套图标。
● SVG的占用空间明显比Bitmap小。比如一张500px×500px的图像,转成SVG后占用的空间大小是20KB,而PNG图片则需要732KB的空间。
● SVG可以转换为Path路径,与Path动画相结合,可以形成更丰富的动画。
Google在Android 5.0中增加了对SVG图形的支持 。对于5.0以下的机型,可以通过引入 com.android.suppor:appcompat-v7:23.4.0及以上版本进行支持。
SVG这种图像格式在HTML中早就被广泛使用,比如下面这段SVG代码:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<rect x="25" y="25" height="200" fill="lime" stroke-width="4" stroke="pink" />
<circle cx="125" cy="125" r="75" fill="orange" />
<polyline points="50,150 50,200 200,200 200,100" stroke="red" stroke-width="4" fill="none" />
<line x1="50" y1="50" x2="200" y2="200" stroke="blue" stroke-width="4" />
</svg>
标准SVG语法中支持很多标签,比如<rect>(绘制矩形)、<circle>(绘制圆形)、<line>(绘制线段)、<polyline>(绘制折线)、<ellipse>(绘制椭圆)、<polygon>(绘制多边形)、<path>(绘制路径)等。
然而 Android 并没有对原生SVG图像语法进行支持,而是以一种简化的方式对SVG进行兼容,也就是通过使用它的<path>标签,几乎可以实现SVG中的其他所有标签。虽然可能会复杂一些,但这些东西都是可以通过工具完成的,所以不用担心写起来会很复杂。
<vector>标签与图像显示:
<vector xmlna:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="50">
<path
android:name="bar"
android:pathData="M50,25 L100,25"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray"/>
</vector>
● width、height:表示该SVG图形的具体大小。
● viewportWidth、viewportHeight:表示SVG图形划分的比例。指将画布的宽、高分为多少个点。Path中的点坐标都是以viewportWidth与viewportHeight的点数为坐标的,而不是dp值。
比如,将宽度200dp分为100个点,高度100dp分为50个点。<path>中M表示moveTo,L表示lineTo。所以,这里代表从点(50,25)到点(100,25)画了一条线段。这里的坐标就是以viewportWidth和viewportHeight所指定的点为单位的,即一个点有2dp。在高度上点25是中间点,在宽度上点50是中间点,而点100则表示宽度的结束位置。
很明显,<vector>标签指定的是画布大小,而<path>标签则指定的是路径内容。
1.path标签
1)常用属性
• android:name:声明一个标记,类似于ID,便于对其做动画的时候顺利地找到该节点。
• android:pathData:对SVG矢量图的描述。
• android:strokeWidth:画笔的宽度。
• android:fillColor:填充颜色。
• android:fillAlpha:填充颜色的透明度。
• android:strokeColor:描边颜色。
• android:strokeWidth:描边宽度。
• android:strokeAlpha:描边透明度。
• android:strokeLineJoin:用于指定折线拐角形状,取值有miter(结合处为锐角)、round(结合处为圆弧)、bevel(结合处为直线)。
• android:strokeLineCap:画出线条的终点的形状(线帽),取值有butt(无线帽)、round(圆形线帽)、square(方形线帽)。
• android:strokeMiterLimit:设置斜角的上限。注意:当strokeLineJoin设置为“miter”,即绘制的两条线
段以锐角相交的时候,所得的斜面可能相当长。当斜面太长时,就会变得不协调。
strokeMiterLimit属性为斜面的长度设置了一个上限。这个属性表示斜面长度
和线条长度的比值,默认值是 10,意味着一个斜面的长度不应该超过线条宽度的10倍。
如果斜面达到这个长度,它就变成斜角了。
当strokeLineJoin为“round”或“bevel”的时候,这个属性无效。
其中,android:strokeLineJoin的效果对应于setStrokeJoin(Paint.Join join)函数,
android:strokeLineCap的效果对应于Paint.setStrokeCap(Paint.Cap cap)函数,
各个取值的效果在后面讲解Paint类时会具体讲述。
2)android:trimPathStart属性(trim:[trɪm]:修剪;切去;割掉;剪下;除去)
该属性用于指定路径从哪里开始,取值0~1,表示路径开始位置的百分比。取值0时,从头部开始;取值1时,整条路径不可见。
为了展示效果,灰色部分代表的是被删除的路径部分,而黑色部分是显示出来的路径。
3)android:trimPathEnd属性
该属性用于指定路径从哪里结束,取值0~1,表示路径结束位置的百分比。取值0时,从开始位置就已经结束;取值1时,正常结束。
4)android:trimPathOffset属性
该属性用于指定结果路径的位移位置,取值0~1。取值0时,不进行位移;取值1时,位移整条路径的长度。
当trimPathOffset=1时,位移整条路径的长度,被截取的路径又回到原来的位置。
5)android:pathData属性
在<path>标签中,主要通过pathData属性来指定SVG图像的显示内容。
● M = moveTo(M X,Y):将画笔移动到指定的坐标位置。
● L = lineTo(L X,Y):画直线到指定的坐标位置。
● H = horizontal lintto(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):三阶贝济埃曲线。这里传值相比C指令少了X1,Y1坐标,这是因为S指令会将上一条指令的终点作为这条指令的起始点。
● 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():关闭路径
A指令用来绘制一条弧线,且允许弧线不闭合。A指令各参数含义如下:
● RX,RY:指所有椭圆的半轴大小。
● XROTATION:指椭圆的X轴和水平方向顺时针方向的夹角,可以相像成一个水平的椭圆绕中心点顺时针旋转XROTAION角度。
● FLAG1:只有两个值,1表示大角度弧度,0表示小角度弧度。
● FLAG2:只有两个值,确定从起始点到终点的方向,1表示顺时针,0表示逆时针。
● X,Y:为终点坐标
有以下几点要注意:
● 坐标轴心(0,0)为中心,X轴水平向右,Y轴垂直向下。
● 所有指令大小写均可。大写表示绝对定位,参照全局坐标系;小写表示相对定位,参照父容器坐标系。
● 指令和数据间的空格可以省略。
● 同一指令出现多次可以只用一个。
2.group标签
path标签用于定义可绘图的路径,而group标签则用于定义一系列路径或者将path标签分组。在静态显示图像时,是单纯使用一个path标签实现还是使用一组path标签实现没有什么实质性的区别,其主要应用在动画中。在动画中,我们可以指定每个path路径做特定的动画,通过group标签则可以将原本由一个path路径实现的内容分为多个path路径来实现,每个path路径可以指定特定的动画,这样一来,效果显示就丰富多彩了。
group标签的使用非常随意,在vector标签下可以同时有一个或多个group标签和path标签,比如下图所示的用法是允许的。
group标签具有以下常用属性:
● android:name:组的名字,用于与动画相关联。
● android:rotation:指定该组图像的旋转度数。
● android:pivotX:定义缩放和旋转该组时的X参考点。该值是相对于vector的viewport值来指定的。
● android:pivotY:定义缩放和旋转该组时的Y参考点。该值是相对于vector的viewport值来指定的。
● android:scaleX:指定该组X轴缩放大小。
● android:scaleY:指定该组Y轴缩放大小。
● android:translateX:指定该组沿X轴平移的距离。
● android:translateY:指定该组沿Y轴平移的距离。
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="50">
<group
android:rotation="90"
android:pivotX="50"
android:pivotY="25">
<path
android:name="bar"
android:pathData="M50,23 L100,23"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray"/>
</group>
</vector>
以上例子是将path的水平路径围绕画面中心点旋转90°:
3.制作SVG图像
可以使用Illustrator,或在线SVG图像制作工具:Method Draw,或通过SVG源文件下载网站下载后进行编辑。
有很多Iconfont开源网站,比如国内的阿里巴巴失量图库:http://www.iconfont.cn
4.在Android中引入SVG图像
在Android中是不支持SVG图像解析的,我们必须将SVG图像转换为vector标签描述,有两种方法。
方法一:在线转换
将SVG图像,直接拖入在线转换网站Android SVG to VectorDrawable
方法二:通过Android Studio引入
可选择本地SVG文件、可选择IDE自带的SVG文件,可调节大小、透明度。
Enable auto mirroring for RTL layout:中国的习惯是从左向右,当出现从右向左显示时,通过RTL可以水平镜像翻转图标。
5.示例
1)引入兼容包
android {
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
}
2)生成Vector图像
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="50">
<path
android:name="bar"
android:pathData="M50,25 L100,25"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray"/>
</vector>
3)在ImageView、ImageButton中使用
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/svg_line"/>
ImageView iv = (ImageView) findViewById(R.id.iv);
iv.setImageResource(R.drawable.svg_line);
4)在Button、RadioButton中使用
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/svg_line" android:state_pressed="true"/>
<item android:drawable="@drawable/svg_line"/>
</selector>
<Button
android:id="@+id/btn"
android:layout_width="70dp"
android:layout_height="70dp"
android:background="@drawable/selector_svg_line"/>
然而到这里并不能直接运行,因为兼容包还存在一个缺陷,我们需要把下面这段代码放在Activity前面。
public class MainActivity extends AppCompatActivity {
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
...
}
}
动态Vector:
动态Vector所实现的动态SVG效果才是SVG图像在Android应用中的精髓。
drawable/svg_line.xml:
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="50">
<path
android:name="bar"
android:pathData="M50,25 L100,25"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray"/>
</vector>
animator/anim_trim_start.xml:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="trimPathStart"
android:valueFrom="0"
android:valueTo="1"
android:duration="2000" />
drawable/line_animated_vector.xml:
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/svg_line">
<target
android:name="bar"
android:animation="@animator/anim_trim_start"/>
</animated-vector>
首先通过<animated-vector>标签的android:drawable属性指定Vector图像;然后通过<target>标签将路径与动画关联,<target>标签的android:name属性就是指定的<path>标签的name,它与Vector文件中的<path>标签相对应,两者必须相同,代表的就是对哪个<path>标签做动画;最后通过android:animation属性来指定这个<path>标签所对应的动画。在<animated-vector>标签中可以有很多个<target>标签,每个target标签可以将一个Path与Animator相关联。
最后在代码中使用:
ImageView imgView = (ImageView) findViewById(R.id.iv);
AnimatedVectorDrawableCompat animatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(MainActivity.this, R.drawable.line_animated_vector);
imgView.setImageDrawable(animatedVectorDrawableCompat);
((Animatable) imgView.getDrawable()).start();
示例:输入搜索动画
1.准备SVG图像(drawable/vector_search_bar.xml):
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="24dp"
android:viewportWidth="150"
android:viewportHeight="24">
<!--搜索图形-->
<path
android:name="search"
android:pathData="M141,17 A9,9 0 1,1 142,16 L149,23"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray"/>
<!--底部横线-->
<path
android:name="bar"
android:trimPathStart="1"
android:pathData="M0,23 L149,23"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray"/>
</vector>
2.准备动画(animator/anim_bar_trim_start.xml):
对于底部横线而言,从左至右逐渐减小,所以是对起始点位置的操作。(animator/anim_search_trim_start.xml)
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="trimPathStart"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"
android:duration="500" />
对于搜索图形而言,则从无到有显示出来,所以是对终点位置的操作。(animator/anim_search_trim_end.xml)
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:propertyName="trimPathEnd"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
通过<animated-vector>标签将SVG图像与动画关联起来。(drawable/animated_vector_search.xml)
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/vector_search_bar" >
<target
android:animation="@animator/anim_search_trim_end"
android:name="search"/>
<target
android:animation="@animator/anim_bar_trim_start"
android:name="bar"/>
</animated-vector>
3.布局与开始动画
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_marginTop="20dp"
android:layout_marginLeft="20dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit"
android:hint="点击输入"
android:layout_width="150dp"
android:layout_height="24dp"
android:background="@null"/>
<ImageView
android:id="@+id/anim_img"
android:layout_width="150dp"
android:layout_height="24dp"/>
</FrameLayout>
public class SearchEditActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.svg_edit_search_activity);
final ImageView imageView = (ImageView) findViewById(R.id.anim_img);
//将焦点放在ImageView上
imageView.setFocusable(true);
imageView.setFocusableInTouchMode(true);
imageView.requestFocus();
imageView.requestFocusFromTouch();
EditText editText = (EditText)findViewById(R.id.edit);
//当EditText获得焦点时开始动画
editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
AnimatedVectorDrawableCompat animatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(
SearchEditActivity.this, R.drawable.animated_vecotr_search);
imageView.setImageDrawable(animatedVectorDrawableCompat);
((Animatable) imageView.getDrawable()).start();
}
}
});
}
}
由于EditText会默认获得焦点,所以我们首先需要将焦点放在ImageView上,然后当用户点击EditText的时候,EditText获得焦点,此时开始动画。
这里我们产生讲解了如何通过兼容包来实现在Android2.1以上平台中显示SVG图像与使用SVG动画的问题。相比5.0以上的原生SVG支持,兼容包对以下内容是不支持的。
● Path Morphing:路径变换动画,在Anroid pre-L版本下是无法使用的。
● Path Interpolator:路径插值器,在Android pre-L版本下只能使用系统的插值器,不能自定义插值器。