因為公司開發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的位置坐標點。 (感覺上面有點啰里啰嗦,表達能力欠缺)。 那么基本算法偽代碼可以如下:
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得關系偽代碼:
代碼實現如下: 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了,就不多做說明。如有不當之處,歡迎批評指正,老規矩最后上代碼:下載鏈接
新聞熱點
疑難解答