Android自定義曲線路徑動畫框架
最近在一個專案中需要一個像QQ開啟個人愛好那樣的動畫效果如下圖:
可以看出每個小球都是以順時針旋轉出來的,說明像這樣的曲線動畫用Android中自帶的平移動畫是很難實現的。###
曲線動畫怎麼畫???##
我們先來看看Android自帶的繪製曲線的方式是怎樣的:
android自定義View中畫圖經常用到這幾個什麼什麼To
1、moveTo
moveTo 不會進行繪製,只用於移動移動畫筆,也就是確定繪製的起始座標點。結合以下方法進行使用。
2、lineTo
lineTo 用於進行直線繪製。
mPath.lineTo(300, 300);
canvas.drawPath(mPath, mPaint);
預設從座標(0,0)開始繪製。如圖:
剛才我們不是說了moveTo是用來移動畫筆的嗎?
mPath.moveTo(100, 100);
mPath.lineTo(300, 300);
canvas.drawPath(mPath, mPaint);
把畫筆移動(100,100)處開始繪製,效果如圖:
3、quadTo
quadTo 用於繪製圓滑曲線,即貝塞爾曲線。
4、cubicTo
cubicTo 同樣是用來實現貝塞爾曲線的。mPath.cubicTo(x1, y1, x2, y2, x3, y3) (x1,y1) 為控制點,(x2,y2)為控制點,(x3,y3) 為結束點。那麼,cubicTo 和 quadTo 有什麼不一樣呢?說白了,就是多了一個控制點而已。然後,我們想繪製和上一個一樣的曲線,應該怎麼寫呢?
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演算法開發,以穩定數值的方法求出貝塞爾曲線。
1、線性貝塞爾曲線
給定點P0、P1,線性貝塞爾曲線只是一條兩點之間的直線。這條線由下式給出:
二次方貝塞爾曲線
二次方貝塞爾曲線的路徑由給定點P0、P1、P2的函式B(t)追蹤:
三次方貝塞爾曲線
P0、P1、P2、P3四個點在平面或在三維空間中定義了三次方貝塞爾曲線。曲線起始於P0走向P1,並從P2的方向來到P3。一般不會經過P1或P2;這兩個點只是在那裡提供方向資訊。P0和P1之間的間距,決定了曲線在轉而趨進P2之前,走向P1方向的“長度有多長”。
曲線的引數形式為:
以上都是維基百科給出的定義,以及不同曲線的公式和效果圖; 如果不清楚可以自己百度搜尋或者維基百科搜尋,麼麼噠!
一般貝塞爾曲線方程
對於四次曲線,可由線性貝塞爾曲線描述的中介點Q0、Q1、Q2、Q3,由二次貝塞爾曲線描述的點R0、R1、R2,和由三次貝塞爾曲線描述的點S0、S1所建構:
那麼在上程式碼之前先看看我們最後實現出來的效果圖:
運動路徑自己想怎麼設定就怎麼設定,是不是感覺很裝逼,好了下面正式開擼...###
先看看專案整體結構:
下面是程式碼時間
PathPoint.java中的程式碼:
/**
* 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類
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<T>
可以很簡單的實現這個難題。這裡我就把它叫做"估值器".我們只要建立一個類來實現這個介面,然後通過自己計算公式(就是我們上面的貝塞爾曲線公式)##
下面來看看我專案中的估值器類:PathEvaluator
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中的程式碼:
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中的程式碼:
<?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來顯示小球的運動軌跡:
/**
* 時 間: 2016/11/8 0008
* 作 者: 鄭亮
* Q Q : 1023007219
*/
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);
}
}
記錄一下,方便以後使用,完事了額! 如果喜歡我的部落格可以直接下面:##
印象丶亮仔