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

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

記錄一下,方便以后使用,完事了額! 如果喜歡我的博客可以直接下面:##

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

項目地址Github:https://github.com/azhengyongqin/CustomAnimationFramework/tree/master

印象丶亮仔

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內容