Android自定義曲線路徑動畫框架

weixin_33866037發表於2016-11-09

Android自定義曲線路徑動畫框架

最近在一個專案中需要一個像QQ開啟個人愛好那樣的動畫效果如下圖:

可以看出每個小球都是以順時針旋轉出來的,說明像這樣的曲線動畫用Android中自帶的平移動畫是很難實現的。###

曲線動畫怎麼畫???##

我們先來看看Android自帶的繪製曲線的方式是怎樣的:

android自定義View中畫圖經常用到這幾個什麼什麼To

1、moveTo

moveTo 不會進行繪製,只用於移動移動畫筆,也就是確定繪製的起始座標點。結合以下方法進行使用。

2、lineTo

lineTo 用於進行直線繪製。

mPath.lineTo(300, 300);
canvas.drawPath(mPath, mPaint);

預設從座標(0,0)開始繪製。如圖:

2909848-5c16f5f9505fa721.png

剛才我們不是說了moveTo是用來移動畫筆的嗎?

mPath.moveTo(100, 100);
mPath.lineTo(300, 300);
canvas.drawPath(mPath, mPaint);

把畫筆移動(100,100)處開始繪製,效果如圖:

2909848-499f73306c7dff1d.png

3、quadTo

quadTo 用於繪製圓滑曲線,即貝塞爾曲線。

2909848-ec6165f8c1c0e1da.png

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

看看效果:

2909848-1b6ad4bbde273f04.png

一模一樣!如果我們不加 moveTo 呢?###

則以(0,0)為起點,(100,500)和(300,100)為控制點繪製貝塞爾曲線:

2909848-7d787885d723803c.png

受到上面的啟發,我們也可以用同樣的方法來實現一個曲線動畫框架

在寫框架之前我們必須要先了解一樣東西:

貝塞爾曲線:

維基百科中這樣說到:

在數學的數值分析領域中,貝塞爾曲線(英語:Bézier curve)是計算機圖形學中相當重要的引數曲線。更高維度的廣泛化貝塞爾曲線就稱作貝塞爾曲面,其中貝塞爾三角是一種特殊的例項。

貝塞爾曲線於1962年,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來為汽車的主體進行設計。貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau演算法開發,以穩定數值的方法求出貝塞爾曲線。

1、線性貝塞爾曲線

給定點P0、P1,線性貝塞爾曲線只是一條兩點之間的直線。這條線由下式給出:

2909848-09bb7ec8a1d226f2.png

2909848-8706171f8c930664.gif

二次方貝塞爾曲線

二次方貝塞爾曲線的路徑由給定點P0、P1、P2的函式B(t)追蹤:

2909848-9ce591d9ba15896f.png

2909848-cd2d62e7e32da9e3.gif

三次方貝塞爾曲線

P0、P1、P2、P3四個點在平面或在三維空間中定義了三次方貝塞爾曲線。曲線起始於P0走向P1,並從P2的方向來到P3。一般不會經過P1或P2;這兩個點只是在那裡提供方向資訊。P0和P1之間的間距,決定了曲線在轉而趨進P2之前,走向P1方向的“長度有多長”。

曲線的引數形式為:

2909848-9e8914afa2a876e7.png

2909848-4075c5b5e0459196.gif

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

一般貝塞爾曲線方程

2909848-0aaa0b083936fd5b.png

對於四次曲線,可由線性貝塞爾曲線描述的中介點Q0、Q1、Q2、Q3,由二次貝塞爾曲線描述的點R0、R1、R2,和由三次貝塞爾曲線描述的點S0、S1所建構:

2909848-a2bd4fc4274ecfce.gif

那麼在上程式碼之前先看看我們最後實現出來的效果圖:

2909848-e906b52d90d0c55f.gif

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

先看看專案整體結構:

2909848-b45a7eeb53e1a1cf.png

下面是程式碼時間

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

記錄一下,方便以後使用,完事了額! 如果喜歡我的部落格可以直接下面:##

CSDN地址: http://blog.csdn.net/qq_23179075

專案地址Github:https://github.com/azhengyongqin/CustomAnimationFramework/tree/master

印象丶亮仔

相關文章