轉(zhuǎn)載請標(biāo)明出處:http://www.reibang.com/p/c0d7ad796cee
前言:
貝塞爾曲線又稱貝茲曲線跑杭,它的主要意義在于無論是直線或曲線都能在數(shù)學(xué)上予以描述。最初由保羅·德卡斯特里奧(Paul de Casteljau)于1959年運用德卡斯特里奧演算法開發(fā)(de Casteljau Algorithm)齐遵,在1962戒良,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發(fā)表体捏。目前廣泛應(yīng)用于圖形繪制領(lǐng)域來模擬光滑曲線,為計算機(jī)矢量圖形學(xué)奠定了基礎(chǔ)糯崎。在一些圖形處理軟件中都能見到貝塞爾曲線几缭,比如CorelDraw中翻譯成“貝賽爾工具”;而在Fireworks中叫“畫筆”沃呢;Photoshop中叫“鋼筆工具”年栓。下圖為Photoshop中用鋼筆繪制的貝塞爾曲線,共繪制了三條貝塞爾曲線:
數(shù)學(xué)表達(dá)
術(shù)語:數(shù)據(jù)點薄霜、控制線某抓、控制點、德卡斯特里奧算法惰瓜、一階否副,二階,三階崎坊,n階……
數(shù)據(jù)點:一條貝塞爾曲線的起始點和終結(jié)點都叫數(shù)據(jù)點备禀。
控制線:在圖中可以看到那條中心點為數(shù)據(jù)點的線段,每個數(shù)據(jù)點對應(yīng)一條控制線
控制點:就是控制線的端點奈揍,通過控制線隨著控制點的變化而變化曲尸;數(shù)據(jù)點和控制點決定一條貝塞爾曲線。
-
一階貝塞爾曲線:其實是一條直線段男翰,沒有控制點另患。
-
二階貝塞爾曲線:圖中第二段為二階貝塞爾曲線,只有一個控制點奏篙,即只有一個控制點和兩個數(shù)據(jù)點來決定曲線形狀柴淘。
二階公式推導(dǎo):
根據(jù)控制點和數(shù)據(jù)點迫淹,對貝塞爾曲線進(jìn)行約束,滿足的條件為
問題變?yōu)椋阂阎狿0(x0,y0), P1(x1,y1), P2(x2,y2)为严,根據(jù)上式求P點坐標(biāo)敛熬?
先求出A、B點坐標(biāo)第股,其坐標(biāo)
PA=P0+(P1-P0)·t
PB=P1+(P2-P1)·t
之后求P點坐標(biāo)应民,其坐標(biāo)
P=PA+(PB-PA)·t
P=(1-t)2P0+2t(1-t)P1+t2P2, t∈[0,1]
- 三階貝塞爾曲線:圖中第三段為三階貝塞爾曲線夕吻,有兩個控制點和兩個數(shù)據(jù)點決定的曲線诲锹,同樣滿足等比條件:
德卡斯特里奧算法的思想:給定數(shù)據(jù)點和控制點P0、P1…Pn涉馅,首先將數(shù)據(jù)點和控制點連接形成一條折線归园,計算出每條折線上面的一點,使得初始數(shù)據(jù)點(初始控制點)到該點的距離與初始數(shù)據(jù)點(初始控制點)到終止數(shù)據(jù)點(終止控制點)的距離之比為t:1稚矿。將這些點連接起來形成新的折線(折線少了一段)庸诱,用遞歸的算法繼續(xù)計算,指導(dǎo)只有兩個點晤揣,在這兩個點形成的線段上去一點桥爽,滿足以上的比例關(guān)系。隨著t的從0到1的變化昧识,該點的集合形成了貝塞爾曲線钠四。
n階貝塞爾曲線公式如下。
Android中的應(yīng)用
對android中如何獲取貝塞爾曲線上的點跪楞,如何繪制貝塞爾曲線缀去,以及結(jié)合貝塞爾曲線的知識做出的效果進(jìn)行分析。
1. 獲取貝塞爾曲線上點的坐標(biāo)
在android中并沒有直接獲取的方法习霹,因此需要利用上面的公式進(jìn)行計算朵耕,以二階貝塞爾曲線為例,獲取51個點淋叶,主要是t從0開始到1間取51項的等差數(shù)列:
public class MainActivity extends AppCompatActivity {
private static final float mPointNum = 50f;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
PointF mStartPoint = new PointF(0, 0);
PointF mEndPoint = new PointF(0, 1200);
PointF mControlPoint = new PointF(500, 600);
List<PointF> mPointList = new ArrayList<>();
for (int i = 0; i <= mPointNum; i++) {
mPointList.add(getBezierPoint(mStartPoint, mEndPoint, mControlPoint, i / mPointNum));
Log.d("Bezier", "X:" + mPointList.get(i).x + " Y:" + mPointList.get(i).y);
}
}
private PointF getBezierPoint(PointF start, PointF end, PointF control, float t) {
PointF bezierPoint = new PointF();
bezierPoint.x = (1 - t) * (1 - t) * start.x + 2 * t * (1 - t) * control.x + t * t * end.x;
bezierPoint.y = (1 - t) * (1 - t) * start.y + 2 * t * (1 - t) * control.y + t * t * end.y;
return bezierPoint;
}
}```
如果需要更高階,可以使用遞歸函數(shù)來運算
```java
//用遞歸獲取貝塞爾曲線點的x軸坐標(biāo)
private float getBezierPointX(int n, int position, float t) {
if (n == 1) {
return (1 - t) * mPointList.get(position).x + t * mPointList.get(position + 1).x;
}
return (1 - t) * getBezierPointX(n - 1, position, t) + t * getBezierPointX(n - 1, position + 1, t);
}
//用遞歸獲取貝塞爾曲線點的x軸坐標(biāo)
private float getBezierPointX(int n, int position, float t) {
if (n == 1) {
return (1 - t) * mPointList.get(position).x + t * mPointList.get(position + 1).x;
}
return (1 - t) * getBezierPointX(n - 1, position, t) + t * getBezierPointX(n - 1, position + 1, t);
}
private ArrayList<PointF> buildBezierPoints() {
ArrayList<PointF> points = new ArrayList<>();
int order = mPointList.size() - 1;
float delta = 1.0f / POINT_NUM;
for (float t = 0; t <= 1; t += delta) {
// Bezier點集
points.add(new PointF(getBezierPointX(order, 0, t), getBezierPointY(order, 0, t)));
}
return points;
}
2. 繪制貝塞爾曲線
Android 中的Path類可以直接繪制一階到三階的貝塞爾曲線伪阶,在onDraw(Canvas canvas) 方法中使用:
- 繪制一階貝塞爾曲線:
canvas.drawLine(start.x,start.y,end.x,end.y);```
* 繪制二階貝塞爾曲線:
```java
mPath.moveTo(startPoint.x, startPoint.y);//起點
mPath.quadTo(controlPoint1.x, controlPoint1.y, endPoint.x, endPoint.y);
canvas.drawPath(mPath, mPaint);```
* 繪制三階貝塞爾曲線:
```java
mPath.moveTo(startPoint.x, startPoint.y);//起點
mPath.cubicTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, endPoint.x, endPoint.y);
canvas.drawPath(mPath, mPaint);```
* 繪制n階貝塞爾曲線
n階貝塞爾曲線繪制煞檩,需要結(jié)合遞歸函數(shù),設(shè)定一條曲線由多少個點組成栅贴,通過循環(huán)獲取每個點的坐標(biāo)進(jìn)行繪制斟湃。
![7階貝塞爾曲線](http://upload-images.jianshu.io/upload_images/1903970-204ebcd675f7a0f1.gif?imageMogr2/auto-orient/strip)
![8階貝塞爾曲線](http://upload-images.jianshu.io/upload_images/1903970-a93923157b0c6a70.gif?imageMogr2/auto-orient/strip)
### 3.Demo
貝塞爾曲線在android中最常用的是做出一些動畫特效,如QQ消息數(shù)拖拽形變效果檐薯,一些炫酷的下拉刷新控件凝赛,閱讀軟件的翻書效果注暗,一些平滑的折線圖的制作,某圖片的運動軌跡等……
##### 3.1 形狀變形
只需要知道變形前和變形后圖形的數(shù)據(jù)點和控制點即可墓猎,通過改變數(shù)據(jù)點和控制點使得形狀發(fā)生改變捆昏。
![變形](http://upload-images.jianshu.io/upload_images/1903970-8a62b7b1d05dfa9b.gif?imageMogr2/auto-orient/strip)
初始化
```java
private float[] mData = new float[8]; // 順時針記錄繪制圓形的四個數(shù)據(jù)點
private float[] mCtrl = new float[16]; // 順時針記錄繪制圓形的八個控制點
private float mDuration = 1000; // 變化總時長
private float mCurrent = 0; // 當(dāng)前已進(jìn)行時長
private float mCount = 100; // 將時長總共劃分多少份
private float mPiece = mDuration / mCount; // 每一份的時長```
在onDraw(Canvas canvas)方法中繪制
```java
path.reset();
path.moveTo(mData[0], mData[1]);
path.cubicTo(mCtrl[0], mCtrl[1], mCtrl[2], mCtrl[3], mData[2], mData[3]);
path.cubicTo(mCtrl[4], mCtrl[5], mCtrl[6], mCtrl[7], mData[4], mData[5]);
path.cubicTo(mCtrl[8], mCtrl[9], mCtrl[10], mCtrl[11], mData[6], mData[7]);
path.cubicTo(mCtrl[12], mCtrl[13], mCtrl[14], mCtrl[15], mData[0], mData[1]);
canvas.drawPath(path, mPaint);
mCurrent += mPiece;
if (mCurrent < mDuration) {
mData[1] -= 120 / mCount;
mCtrl[7] += 80 / mCount;
mCtrl[9] += 80 / mCount;
mCtrl[4] -= 20 / mCount;
mCtrl[10] += 20 / mCount;
postInvalidateDelayed((long) mPiece);
}
3.2 漂浮的愛心
愛心的漂浮軌跡就是一條三階貝塞爾曲線,結(jié)合屬性動畫中的估值器進(jìn)行設(shè)置毙沾。
- 首先定義一個屬性動畫的估值器
public class BezierEvaluator implements TypeEvaluator<PointF> {
private PointF mControlP1;
private PointF mControlP2;
public BezierEvaluator(PointF controlP1, PointF controlP2) {
this.mControlP1 = controlP1;
this.mControlP2 = controlP2;
}
@Override
public PointF evaluate(float time, PointF start, PointF end) {
float timeLeft = 1.0f - time;
PointF point = new PointF();
point.x = timeLeft * timeLeft * timeLeft * (start.x) + 3 * timeLeft * timeLeft * time *
(mControlP1.x) + 3 * timeLeft * time *
time * (mControlP2.x) + time * time * time * (end.x);
point.y = timeLeft * timeLeft * timeLeft * (start.y) + 3 * timeLeft * timeLeft * time *
(mControlP1.y) + 3 * timeLeft * time *
time * (mControlP2.y) + time * time * time * (end.y);
return point;
}
}```
* 之后自定義一個view可以生成愛心骗卜,添加透明度,縮放等動畫和根據(jù)貝塞爾曲線改變其位置的屬性動畫左胞。
初始化愛心圖片和多個插值器等寇仓,到時隨即選取
```java
private void init() {
// 初始化顯示的圖片
drawables = new Drawable[3];
drawables[0] = getResources().getDrawable(R.drawable.red);
drawables[1] = getResources().getDrawable(R.drawable.yellow);
drawables[2] = getResources().getDrawable(R.drawable.green);
// 初始化插補(bǔ)器
mInterpolators = new Interpolator[4];
mInterpolators[0] = new LinearInterpolator();// 線性
mInterpolators[1] = new AccelerateInterpolator();// 加速
mInterpolators[2] = new DecelerateInterpolator();// 減速
mInterpolators[3] = new AccelerateDecelerateInterpolator();// 先加速后減速
// 底部 并且 水平居中
dWidth = drawables[0].getIntrinsicWidth();
dHeight = drawables[0].getIntrinsicHeight();
lp = new LayoutParams(dWidth, dHeight);
lp.addRule(CENTER_HORIZONTAL, TRUE);// 這里的TRUE 要注意 不是true
lp.addRule(ALIGN_PARENT_BOTTOM, TRUE);
}```
入場動畫
```java
private AnimatorSet getEnterAnimator(final View target) {
ObjectAnimator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, 0.2f, 1f);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, 0.2f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, 0.2f, 1f);
AnimatorSet enter = new AnimatorSet();
enter.setTarget(target);
enter.setInterpolator(new LinearInterpolator());
enter.setDuration(500).playTogether(alpha, scaleX, scaleY);
return enter;
}```
貝塞爾曲線動畫
```java
private ValueAnimator getBezierValueAnimator(final View target) {
// 初始化貝塞爾估值器
BezierEvaluator evaluator = new BezierEvaluator(getPointF(2), getPointF(1));
// 起點在底部中心位置,終點在底部隨機(jī)一個位置
ValueAnimator animator = ValueAnimator.ofObject(evaluator, new PointF((mWidth - dWidth) /
2, mHeight - dHeight), new PointF(random.nextInt(getWidth()), 0));
animator.setTarget(target);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// 這里獲取到貝塞爾曲線計算出來的的x y值 賦值給view 這樣就能讓愛心隨著曲線走啦
PointF pointF = (PointF) valueAnimator.getAnimatedValue();
target.setX(pointF.x);
target.setY(pointF.y);
// alpha動畫
target.setAlpha(1 - valueAnimator.getAnimatedFraction());
}
});
animator.setDuration(3000);
return animator;
}
結(jié)合動畫添加愛心
public void addHeart() {
final ImageView imageView = new ImageView(getContext());
// 隨機(jī)選一個愛心
imageView.setImageDrawable(drawables[random.nextInt(3)]);
imageView.setLayoutParams(lp);
addView(imageView);
AnimatorSet finalSet = new AnimatorSet();
AnimatorSet enterAnimatorSet = getEnterAnimator(imageView);//入場動畫
ValueAnimator bezierValueAnimator = getBezierValueAnimator(imageView);//貝塞爾曲線路徑動畫
finalSet.playSequentially(enterAnimatorSet, bezierValueAnimator);
finalSet.setInterpolator(mInterpolators[random.nextInt(4)]);
finalSet.setTarget(imageView);
finalSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
removeView((imageView));//刪除愛心
}
});
finalSet.start();
}