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方向的“長度有多長”。

曲線的參數形式為:

以上都是維基百科給出的定義,以及不同曲線的公式和效果圖; 如果不清楚可以自己百度搜索或者維基百科搜索,么么噠!

那么在上代碼之前先看看我們最后實現出來的效果圖:

運動路徑自己想怎么設置就怎么設置,是不是感覺很裝逼,好了下面正式開擼...

先看看項目整體結構:

下面是代碼時間

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);
}

}</code></pre>

這個類主要是用來記錄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;
}

}</code></pre>

該類是最終在客戶端使用的,記錄一系列的不同移動軌跡,使用時調用里面的方法就可以添加不同的移動軌跡最后通過 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);
}

}</code></pre>

泛型中傳入我們自己的定義的 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;
    }
}

}</code></pre>

上面代碼中的: 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="

<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></code></pre>

為了可以清晰的看見小球的移動軌跡,自定義了以個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);
}

}</code></pre>

 

 

來自:http://www.jianshu.com/p/f64c3cd25f67

 

 本文由用戶 sxcr8001 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!