1.1 利用PathMeasure实现路径动画
androidSDK提供了一个非常有用的API来帮助开发者实现这样一个Path路径点的坐标追踪,这个API就是PathMeasure,通过它就可以是实现复杂的动画效果
1.1.1 初始化PathMeasure方法
方法1:
PathMeasure pathMeasure=new PathMeasure();
setPath(Path path,boolean forceClosed)
使用pathMeasure.setPath()函数将Path与PathMeasure进行绑定
方法2:
通过PathMeasure的另外的构造方法直接完成初始化
PathMeasure(Path path,boolean forceClosed);
在这个构造函数与setPath函数中都有forceClosed这个参数,这个参数的含义是Path最终是否需要闭合,如果为true,则不管关联的Path是否是闭合的,都会被闭合;
1.1.2 简单函数使用
1.getLength()
public float getLength();
pathMeasure.getLength()函数就是用来计算路径的长度。getLength()函数是针对当前曲线,而不是整个Path。
我们创建一个控件,在onDraw()中重写以下内容:
canvas.translate(50,50);
Paint paint=new Paint();
paint.setColor(Color.BLACK);
paint.setStrokeWidth(8);
paint.setStyle(Paint.Style.STROKE);
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.e("qwe","forceClosed----->"+measure1.getLength());
Log.e("qwe","forceClosed----->"+measure2.getLength());
canvas.drawPath(path,paint);
运行截图如下:
很明显,当我们设置forceClosed为false时,测量的是当前Path状态的长度;如果为true,不论Path是否闭合,测试的都是Path的闭合长度。
2.isClosed()函数
public boolean isClosed();
用于判断测量Path时是否计算闭合,所以如果在关联Path的时候设置forceClosed为true,则这个函数的返回值一定为true
3.nextContour()函数
用于跳转带下一条曲线的函数,如果跳转成功返回true,失败返回false,相对于getlength();getSegment()这些函数都只会对其中第一条线段进行计算。以下是一个path路径跳转的实例:
onDraw()中代码如下所示:
canvas.translate(250,250);
Paint paint=new Paint();
paint.setColor(Color.BLACK);
paint.setStrokeWidth(8);
paint.setStyle(Paint.Style.STROKE);
Path path=new Path();
path.addRect(-50,-50,50,50,Path.Direction.CW);
canvas.drawPath(path,paint);
path.addRect(-100,-100,100,100,Path.Direction.CW);
canvas.drawPath(path,paint);
path.addRect(-120,-120,120,120,Path.Direction.CW);
canvas.drawPath(path,paint);
PathMeasure measure1=new PathMeasure(path,false);
do{
float len = measure1.getLength();
Log.d("qwe", "len="+len);
}while (measure1.nextContour());
运行截图:
打印日志如下:
从打印日志中可以看到nextContour的跳转过程。
1.1.3 getSegment()函数
boolean getSegment (float startD; float stopD; Path dst; boolean startWithMoveTo)
这个API用于截取整个Path中的某个片段,startD和stopD用来控制长度,并将截取后的Path保存在参数dst中。最后一个参数表示起始点是否使用moveTo将路径的新起始点移到Path的起始点,通常设置为true,以保证每次截取的Path都是正常的。通常和dst一起使用;因为dst中保存的Path是被不断添加的,而不是每次被覆盖的;如果设置为false,则新增的片段会从上一次Path终点开始计算,这样可以保证截取的Path片段是连续的。
使用实例:
canvas.translate(250,250);
Paint paint=new Paint();
paint.setColor(Color.BLACK);
paint.setStrokeWidth(8);
paint.setStyle(Paint.Style.STROKE);
Path path=new Path();
path.addRect(-50,-50,50,50,Path.Direction.CW);
// canvas.drawPath(path,paint);
Path dst = new Path();
PathMeasure pathMeasure = new PathMeasure(path,false);
pathMeasure.getSegment(0,150,dst,true);
canvas.drawPath(dst,paint);
运行截图:
该图是移动坐标和后,绘制的矩形图案。
这幅图是使用getSegment截取的路径,从这个街区结果可以看出:
路径截取是以路径的左上角为起始点开始的。
还有截取路径的方向是由Path.Direction.CW(顺时针方向)决定的,若设置为Path.Direction.CCW则是逆时针截取。
实例二当dst中不为空,存储有路径时:
Path path=new Path();
path.addRect(-50,-50,50,50,Path.Direction.CW);
Path dst = new Path();
dst.lineTo(20,100);
PathMeasure pathMeasure = new PathMeasure(path,false);
pathMeasure.getSegment(0,150,dst,true);
canvas.drawPath(dst,paint);
运行截图如下:
这个实例中dst出事并不是空的是一条线段,从(0,0)到(20,100);从结果中看,dst中原有的线段被保留下来了,所以的得出结论:dst是将截取的路径添加到dst中,而不是替换原有内容。
实例三:当startWithMoveTo为false
还是上面的代码,将参数改为false:
pathMeasure.getSegment(0,150,dst,false);
运行截图:
这个看起来是不是很迷糊其实它的实现是这样的:
得出结论:若startWithMoveTo为True,则被截取出来的是Path片段保持原状,为false则将Path片段的起始点移动到dst的最后一个点,以保证dst路径的连续性。
2.示例加载动画
以下是该动画的代码实现:
public Weigetm(Context context, AttributeSet attrs) {
super(context, attrs);
paint=new Paint();
paint.setColor(Color.BLACK);
paint.setStrokeWidth(8);
paint.setStyle(Paint.Style.STROKE);
path=new Path();
Path circlePath= new Path();
circlePath.addCircle(100,100,50,Path.Direction.CW);
pathMeasure = new PathMeasure(circlePath,true);
final ValueAnimator animator = ValueAnimator.ofFloat(0,1);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
currentValue = (Float) valueAnimator.getAnimatedValue();
invalidate();
}
});
animator.setDuration(2000);
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
float stop = pathMeasure.getLength()*currentValue;
path.reset();
pathMeasure.getSegment(0,stop,path,true);
canvas.drawPath(path,paint);
}
运行截图如下:
这个动画就是实现一条圆形路径从长度为0增加到整个圆,如此反复。
1.1.4 getPosTan 函数
//getPosTan函数用于得到路径上某一长度的位置以及该位置的正切值
boolean getPosTan(float distance ,float[] pos;float[] tan);
参数:
- float distance :距离Path起始点的长度,取值范围0<=distance<=getLength
- float [] pos:该店的坐标值
- float [] tan: 该点的正切值
箭头加载动画实例:图中的箭头就需要实时的改变角度
private Path mCirclePath,mDstPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private Float mCurrAnimValue;
private Bitmap mArrawBmp;
//tan pos 在使用时必须先使用new关键词分配存储空间
private float [] pos = new float[2];
private float [] tan = new float[2];
public Weigetm(Context context, AttributeSet attrs) {
super(context, attrs);
mArrawBmp= BitmapFactory.decodeResource(getResources(),R.drawable.arraw);
mPaint=new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(8);
mPaint.setStyle(Paint.Style.STROKE);
mDstPath=new Path();
mCirclePath= new Path();
mCirclePath.addCircle(100,100,300,Path.Direction.CW);
mPathMeasure = new PathMeasure(mCirclePath,true);
final ValueAnimator animator = ValueAnimator.ofFloat(0,1);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCurrAnimValue = (Float) valueAnimator.getAnimatedValue();
invalidate();
}
});
animator.setDuration(2000);
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(300,300);
canvas.drawColor(Color.WHITE);
float stop = mPathMeasure.getLength()*mCurrAnimValue;
mDstPath.reset();
mPathMeasure.getSegment(0,stop,mDstPath,true);
canvas.drawPath(mDstPath,mPaint);
//旋转箭头图片,并绘制
mPathMeasure.getPosTan(stop,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]-mArrawBmp.getWidth()/2,pos[1]-mArrawBmp.getHeight()/2);//将图片从默认的(0,0)点移动到当前路劲的最前端
canvas.drawBitmap(mArrawBmp,matrix,mPaint);
}
运行截图如下:
图中的箭头是我们准备的图片。
1.1.5 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旋转。可以同时指定。
该函数很明显和getPosTan的实现效果是一样的,实现上面的例子只需要修改如下代码:
1.1.6 实例:支付宝支付成功动画
以下时是实现动画的代码:
private float mRadius=200;
private float mCentY=300;
private float mCentX=300;
private Path mCirclePath,mDstPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private Float mCurrAnimValue;
public Weigetm(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint=new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(8);
mPaint.setStyle(Paint.Style.STROKE);
//dst容器
mDstPath=new Path();
//Path路径
mCirclePath= new Path();
mCirclePath.addCircle(mCentX,mCentY,mRadius,Path.Direction.CW);
mCirclePath.moveTo(mCentX-mRadius/2,mCentY);
mCirclePath.lineTo(mCentX,mCentY+mRadius/2);
mCirclePath.lineTo(mCentX+mRadius/2,mCentY-mRadius/3);
mPathMeasure = new PathMeasure(mCirclePath,false);
final ValueAnimator animator = ValueAnimator.ofFloat(0,2);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCurrAnimValue = (Float) valueAnimator.getAnimatedValue();
invalidate();
}
});
animator.setDuration(4000);
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
//画出外圈的圆
if(mCurrAnimValue<1){
float stop=mPathMeasure.getLength()*mCurrAnimValue;
mPathMeasure.getSegment(0,stop,mDstPath,true);
}else {
//跳转到圈内,使用nextContour函数
mPathMeasure.getSegment(0,mPathMeasure.getLength(),mDstPath,true);
mPathMeasure.nextContour();
float stop = mPathMeasure.getLength()*(mCurrAnimValue);
mPathMeasure.getSegment(0,stop,mDstPath,true);
}
canvas.drawPath(mDstPath,mPaint);
}
结果如图:
1.2 SVG动画
SVG是矢量图,专门用于网络的适量图形标准。与矢量图对应的是位图,Bitmap就是位图,它是由一个个像素点组成。
SVG与Bitmap相比的好处
- SVG使用XML格式定义图形,可被非常多的工具读取和修改
- SVG由点来存储,由计算机根据点信息绘图,不会失真,无须根据分辨率适配多套图标
- SVG的占用空间明显比Bitmap小
- SVG可以转换为Path路径,与Path动画相结合,可以形成更丰富的动画
SVG在HTML中使用的非常多,如下面一段代码:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<rect x="25" y="25" width="200" 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标签,几乎可以实现其他所有的标签。
1.2.2 vector标签与图像显示
SVG矢量图是使用标签定义的,放在res/drawable下
如下SVG代码:
<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:fillColor="@color/colorPrimary"
android:pathData="M0,0 L100 0 L100,50 L0,50 L0,0 M50,25 L100,25"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray"/>
</vector>
显示图像为:
整个蓝色区域是我们设置的SVG图形大大小区域,我们使用了白色边框将其显示出来。
vector 参数属性:
- width与height属性:表示该SVG图形的具体大小
- viewportWidth与viewportHeight属性:表示SVG图像划分的比例;即将图形均分为多少份。
像上面的例子我们将宽度分为100个点,高度分为50个点,而M0,0 L100 ,0分别代表将点(0,0)移动到(100,0)。这里的坐标就是以viewportWidth和viewportHeight为单位的,即一个点有2dp。
vector是指定标签大小;path标签是指定路径的内容。
1.path标签
常用属性:
- android:name :声明一个标记 ,类似于ID,便于对其做动画的时候顺利地找到该节点。
- android:pathData :对SVG矢量图的描述。
- 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 X1,Y1,ENDX,ENDY):二阶贝济埃曲线
- T=smooth quardratic Belzier curve(T,ENDX,ENDY):映射前面枯井后的终点
- A=elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧线
- Z=closepath():关闭路径
- android:strokeWidth :画笔的宽度
- android:fillColor :填充颜色
- android:fillAlpha:填充颜色的透明度
- android:strokeWidth:描边宽度
- android:strokeHeight:描边透明度
- android:strokeLineJoin:用于指定折线拐角形状。去只有miter(锐角),round(圆弧),bevel(直线)
- android:strokeLineCap:画出线条的终点形状
- android:strokeMiterLimit:设置斜角的上限
- android:trimPathEnd:指定路径结束的位置,取值为(0,1)
- android:trimPathStart:指定路径开始的位置,取值为(0,1)
- android:trimPathOffset:用于指定结果路径的位移距离,取值为(0,1)
2.group标签
用于定义一系列路径或者将Path标签分组。在动画中,我们可以指定每个path 路径做特定的
动画,通过 group 标签则可以将原本由一个 path 路径实现的内容分为多个 path 路径来实现,
每个 path 路径可以指定特定的动画,这样一来,效果显示就丰富多彩了。
group 签具有以下常用属性。
• ndroid: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轴平移的距离
3.制作SVG图像
方法一:设计软件
如果有绘图基础,则可以直接使用Illustrator或在线SVG工具制作SVG图像,或者通过SVG源文件下载网站后进行编辑。
方法二:Iconfont
它原理是把你想要矢量图标打包成.ttf 文件,在 Android 中应用这个ttf 文件来方便地加载和指定各 种图标。由于是 SVG图像,所以也不存在屏幕适配问题,可以减少各种图标的占用空间。
4.实例
在android种使用ImageView,显示SVG图像
(1)引入兼容包
添加对Appcompat的支持
implementation 'androidx.appcompat:appcompat:1.1.0'
在Android 5.0之前使用Vector,需要aapt来对资源进行一些处理,这一过程可以在aapt的配置中进行设置,如果没有启用这样一个flag,那么在5.0以下的设备上运行就会发生android.content.res.Resources$NotFoundException。
首先,你需要在项目的build.gradle脚本中,增加对Vector兼容性的支持,代码如下所示:
使用Gradle Plugin 2.0以上:
android {
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
}
使用Gradle Plugin 2.0以下,Gradle Plugin 1.5以上:
android {
defaultConfig {
// Stops the Gradle plugin’s automatic rasterization of vectors
generatedDensities = []
}
// Flag to tell aapt to keep the attribute ids around
aaptOptions {
additionalParameters "--no-version-vectors"
}
(2)生成Vector图像
使用之前的Vector图像:
<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中使用
代码如下:
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/svg"/>
在java代码中设置
ImageView iv=findViewByid(R.id.iv);
iv.setImageResource(R.drawable.svg);
(4)在Button和RadioButton中使用
在这两种控件中使用时需要通过selector标签来使用
综合实例
Java代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ImageView imageView=findViewById(R.id.anim_img);
//将焦点放在ImageView上
imageView.setFocusable(true);
imageView.setFocusableInTouchMode(true);
imageView.requestFocus();
imageView.requestFocusFromTouch();
EditText editText=findViewById(R.id.edit);
editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean b) {
if(b){
AnimatedVectorDrawableCompat animatedVectorDrawableCompat=AnimatedVectorDrawableCompat.create(
MainActivity.this,R.drawable.animated_vector_search);
imageView.setImageDrawable(animatedVectorDrawableCompat);
((Animatable)imageView.getDrawable()).start();
}
}
});
}
}
xml文件目录:
anim_nar_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">
</objectAnimator>
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:valueType="floatType"
android:valueTo="1">
</objectAnimator>```
animated_vector_search.xml
```java
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/svg">
<target
android:animation="@animator/anim_search_trim_end"
android:name="search"/>
<target
android:animation="@animator/anim_nar_trim_start"
android:name="bar"/>
</animated-vector>
svg.xml
<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="@color/colorPrimary"/>
<path
android:name="bar"
android:trimPathStart="1"
android:pathData="M50,25 L100,25"
android:strokeWidth="2"
android:strokeColor="@color/colorPrimary"/>
</vector>
运行截图: