Android動(dòng)畫——Bezier曲線

1. 了解Bezier曲線

如何表示一條曲線,能夠精確地控制曲線的路徑,一直以來是一個(gè)很困難的問題。Bezier曲線就是利用數(shù)學(xué)公式,能夠精確描述一條我們想要的曲線。主要是由起始點(diǎn)、終止點(diǎn)和控制點(diǎn)三個(gè)部分組成,其中控制點(diǎn)是控制曲線的關(guān)鍵。

接下來看一下具體的實(shí)現(xiàn):

  • 一階Bezier曲線


    一階動(dòng)畫.gif

    一階Bezier曲線,是兩個(gè)點(diǎn)的連線,是一條直線。

  • 二階Bezier曲線


    二階動(dòng)畫.gif

    其中的p0和p2分別是起始點(diǎn)和終止點(diǎn),p1即是控制點(diǎn)。在三個(gè)點(diǎn)形成的兩條線段上,選取各自的起始位置然后向各自的終點(diǎn)位置移動(dòng),并且將兩個(gè)點(diǎn)連接成一條輔助線,在這條輔助線上同樣有一個(gè)點(diǎn)從起始位置移動(dòng)到重點(diǎn)位置,這個(gè)點(diǎn)與p0點(diǎn)的連線就是一條二階Bezier曲線。

  • 三階Bezier曲線


    三階動(dòng)畫.gif

    可以看出與二階Bezier曲線類似,只不過是控制點(diǎn)變成了2個(gè),形成的三條線段構(gòu)成了兩條輔助線,在這兩條輔助線上又構(gòu)造了一條輔助線,并且其運(yùn)動(dòng)的點(diǎn)與p0的連線構(gòu)成一條三階Bezier曲線。

在Android中,提供了二階和三階的實(shí)現(xiàn)api,對于其他多階Bezier曲線通過二階和三階的拼接也能達(dá)到同樣地效果。

2. Bezier曲線Demo

接下來通過兩個(gè)Demo來簡單認(rèn)識一下Bezier曲線的實(shí)現(xiàn)。

2.1 二階Bezier曲線

首先創(chuàng)建SecondBezierActivity及其布局,然后創(chuàng)建一個(gè)自定義的SecondBezierView:

public class SecondBezierActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second_bezier);
    }
}
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.blue.animationart.SecondBezierActivity">

    <com.blue.animationart.view.SecondBezierView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>

接下來,我們著重看一下SecondBezierView.java這個(gè)自定義view的實(shí)現(xiàn)。
先創(chuàng)建一系列坐標(biāo)點(diǎn):

    private float mStartPointX;
    private float mStartPointY;
    private float mEndPointX;
    private float mEndPointY;
    private float mFlagPointX;
    private float mFlagPointY;

在onSizeChanged方法里對這些坐標(biāo)點(diǎn)進(jìn)行賦值:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 起始點(diǎn)橫坐標(biāo)是屏幕寬度的四分之一處
        mStartPointX = w / 4;
       // 起始點(diǎn)縱坐標(biāo)是屏幕高度的一半再減200
        mStartPointY = h / 2 - 200;
        mEndPointX = w * 3 / 4;
        mEndPointY = h / 2 - 200;
        mFlagPointX = w / 2;
        mFlagPointY = h - 400;
    }

在onDraw方法里對曲線進(jìn)行繪制,在繪制之前需要?jiǎng)?chuàng)建一個(gè)Path路徑描述類的實(shí)例和對應(yīng)的Paint畫筆實(shí)例:

    private Path mPath;
    // 曲線的畫筆
    private Paint mPaintBezier;
    // 控制點(diǎn)的畫筆
    private Paint mPaintFlag;
... ...
    public SecondBezierView(Context context, AttributeSet attrs) {
        super(context, attrs);
       // 消除鋸齒
        mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintBezier.setStrokeWidth(8);
        mPaintBezier.setStyle(Paint.Style.STROKE);

        mPaintFlag = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintFlag.setStrokeWidth(3);
        mPaintFlag.setStyle(Paint.Style.STROKE);
    }
... ...
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        ... ...
        mPath = new Path();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 讓Path恢復(fù),養(yǎng)成良好的習(xí)慣
        mPath.reset();
        // 將Path移動(dòng)到初始位置點(diǎn)
        mPath.moveTo(mStartPointX, mStartPointY);
        // quadTo即二階Bezier曲線的Android API方法,前兩個(gè)參數(shù)是控制點(diǎn)坐標(biāo),后兩個(gè)參數(shù)是終止點(diǎn)坐標(biāo)
        mPath.quadTo(mFlagPointX, mFlagPointY, mEndPointX, mEndPointY);
       // 下面是繪制一些輔助的點(diǎn)和線段
        canvas.drawPoint(mStartPointX, mStartPointY, mPaintFlag);
        canvas.drawPoint(mEndPointX, mEndPointY, mPaintFlag);
        canvas.drawPoint(mFlagPointX, mFlagPointY, mPaintFlag);
        canvas.drawLine(mStartPointX, mStartPointY, mFlagPointX, mFlagPointY, mPaintFlag);
        canvas.drawLine(mEndPointX, mEndPointY, mFlagPointX, mFlagPointY, mPaintFlag);
        // 繪制曲線
        canvas.drawPath(mPath, mPaintBezier);
    }
二階Demo.png

接下來對其進(jìn)行拓展,在手機(jī)屏幕上觸摸會(huì)改變控制點(diǎn)的坐標(biāo),以達(dá)到動(dòng)態(tài)改變二階Bezier曲線的目的。實(shí)現(xiàn)起來很簡單,只需要實(shí)現(xiàn)onTouchEvent,獲取MotionEvent.ACTION_MOVE,把當(dāng)前的坐標(biāo)點(diǎn)賦值給控制點(diǎn),最后刷新即可:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mFlagPointX = event.getX();
                mFlagPointY = event.getY();
                invalidate();
                break;
        }
        return true;
    }
二階動(dòng)態(tài)動(dòng)畫.gif

2.2 三階Bezier曲線

與二階Bezier曲線的原理類似,還是先創(chuàng)建所承載的activity和自定義的view:

public class ThridBezierActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thrid_bezier);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.blue.animationart.ThridBezierActivity">

    <com.blue.animationart.view.ThridBezierView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>

接下來也是著重看一下自定義的view實(shí)現(xiàn)。

定義變量,初始化畫筆實(shí)例:

    private float mStartPointX;
    private float mStartPointY;
    private float mEndPointX;
    private float mEndPointY;

    private float mFlagPointOneX;
    private float mFlagPointOneY;
    private float mFlagPointTwoX;
    private float mFlagPointTwoY;

    private Path mPath;
    private Paint mPaintBezier;
    private Paint mPaintFlag;

    public ThridBezierView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintBezier.setStrokeWidth(8);
        mPaintBezier.setStyle(Paint.Style.STROKE);

        mPaintFlag = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintFlag.setStrokeWidth(3);
        mPaintFlag.setStyle(Paint.Style.STROKE);
    }

在onSizeChanged方法中對坐標(biāo)點(diǎn)進(jìn)行賦值:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mStartPointX = w / 4 - 100;
        mStartPointY = h / 2 - 200;
        mEndPointX = w * 3 / 4 + 100;
        mEndPointY = h / 2 - 200;

        mFlagPointOneX = w / 2 - 200;
        mFlagPointOneY = h / 2 + 200;

        mFlagPointTwoX = w / 2 + 200;
        mFlagPointTwoY = h / 2 + 100;

        mPath = new Path();
    }

在onDraw方法對曲線和坐標(biāo)點(diǎn)等進(jìn)行繪制:

        mPath.reset();
        mPath.moveTo(mStartPointX, mStartPointY);
        // 三階Bezier曲線的API是cubicTo,同樣地前四個(gè)參數(shù)對應(yīng)兩個(gè)控制點(diǎn)的坐標(biāo),后兩個(gè)參數(shù)對應(yīng)終止點(diǎn)坐標(biāo)
        mPath.cubicTo(mFlagPointOneX, mFlagPointOneY, mFlagPointTwoX, mFlagPointTwoY, mEndPointX, mEndPointY);

        canvas.drawPoint(mStartPointX, mStartPointY, mPaintFlag);
        canvas.drawPoint(mEndPointX, mEndPointY, mPaintFlag);
        canvas.drawPoint(mFlagPointOneX, mFlagPointOneY, mPaintFlag);
        canvas.drawLine(mStartPointX, mStartPointY, mFlagPointOneX, mFlagPointOneY, mPaintFlag);
        canvas.drawLine(mEndPointX, mEndPointY, mFlagPointTwoX, mFlagPointTwoY, mPaintFlag);
        canvas.drawLine(mFlagPointOneX, mFlagPointOneY, mFlagPointTwoX, mFlagPointTwoY, mPaintFlag);
        canvas.drawPath(mPath, mPaintBezier);
三階Bezier
  • 總結(jié)
  1. Android只提供二階和三階的實(shí)現(xiàn),多階可以通過拼接方式來實(shí)現(xiàn);
  2. 在自定義view中,先賦值坐標(biāo)點(diǎn),然后初始化Paint對象設(shè)置樣式,最后在onDraw方法中進(jìn)行繪制;
  3. 二階對應(yīng)方法quadTo方法,三階對應(yīng)cubicTo方法。

3. Bezier曲線實(shí)踐

3.1 路徑變換

之前的文章提到過,VectorDrawable在L版本以下是不支持路徑變換動(dòng)畫的,我們可以通過Bezier曲線來實(shí)現(xiàn)L版本以下的路徑變換。

path.gif
  • 分析
    能夠看出,是利用三階Bezier曲線所實(shí)現(xiàn),通過屬性動(dòng)畫控制兩個(gè)控制點(diǎn)的坐標(biāo)向下運(yùn)動(dòng),從而帶動(dòng)曲線跟著運(yùn)動(dòng),進(jìn)而實(shí)現(xiàn)了路徑的變換動(dòng)畫。

  • 實(shí)現(xiàn)
    創(chuàng)建對應(yīng)的activity和自定義的view,與前面的demo的方式相同,在此不多做介紹。

下面來實(shí)現(xiàn)這個(gè)自定義view。

首先還是創(chuàng)建坐標(biāo)點(diǎn)和Paint畫筆的變量:

    private float mStartPointX;
    private float mStartPointY;

    private float mEndPointX;
    private float mEndPointY;

    private float mFlagPointOneX;
    private float mFlagPointOneY;
    private float mFlagPointTwoX;
    private float mFlagPointTwoY;

    private Path mPath;
    private Paint mPaintBezier;
    private Paint mPaintFlag;

然后設(shè)置畫筆的樣式:

    public PathMorphingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintBezier.setStrokeWidth(8);
        mPaintBezier.setStyle(Paint.Style.STROKE);

        mPaintFlag = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintFlag.setStrokeWidth(3);
        mPaintFlag.setStyle(Paint.Style.STROKE);
    }

接著在onSizeChanged給坐標(biāo)點(diǎn)賦值,此處我們給控制點(diǎn)坐標(biāo)初始賦值為起始點(diǎn)和終止點(diǎn)坐標(biāo),讓曲線初始狀態(tài)是直線:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mStartPointX = w / 4;
        mStartPointY = h / 2 - 200;
        mEndPointX = w * 3 / 4;
        mEndPointY = h / 2 - 200;

        mFlagPointOneX = mStartPointX;
        mFlagPointOneY = mStartPointY;

        mFlagPointTwoX = mEndPointX;
        mFlagPointTwoY = mEndPointY;

        mPath = new Path();
    }

在onDraw方法中對曲線進(jìn)行繪制:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        mPath.moveTo(mStartPointX, mStartPointY);
        mPath.cubicTo(mFlagPointOneX, mFlagPointOneY, mFlagPointTwoX, mFlagPointTwoY, mEndPointX, mEndPointY);

        canvas.drawPoint(mStartPointX, mStartPointY, mPaintFlag);
        canvas.drawPoint(mEndPointX, mEndPointY, mPaintFlag);
        canvas.drawPoint(mFlagPointOneX, mFlagPointOneY, mPaintFlag);
        canvas.drawLine(mStartPointX, mStartPointY, mFlagPointOneX, mFlagPointOneY, mPaintFlag);
        canvas.drawLine(mEndPointX, mEndPointY, mFlagPointTwoX, mFlagPointTwoY, mPaintFlag);
        canvas.drawLine(mFlagPointOneX, mFlagPointOneY, mFlagPointTwoX, mFlagPointTwoY, mPaintFlag);
        canvas.drawPath(mPath, mPaintBezier);
    }

接下來就是重要的屬性動(dòng)畫的實(shí)現(xiàn):

   private ValueAnimator mValueAnimator;
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
       ... ...
        // 屬性動(dòng)畫數(shù)值增長的范圍
        mValueAnimator = ValueAnimator.ofFloat(mStartPointY, h);
        // 彈性效果的插值器
        mValueAnimator.setInterpolator(new BounceInterpolator());
        mValueAnimator.setDuration(1000);
        // 監(jiān)聽屬性動(dòng)畫所改變的值,并且將值設(shè)置給兩個(gè)控制點(diǎn)
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mFlagPointOneY = (float) valueAnimator.getAnimatedValue();
                mFlagPointTwoY = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        // 設(shè)置點(diǎn)擊事件
        setOnClickListener(this);
    }

在點(diǎn)擊事件中,播放動(dòng)畫:

    @Override
    public void onClick(View view) {
        mValueAnimator.start();
    }
  • 總結(jié)
  1. Bezier曲線的路徑變化動(dòng)畫可以解決VectorDrawable不能向下兼容的問題;
  2. Bezier曲線的路徑變化的實(shí)現(xiàn)原理就是通過屬性動(dòng)畫來動(dòng)態(tài)改變坐標(biāo)點(diǎn)的值,進(jìn)而帶動(dòng)改變曲線的路徑。

3.2 波浪運(yùn)動(dòng)

波浪動(dòng)畫在手機(jī)清理軟件和加載動(dòng)畫上運(yùn)用很多,運(yùn)用Bezier曲線的實(shí)現(xiàn)效果如下圖:


wave.gif
  • 分析
    要實(shí)現(xiàn)動(dòng)態(tài)地波浪效果,首先要知道怎么繪制一個(gè)靜態(tài)的波浪曲線。

波浪

desmos是一個(gè)繪制數(shù)學(xué)公式的工具。)

能夠看到,兩個(gè)二階Bezier曲線就能夠組成一個(gè)完整的波形曲線。將這個(gè)組裝而成的波形通過循環(huán)波長個(gè)數(shù)進(jìn)行繪制,就能達(dá)到一連串的波形曲線。而曲線的起始點(diǎn)、終止點(diǎn)和控制點(diǎn)的橫坐標(biāo)由屬性動(dòng)畫操作向右移動(dòng),達(dá)到曲線移動(dòng)的效果。但是要注意的是,需要在屏幕之外添加一個(gè)完整的波形,這樣在整個(gè)波形移動(dòng)的時(shí)候不會(huì)發(fā)生中斷。

  • 實(shí)現(xiàn)
    下面來實(shí)現(xiàn)這個(gè)自定義view。

首先初始化相應(yīng)的變量,并且確定需要繪制的波長個(gè)數(shù):

    private Path mPath;
    private Paint mPaintBezier;
    private int mWaveCount;

    private int mWaveLength;
    private int mScreenHeight, mScreenWidth;
    // 波形繪制的縱坐標(biāo)
    private int mCenterY;
    public WaveBezierView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintBezier.setColor(Color.BLUE);
        mPaintBezier.setStrokeWidth(8);
        mPaintBezier.setStyle(Paint.Style.FILL_AND_STROKE);
        // 定義波長的長度
        mWaveLength = 800;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mPath = new Path();
        setOnClickListener(this);
        mScreenHeight = h;
        mScreenWidth = w;
        mCenterY = h / 2;
        // 屏幕的寬度除以波長長度是屏幕所容納的個(gè)數(shù),1.5是屏幕之外的波長個(gè)數(shù)
        mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5);
    }

接下來是繪制波形:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        // 從屏幕外開始,所以需要初始位置在屏幕之外一個(gè)波長的距離
        mPath.moveTo(-mWaveLength, mCenterY);
        // 將之前計(jì)算而得的波長個(gè)數(shù)進(jìn)行循環(huán)繪制
        for (int i = 0; i < mWaveCount; i++) {
            // 控制點(diǎn)對應(yīng)著波峰和波谷,然后依次向后一個(gè)波長位置繼續(xù)繪制
            mPath.quadTo(-mWaveLength * 3 / 4 + i * mWaveLength, mCenterY + 60, -mWaveLength / 2 + i * mWaveLength, mCenterY);
            mPath.quadTo(-mWaveLength / 4 + i * mWaveLength, mCenterY - 60, i * mWaveLength, mCenterY);
        }
        // 圖形與屏幕下邊緣封閉,然后對封閉圖形填充顏色。
        mPath.lineTo(mScreenWidth, mScreenHeight);
        mPath.lineTo(0, mScreenHeight);
        mPath.close();
        canvas.drawPath(mPath, mPaintBezier);
    }

接下來對坐標(biāo)點(diǎn)的橫坐標(biāo)進(jìn)行偏移:

    private ValueAnimator mValueAnimator;
    // 偏移量
    private int moffset;
    ... ...
    @Override
    public void onClick(View view) {
        // 設(shè)置范圍
        mValueAnimator = ValueAnimator.ofInt(0, mWaveLength);
        mValueAnimator.setDuration(1000);
        mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        // 線性插值器
        mValueAnimator.setInterpolator(new LinearInterpolator());
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                // 給偏移量進(jìn)行賦值
                moffset = (int) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        mValueAnimator.start();
    }

將偏移量moffset作用到整個(gè)波形曲線上,只需要修改quadTo方法即可:

    @Override
    protected void onDraw(Canvas canvas) {
        ... ...
        mPath.moveTo(-mWaveLength + moffset, mCenterY);
        for (int i = 0; i < mWaveCount; i++) {
            mPath.quadTo(-mWaveLength * 3 / 4 + i * mWaveLength + moffset, mCenterY + 60, -mWaveLength / 2 + i * mWaveLength + moffset, mCenterY);
            mPath.quadTo(-mWaveLength / 4 + i * mWaveLength + moffset, mCenterY - 60, i * mWaveLength + moffset, mCenterY);
        }
        ... ...
    }   
  • 總結(jié)
  1. 運(yùn)用Bezier曲線實(shí)現(xiàn)路徑變化動(dòng)畫是不需要考慮兼容性問題的。
  2. 橫坐標(biāo)偏移的動(dòng)畫要考慮到屏幕之外也需要繪制。
  3. 繪制路徑變化動(dòng)畫的一般流程:畫出靜態(tài)的整個(gè)圖像,通過屬性動(dòng)畫改變坐標(biāo)點(diǎn)的坐標(biāo)。

3.3 模擬物體運(yùn)動(dòng)軌跡

movepath.gif

模擬添加物品到購物車的運(yùn)動(dòng)軌跡動(dòng)畫。

  • 分析
    此動(dòng)畫分為兩個(gè)部分,一個(gè)是Bezier曲線的繪制,一個(gè)是曲線上的點(diǎn)移動(dòng)。在Android API中沒有提供獲取Bezier曲線上點(diǎn)的坐標(biāo)值,需要通過一系列計(jì)算公式,計(jì)算出其坐標(biāo)值:
二階公式
三階公式

通過使用估值器TypeEvaluator來計(jì)算動(dòng)畫運(yùn)動(dòng)的值,根據(jù)這個(gè)值來讓點(diǎn)移動(dòng),從而達(dá)到Bezier曲線所規(guī)劃的運(yùn)動(dòng)軌跡。

  • 實(shí)現(xiàn)

首先實(shí)現(xiàn)計(jì)算Bezier曲線坐標(biāo)點(diǎn)的工具類:

public class BezierUtil {

    /**
     * B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
     *
     * @param t  曲線長度比例
     * @param p0 起始點(diǎn)
     * @param p1 控制點(diǎn)
     * @param p2 終止點(diǎn)
     * @return t對應(yīng)的點(diǎn)
     */
    public static PointF CalculateBezierPointForQuadratic(float t, PointF p0, PointF p1, PointF p2) {
        PointF point = new PointF();
        float temp = 1 - t;
        point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x;
        point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y;
        return point;
    }

    /**
     * B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1]
     *
     * @param t  曲線長度比例
     * @param p0 起始點(diǎn)
     * @param p1 控制點(diǎn)1
     * @param p2 控制點(diǎn)2
     * @param p3 終止點(diǎn)
     * @return t對應(yīng)的點(diǎn)
     */
    public static PointF CalculateBezierPointForCubic(float t, PointF p0, PointF p1, PointF p2, PointF p3) {
        PointF point = new PointF();
        float temp = 1 - t;
        point.x = p0.x * temp * temp * temp + 3 * p1.x * t * temp * temp + 3 * p2.x * t * t * temp + p3.x * t * t * t;
        point.y = p0.y * temp * temp * temp + 3 * p1.y * t * temp * temp + 3 * p2.y * t * t * temp + p3.y * t * t * t;
        return point;
    }
}

初始化相應(yīng)變量,繪制Bezier曲線,繪制曲線上的移動(dòng)點(diǎn):

    private int mStartPointX, mStartPointY, mEndPointX, mEndPointY;
    private int mFlagPointX, mFlagPointY;
    private int mMovePaintX, mMovePaintY;
    private Path mPath;
    private Paint mPaintPath;
    private Paint mPaintCircle;
    public PathBezierView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        mPath = new Path();
        mPaintPath = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintPath.setStyle(Paint.Style.STROKE);
        mPaintPath.setStrokeWidth(8);

        mPaintCircle = new Paint(Paint.ANTI_ALIAS_FLAG);

        mStartPointX = 100;
        mStartPointY = 100;
        mEndPointX = 600;
        mEndPointY = 600;
        mFlagPointX = 500;
        mFlagPointY = 0;
        mMovePaintX = mStartPointX;
        mMovePaintY = mStartPointY;
        setOnClickListener(this);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(mStartPointX, mStartPointY, 20, mPaintCircle);
        canvas.drawCircle(mEndPointX, mEndPointY, 20, mPaintCircle);
        canvas.drawCircle(mMovePaintX, mMovePaintY, 20, mPaintCircle);
        mPath.reset();
        mPath.moveTo(mStartPointX, mStartPointY);
        mPath.quadTo(mFlagPointX, mFlagPointY, mEndPointX, mEndPointY);
        canvas.drawPath(mPath, mPaintPath);
    }

創(chuàng)建估值器:

public class BezierEvaluator implements TypeEvaluator<PointF> {
    private PointF mFlagPoint;
    public BezierEvaluator(PointF flagPoint) {
        // 傳入控制點(diǎn)的坐標(biāo)
        mFlagPoint = flagPoint;
    }

    @Override
    public PointF evaluate(float v, PointF pointF, PointF t1) {
        // 參數(shù)v代表動(dòng)畫運(yùn)行的比例,其正好對應(yīng)CalculateBezierPointForQuadratic曲線長度比例的參數(shù)
        // return的值就是當(dāng)前運(yùn)動(dòng)的坐標(biāo)點(diǎn)對象
        return BezierUtil.CalculateBezierPointForQuadratic(v, pointF, mFlagPoint, t1);
    }
}

創(chuàng)建屬性動(dòng)畫:

    @Override
    public void onClick(View view) {
        BezierEvaluator evaluator = new BezierEvaluator(new PointF(mFlagPointX, mFlagPointY));
        ValueAnimator animator = ValueAnimator.ofObject(evaluator, new PointF(mStartPointX, mStartPointY), new PointF(mEndPointX, mEndPointY));
        animator.setDuration(600);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                // 獲取我們定義的BezierEvaluator 所計(jì)算的值
                PointF pointF = (PointF) valueAnimator.getAnimatedValue();
                mMovePaintX = (int) pointF.x;
                mMovePaintY = (int) pointF.y;
                invalidate();
            }
        });
        // 加速減速插值器
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.start();
    }
  • 總結(jié)
    模擬運(yùn)動(dòng)軌跡動(dòng)畫的一般步驟:構(gòu)建Bezier曲線,自定義估值器,在屬性動(dòng)畫上獲取曲線上運(yùn)動(dòng)的每個(gè)點(diǎn)坐標(biāo),將這些坐標(biāo)設(shè)置給運(yùn)動(dòng)的點(diǎn)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android動(dòng)畫的開發(fā)中,為了達(dá)到更加酷炫的效果,常常需要自定義運(yùn)動(dòng)軌跡,或者繪制花式復(fù)雜的曲線,這正是Bezi...
    登高且賦閱讀 11,902評論 5 26
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,034評論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,309評論 4 61
  • 看新聞時(shí),整天能聽到諸如日本右翼份子又要篡改歷史了、美國左派人士又看床破不順眼了…… 這類新聞中“左右”這種方位詞...
    d0fdb328d08b閱讀 2,681評論 7 15
  • 昨天一個(gè)人開車7小時(shí),從山東臨沂到河南鄭州,跨兩省行駛700余公里。由于父親身體出了點(diǎn)狀況,身邊需要人照顧,剛參加...
    就是來勁閱讀 375評論 0 0

友情鏈接更多精彩內(nèi)容