Android-自定义曲线路径动画框架

前言

最近在一个项目中需要一个像QQ打开个人爱好那样的动画效果如下图:

20171112151041618224358.gif

可以看出每个小球都是以顺时针旋转出来的,说明像这样的曲线动画用Android中自带的平移动画是很难实现的。

曲线动画怎么画???

我们先来看看Android自带的绘制曲线的方式是怎样的:

android自定义View中画图经常用到这几个什么什么To

moveTo

moveTo 不会进行绘制,只用于移动移动画笔,也就是确定绘制的起始坐标点。结合以下方法进行使用。

lineTo

lineTo 用于进行直线绘制。

1
2
mPath.lineTo(300, 300);
canvas.drawPath(mPath, mPaint);

默认从坐标(0,0)开始绘制。

刚才我们不是说了moveTo是用来移动画笔的吗?

1
2
3
mPath.moveTo(100, 100);
mPath.lineTo(300, 300);
canvas.drawPath(mPath, mPaint);

把画笔移动(100,100)处开始绘制

quadTo

quadTo 用于绘制圆滑曲线,即贝塞尔曲线。

cubicTo

cubicTo 同样是用来实现贝塞尔曲线的。mPath.cubicTo(x1, y1, x2, y2, x3, y3) (x1,y1) 为控制点,(x2,y2)为控制点,(x3,y3) 为结束点。那么,cubicTo 和 quadTo 有什么不一样呢?说白了,就是多了一个控制点而已。然后,我们想绘制和上一个一样的曲线,应该怎么写呢?

1
2
mPath.moveTo(100, 500);
mPath.cubicTo(100, 500, 300, 100, 600, 500);

一模一样!如果我们不加 moveTo 呢?

则以(0,0)为起点,(100,500)和(300,100)为控制点绘制贝塞尔曲线

受到上面的启发,我们也可以用同样的方法来实现一个曲线动画框架

在写框架之前我们必须要先了解一样东西:

贝塞尔曲线:

维基百科中这样说到:

在数学的数值分析领域中,贝塞尔曲线(英语:Bézier curve)是计算机图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。

贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线。

线性贝塞尔曲线

给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:

B(t) = P0 + (P1 - P0)t = (1 - t)P0 + tP1,t->[0,1]

20171112151041640123831.gif

二次方贝塞尔曲线

二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:

20171112151041650962507.png

2017111215104165256666.gif

三次方贝塞尔曲线

P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”。

曲线的参数形式为:

20171112151041654896085.png

20171112151041656320256.gif

以上都是维基百科给出的定义,以及不同曲线的公式和效果图; 如果不清楚可以自己百度搜索或者维基百科搜索,么么哒!

一般贝塞尔曲线方程

20171112151041657814861.png

对于四次曲线,可由线性贝塞尔曲线描述的中介点Q0、Q1、Q2、Q3,由二次贝塞尔曲线描述的点R0、R1、R2,和由三次贝塞尔曲线描述的点S0、S1所建构:

20171112151041659410068.gif

那么在上代码之前先看看我们最后实现出来的效果图:

20171112151041661322165.gif

PathPoint.java中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* Created by zhengliang on 2016/10/15 0015.
* 记录view移动动作的坐标点
*/
public class PathPoint {
/**
* 起始点操作
*/
public static final int MOVE=0;
/**
* 直线路径操作
*/
public static final int LINE=1;
/**
* 二阶贝塞尔曲线操作
*/
public static final int SECOND_CURVE =2;
/**
* 三阶贝塞尔曲线操作
*/
public static final int THIRD_CURVE=3;
/**
* View移动到的最终位置
*/
public float mX,mY;
/**
* 控制点
*/
public float mContorl0X,mContorl0Y;
public float mContorl1X,mContorl1Y;
//操作符
public int mOperation;
/**
* Line/Move都通过该构造函数来创建
*/
public PathPoint(int mOperation,float mX, float mY ) {
this.mX = mX;
this.mY = mY;
this.mOperation = mOperation;
}
/**
* 二阶贝塞尔曲线
* @param mX
* @param mY
* @param mContorl0X
* @param mContorl0Y
*/
public PathPoint(float mContorl0X, float mContorl0Y,float mX, float mY) {
this.mX = mX;
this.mY = mY;
this.mContorl0X = mContorl0X;
this.mContorl0Y = mContorl0Y;
this.mOperation = SECOND_CURVE;
}
/**
* 三阶贝塞尔曲线
* @param mContorl0x
* @param mContorl0Y
* @param mContorl1x
* @param mContorl1Y
* @param mX
* @param mY
*/
public PathPoint(float mContorl0x, float mContorl0Y, float mContorl1x, float mContorl1Y,float mX, float mY) {
this.mX = mX;
this.mY = mY;
this.mContorl0X = mContorl0x;
this.mContorl0Y = mContorl0Y;
this.mContorl1X = mContorl1x;
this.mContorl1Y = mContorl1Y;
this.mOperation = THIRD_CURVE;
}
/**
* 为了方便使用都用静态的方法来返回路径点
*/
public static PathPoint moveTo(float x, float y){
return new PathPoint(MOVE,x,y);
}
public static PathPoint lineTo(float x,float y){
return new PathPoint(LINE,x,y);
}
public static PathPoint secondBesselCurveTo(float c0X, float c0Y,float x,float y){
return new PathPoint(c0X,c0Y,x,y);
}
public static PathPoint thirdBesselCurveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y){
return new PathPoint(c0X,c0Y,c1X,c1Y,x,y);
}
}

这个类主要是用来记录View移动动作的坐标点,通过不同的构造函数传入不同的参数来区分不同的移动轨迹,注释写的很清楚的…

为了让不同类型的移动方式都能在使用时一次性使用我写了一个AnimatorPath类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Created by zhengliang on 2016/10/15 0015.
* 客户端使用类,记录一系列的不同移动轨迹
*/
public class AnimatorPath {
//一系列的轨迹记录动作
private List<PathPoint> mPoints = new ArrayList<>();
/**
* 移动位置到:
* @param x
* @param y
*/
public void moveTo(float x,float y){
mPoints.add(PathPoint.moveTo(x,y));
}
/**
* 直线移动
* @param x
* @param y
*/
public void lineTo(float x,float y){
mPoints.add(PathPoint.lineTo(x,y));
}
/**
* 二阶贝塞尔曲线移动
* @param c0X
* @param c0Y
* @param x
* @param y
*/
public void secondBesselCurveTo(float c0X, float c0Y,float x,float y){
mPoints.add(PathPoint.secondBesselCurveTo(c0X,c0Y,x,y));
}
/**
* 三阶贝塞尔曲线移动
* @param c0X
* @param c0Y
* @param c1X
* @param c1Y
* @param x
* @param y
*/
public void thirdBesselCurveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y){
mPoints.add(PathPoint.thirdBesselCurveTo(c0X,c0Y,c1X,c1Y,x,y));
}
/**
*
* @return 返回移动动作集合
*/
public Collection<PathPoint> getPoints(){
return mPoints;
}
}

该类是最终在客户端使用的,记录一系列的不同移动轨迹,使用时调用里面的方法就可以添加不同的移动轨迹最后通过getPoints()来得到所有的移动轨迹集合

在Android自带的绘制曲线的方法中都是只是通过moveTo()方法设置起始点,在其它的方法中只是传入了终点或控制点坐标。实际上我们要画连续的曲线或连续的移动时,都需要知道起点到终点的之间所有的坐标,哪么怎么来的到这些点的坐标?

Android中为我们提供了一个泛型的接口:TypeEvaluator可以很简单的实现这个难题。这里我就把它叫做”估值器”.我们只要创建一个类来实现这个接口,然后通过自己计算公式(就是我们上面的贝塞尔曲线公式)

下面来看看我项目中的估值器类:PathEvaluator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import android.animation.TypeEvaluator;
/**
* Created by zhengliang on 2016/10/15 0015.
* 估值器类,实现坐标点的计算
*/
public class PathEvaluator implements TypeEvaluator<PathPoint> {
/**
* @param t :执行的百分比
* @param startValue : 起点
* @param endValue : 终点
* @return
*/
@Override
public PathPoint evaluate(float t, PathPoint startValue, PathPoint endValue) {
float x, y;
float oneMiunsT = 1 - t;
//三阶贝塞尔曲线
if (endValue.mOperation == PathPoint.THIRD_CURVE) {
x = startValue.mX*oneMiunsT*oneMiunsT*oneMiunsT+3*endValue.mContorl0X*t*oneMiunsT*oneMiunsT+3*endValue.mContorl1X*t*t*oneMiunsT+endValue.mX*t*t*t;
y = startValue.mY*oneMiunsT*oneMiunsT*oneMiunsT+3*endValue.mContorl0Y*t*oneMiunsT*oneMiunsT+3*endValue.mContorl1Y*t*t*oneMiunsT+endValue.mY*t*t*t;
//二阶贝塞尔曲线
}else if(endValue.mOperation == PathPoint.SECOND_CURVE){
x = oneMiunsT*oneMiunsT*startValue.mX+2*t*oneMiunsT*endValue.mContorl0X+t*t*endValue.mX;
y = oneMiunsT*oneMiunsT*startValue.mY+2*t*oneMiunsT*endValue.mContorl0Y+t*t*endValue.mY;
//直线
}else if (endValue.mOperation == PathPoint.LINE) {
//x起始点+t*起始点和终点的距离
x = startValue.mX + t * (endValue.mX - startValue.mX);
y = startValue.mY + t * (endValue.mY - startValue.mY);
} else {
x = endValue.mX;
y = endValue.mY;
}
return PathPoint.moveTo(x,y);
}
}

泛型中传入我们自己的定义的PathPoint类;其实这些复杂的计算代码很简单,就是上面贝塞尔曲线的公式,将需要的点直接带入公式即可,我相信仔细看看会明白的!

核心代码到这里就没有了,下面看看MainActivity中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private FloatingActionButton fab;
private AnimatorPath path;//声明动画集合
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.fab = (FloatingActionButton) findViewById(R.id.fab);
setPath();
fab.setOnClickListener(this);
}
/*设置动画路径*/
public void setPath(){
path = new AnimatorPath();
path.moveTo(0,0);
path.lineTo(400,400);
path.secondBesselCurveTo(600, 200, 800, 400); //订单
path.thirdBesselCurveTo(100,600,900,1000,200,1200);
}
/**
* 设置动画
* @param view
* @param propertyName
* @param path
*/
private void startAnimatorPath(View view, String propertyName, AnimatorPath path) {
ObjectAnimator anim = ObjectAnimator.ofObject(this, propertyName, new PathEvaluator(), path.getPoints().toArray());
anim.setInterpolator(new DecelerateInterpolator());//动画插值器
anim.setDuration(3000);
anim.start();
}
/**
* 设置View的属性通过ObjectAnimator.ofObject()的反射机制来调用
* @param newLoc
*/
public void setFab(PathPoint newLoc) {
fab.setTranslationX(newLoc.mX);
fab.setTranslationY(newLoc.mY);
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.fab:
startAnimatorPath(fab, "fab", path);
break;
}
}
}

上面代码中的:setPath()方法根据你自己项目的需要来设置不同的坐标 注意:(“这里的坐标是View以当前位置的偏移坐标,不是绝对坐标”)

上面代码中的:startAnimatorPath()参数就不介绍了注释中写的很清楚;这里直接看看ObjectAnimator.ofObject()方法的使用把:

ObjectAnimator.ofObject(this, propertyName, new PathEvaluator(), path.getPoints().toArray())

参数:this:View

参数:propertyName:属性名字 :起始这个名字是一个反射机制的调用,这样说不明白,看看这条代码:

ObjectAnimator.ofFloat(view, "scaleX", 0f, 1f).setDuration(500).start();

相信这句代码都能看懂,其中”scaleX”就相当于参数:propertyName

项目代码中我们传入的参数是:

startAnimatorPath(fab, "fab", path);

“fab”参数其实对应的就是setFab(PathPoint newLoc)方法,当我们在当前类中定义了该方法,就会自动通过反射的机制来调用该方法! ,如果还不懂,可以看看其它大神写的博客!

看看Xml中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="zhengliang.com.customanimationframework.MainActivity">
<zhengliang.com.customanimationframework.CustomView.PathView
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:targetApi="lollipop" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="40dp"
android:layout_height="40dp"
/>
</RelativeLayout>

为了可以清晰的看见小球的移动轨迹,自定义了以个View来显示小球的运动轨迹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class PathView extends View {
private Paint paint;
public PathView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
paint = new Paint();
//抗锯齿
paint.setAntiAlias(true);
//防抖动
paint.setDither(true);
//设置画笔未实心
paint.setStyle(Paint.Style.STROKE);
//设置颜色
paint.setColor(Color.GREEN);
//设置画笔宽度
paint.setStrokeWidth(3);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Path path = new Path();
path.moveTo(60,60);
path.lineTo(460,460);
path.quadTo(660, 260, 860, 460); //订单
path.cubicTo(160,660,960,1060,260,1260);
canvas.drawPath(path,paint);
}
}