属性动画简单说明前篇(一)

因为公司开发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值

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值