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

接下來對其進(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;
}

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

- 總結(jié)
- Android只提供二階和三階的實(shí)現(xiàn),多階可以通過拼接方式來實(shí)現(xiàn);
- 在自定義view中,先賦值坐標(biāo)點(diǎn),然后初始化Paint對象設(shè)置樣式,最后在onDraw方法中進(jìn)行繪制;
- 二階對應(yīng)方法quadTo方法,三階對應(yīng)cubicTo方法。
3. Bezier曲線實(shí)踐
3.1 路徑變換
之前的文章提到過,VectorDrawable在L版本以下是不支持路徑變換動(dòng)畫的,我們可以通過Bezier曲線來實(shí)現(xiàn)L版本以下的路徑變換。

分析
能夠看出,是利用三階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é)
- Bezier曲線的路徑變化動(dòng)畫可以解決VectorDrawable不能向下兼容的問題;
- 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)效果如下圖:

- 分析
要實(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é)
- 運(yùn)用Bezier曲線實(shí)現(xiàn)路徑變化動(dòng)畫是不需要考慮兼容性問題的。
- 橫坐標(biāo)偏移的動(dòng)畫要考慮到屏幕之外也需要繪制。
- 繪制路徑變化動(dòng)畫的一般流程:畫出靜態(tài)的整個(gè)圖像,通過屬性動(dòng)畫改變坐標(biāo)點(diǎn)的坐標(biāo)。
3.3 模擬物體運(yùn)動(dòng)軌跡

模擬添加物品到購物車的運(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)。


