因为公司开发SDK的原因,公司开发需要做各种动画UI特效,也算是对动画有一定的了解,所以准备写个博客巩固下。本篇就对贝塞尔曲线加上属性动画来说一下。 1、线性别塞尔曲线的知识说明 线性贝塞尔曲线的公式如下: B(t) = P0 + (P1 - P0)t 其中t的范围是[0,1](这个范围很是关键). 说白了就是两点Point0,Point1之间构成的一条直线(线段),其作用可以看做是从P0点到P1点的位移路径,假设A从P0走到P1,那么就是随着t的变换,A逐渐走P1点的一个过程。如下图(盗图): 我们知道一个点在平面中是有X,Y两个坐标点组成(特么的废话),假设Point0的坐标是(X0,Y0),Point1的坐标位(X1,Y1)那么A移动的过程中也即是随着t的渐变,A的横坐标点从X0逐渐移动到X1,纵坐标Y0逐渐移动到Y1的过程,用点来表示的话就是A经过一些列的点:(X0,Y0)–>(Xa,Ya)–>(Xb,Yb)–>…–>(X1,Y1)或者Point0–>PointA–>PointB–>…–>Point1才到Point1(此时t=1)。 我们在初中的时候学过直线方程y = kx +b是x跟y的关系,而贝塞尔曲线在应用中其实是x与t构成的直线函数以及y与t构成的直线函数关系: B(tx) = (X1-X0)t+X0 B(ty) = (Y1-Y0)t + Y0 所以如果在android中想要让一个View从一位置移动到另外一个位置,如果用线性贝塞尔曲线的话,就是根据上面的两个函数根据变量t不断修改View的x和y的位置即可;当然因为(x,y)构成一个点,所以就是让View随着t的改变,从一个点移动到新的点的过程直到Point1。那么核心算法就是根据当前t的值(t->[0,1])根据上面的两个函数获取当前的newX和newY构成的坐标点(newX,newY)更新view的位置坐标点。 (感觉上面有点啰里啰嗦,表达能力欠缺)。 那么基本算法伪代码可以如下:
Point point0 = new Point(x0,y0);Point point1 = new Point(x1,y1);int k0 = x1- x0;//x与t直线函数的斜率int k1 = y1 - y0;//y与t直线函数的斜率float t = 0f;while(t<=0){ //最新的x和y的位置 int newX = k0*t + x0; int newY = k1*t + y0; //更新view的位置方式1 LayoutParams params = view.getLayoutParams(); params.leftMargin = newX; params.rightMargin = newY; view.setLayoutParams(params); //t以某种规则递增,比如每次增加0.1 t+=0.1 }2、自己写一个小小的测试例子 根据是上面的说明以及伪代码例子程序如下: 代码也很简单,首先第一一个Point类,包换了x和y:
class Point { PRotected float x;//横坐标 protected float y;//纵坐标 public Point(float x,float y){ this.x = x; this.y = y; } }然后定义一个线性贝塞尔曲线计算器类,这个类需要先传入线段的起始点,然后根据t来计算对应的新的Point对象:
class BezierLine { private Point startPoint;//贝塞尔曲线起点 private Point endPoint;//贝塞尔曲线终点 public BezierLine(Point startPoint, Point endPoint) { this.startPoint = startPoint; this.endPoint = endPoint; } /** * 根据线性贝塞尔函数,获取线性贝塞尔曲线上的某个点 * @param t 在[0,1]范围的某一个值 * @return 根据t的不同而返回的贝塞尔曲线的点 */ public Point createBezierLine(float t){ float newX = (endPoint.x -startPoint.x)*t + startPoint.x; float newY = (endPoint.y -startPoint.y)*t + startPoint.y; return new Point(newX,newY); }}以上可以说完事具备,只欠东风,那么怎么使用上述贝塞尔曲线来更新呢?这里提供一个简单的思路,就是用Handler来发送不断发送消息,简单的代码如下:
private float t = 0.0f; private BezierLine bezierLine; private Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { t+=0.01f; if(t>1.0){ return; } //获取当前t对应的Point位置 Point newPoint = bezierLine.createBezierLine(t); //更新View的位置 updateViewLocation(newPoint); //继续发送消息 handler.sendEmptyMessage(0); } }; private void updateViewLocation(Point point){ moveParams.leftMargin = (int)point.x; moveParams.topMargin =(int)point.y; moveView.setLayoutParams(moveParams); }运行的效果如图所示: 当然这种简单的运动通过scrollTo也可以简单实现,关于滚动的详细说明,可参考《View的滚动原理简单解析》和《View的滚动原理简单解析2》。
到此位置demo结束,只是简单的实现了两个点之间的运动估计,如果点很多的情况下怎么处理呢?比如如果View要进行如下的运动轨迹该怎么办? 在回答这个问题之前需要思考或者准备如下问题(以路径B为例): 1)从P0到P4的所需时间是多少毫秒?(答案是未知,也就是用户可配) 2)从P0–>P1、P1–>P2、P2–>P3、P3–>P4四个线段之间移动所消耗的时间是一致的吗?(答案是不一定,他们的耗时又长又短,这其实是一个动画中的插值器的概念,比如让P0–>P1的时间最短,其余的线段之间速度也设置的不一样)当然本文为了方便说明在此定义为点从P0–>P1、P1–>P2、P2–>P3、P3–>P4四个线段之间所耗时是一样的。也就是说假设传入的运动时间为duration表示,点的总数用n表示,那么每个线段之间的耗时比例关系如下: 根据上图很容易就得出了这些点与t得关系伪代码:
//点总数 int n; //P0的起始时间为0 (0f,P0); //p0 ..pn各个点与t的对应点断数 int lineSegment = n-1; for (int i = 1; i < n; ++i) { ((float)i/lineSegment, Pi); }代码实现如下: 1)定义一个TPoint类来表示t和P0,P1,P2,P3,P4的关系
public class TPoint { protected float t; protected Point point; public TPoint(float t,Point point){ this.t = t; this.point = point; }}2)初始化点数P0,P1,P2,P3,P4列表,并且绑定各个点对应的t
public void bindTPoint(){ //pointList是一个ArrayList size = pointList.size(); tPoints = new TPoint[size]; tPoints[0] = new TPoint(0f,pointList.get(0)); //p0--p1构成的线段 int lineSegment = size -1; for(int i=1;i<size;i++){ tPoints[i] = new TPoint((float)i/lineSegment,pointList.get(i)); } }就这样完成了第一步的工作!
在第一个例子的时候 BezierLine方式提供了startPoint和endPoint两个起止点就可以了,但是现在有若干个点怎么办呢,所以在这里优先重构的的就是BezierLine这个类:
/** * 根据线性贝塞尔函数,获取线性贝塞尔曲线上的某个点 * @param t 在[0,1]范围的某一个值 * @param startPoint 贝塞尔曲线开始的点 * @param endPoint 贝塞尔曲线结束的点 * @return 根据t的不同而返回的贝塞尔曲线的点 */ public static Point createBezierLine(float t,Point startPoint,Point endPoint){ float newX = (endPoint.x -startPoint.x)*t + startPoint.x; float newY = (endPoint.y -startPoint.y)*t + startPoint.y; return new Point(newX,newY); }注意此时相邻两个点组成的路径的范围t仍然为[0,1];只不过t要换一种方法来解释,t对于相邻点之间有点类似于求进度的算法,一个作为起点一个作为终点,其数学公式如下:
那么根据上面的公式,根据当前时间获取最新位置点Point对象的代码如下:
public Point getNewPoint(){ //当前时间 long currentTime = System.currentTimeMillis(); //当前时间进度 float currentProgress = (float) (currentTime - startTime) / DURATION ; if(currentProgress>1.0){ finish = true; } //判断当前时间进度是在哪一个线段上 TPoint prePoint = tPoints[0]; for(int i=1;i<size;i++){ TPoint nextPoint = tPoints[i]; //运动的点在prePoint和nextPoint之间 if(currentProgress<nextPoint.t){ //当前点在当前路径的进度 float progress = (currentProgress-prePoint.t)/(nextPoint.t-prePoint.t); return BezierLine.createBezierLine(progress, prePoint.point, nextPoint.point); } prePoint = nextPoint; }//end for //其实这一步感觉不应该返回 return tPoints[size-1].point; }那么有了这个getNewPoint方法,直接调用第一个Demo中的updateViewLocation方法即可,同样是用handler来发送消息,并跟新位置,详细见文章最后代码下载链接,运行效果如图: 其实上面分析了这么多有点啰里啰嗦了,总结下来基本的算法思路很简单(在各个线段耗时相等的情况下,假设view从P0出发): 1)分配View 从P0到达P1,P2,P3,P4到这几个点的时间节点 1)根据当前时间和开始时间以及步骤1计算此时view应该位于哪一条路径上 2)计算view在当前路径相对当前路径起始点的进度。比如view此时位于p(n-1)和pn这条路径上,那么此进度当前路径的贝塞尔曲线的t值
本来本篇就涉及到属性动画的介绍的,但是昨天因为搬家折腾了一天,一篇博客写了两天,这样的话思路有点混乱了感觉,下一篇在分析属性动画的贝塞尔曲线实现方案吧,不过感觉应该跟本文的思路相差不大;另外本篇只讨论了线性的,抛物线的运动轨迹改下createBezierLine的方程就OK了,就不多做说明。如有不当之处,欢迎批评指正,老规矩最后上代码:下载链接
新闻热点
疑难解答