學(xué)習(xí)資料:
十分感謝兩位大神 :)
1. Bezier
<p>
Bezier
是一個(gè)法國的數(shù)學(xué)家的名字评汰。在Path
中装蓬,lineTo()
方法是用來繪制直線的穗椅,quadTo()
和cubicTo()
來繪制曲線
Bezier
原理就是利用多個(gè)點(diǎn)的位置來確定出一條曲線洛波。這多個(gè)點(diǎn)就是起點(diǎn),終點(diǎn)昨登,控制點(diǎn)」岬祝控制點(diǎn)可以沒有丰辣,也可以有多個(gè)。個(gè)人感覺禽捆,除了這些必要的點(diǎn)外笙什,還可以虛擬出一個(gè)運(yùn)動(dòng)點(diǎn)
曲線可以看作是一個(gè)運(yùn)動(dòng)點(diǎn)的軌跡。在高中時(shí)胚想,數(shù)學(xué)大題中琐凭,往往會(huì)有一道讓求一個(gè)點(diǎn)的運(yùn)動(dòng)軌跡,一般結(jié)果是一個(gè)橢圓或者圓的數(shù)學(xué)公式顿仇。Bezier
的繪制曲線淘正,感覺也就是這個(gè)運(yùn)動(dòng)點(diǎn)的運(yùn)動(dòng)軌跡
1.1 一階貝塞爾曲線
<p>
沒有控制點(diǎn)摆马,一階貝塞爾曲線
這時(shí)臼闻,只有起點(diǎn)P0
和終點(diǎn)P1
,運(yùn)動(dòng)點(diǎn)在P0,P1
間的運(yùn)動(dòng)軌跡就是一條線段
公式:
B(t)
就是運(yùn)動(dòng)點(diǎn)在t
時(shí)刻的坐標(biāo)囤采,p0
起點(diǎn)述呐,p1
終點(diǎn)
對(duì)應(yīng)的就是lineTo()
方法
圖和公式來自愛哥的博客
1.2 二階貝塞爾曲線
<p>
一個(gè)控制點(diǎn),二階貝塞爾曲線
起點(diǎn)P0
和終點(diǎn)P2
蕉毯,控制點(diǎn)就是P1
乓搬,運(yùn)動(dòng)點(diǎn)在P0,P1思犁,P2
三個(gè)點(diǎn)的約束下,運(yùn)動(dòng)形成的軌跡就是紅色的曲線
公式:
二階對(duì)應(yīng)的方法就是quadTo()
1.3 三階貝塞爾曲線
<p>
兩個(gè)個(gè)控制點(diǎn)进肯,三階貝塞爾曲線
紅色就是運(yùn)動(dòng)點(diǎn)的軌跡激蹲,也就是最終會(huì)繪制的曲線
公式:
三階對(duì)應(yīng)的方法就是cubicTo()
幸虧Path
類對(duì)計(jì)算過程做了封裝 : )
2. 模擬向杯子中倒水
<p>
主要的思路,就是起點(diǎn)江掩,終點(diǎn)学辱,控制點(diǎn)的Y
坐標(biāo)不斷減小,屏幕頂部的``Y軸坐標(biāo)為
0环形,向屏幕上方偏移策泣,也就是水位上升。在水位上升的同時(shí)抬吟,控制點(diǎn)
X軸不斷變化萨咕,產(chǎn)生水波浪左右涌動(dòng)的感覺;還要將水位線下方的區(qū)域用
mPath.close()`閉合火本,這樣才會(huì)有種水不斷在杯子中增多的感覺
代碼:
public class BezierView extends View {
private Paint mPaint;
private Path mPath;
private Paint paint;
private int viewWidth, viewHeight; //控件的寬和高
private float commandX, commandY; //控制點(diǎn)的坐標(biāo)
private float waterHeight; //水位高度
private boolean isInc;// 判斷控制點(diǎn)是該右移還是左移
public BezierView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化畫筆 路徑
*/
private void init() {
//畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#AFDEE4"));
//路徑
mPath = new Path();
//輔助畫筆
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStrokeWidth(5f);
}
/**
* 獲取控件的寬和高
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWidth = w;
viewHeight = h;
// 控制點(diǎn) 開始時(shí)的Y坐標(biāo)
commandY = 7 / 8f * viewHeight;
//終點(diǎn)一開始的Y坐標(biāo) 危队,也就是水位水平高度 , 紅色輔助線
waterHeight = 15 / 16F * viewHeight;
}
/**
* 繪制
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 起始點(diǎn)位置
mPath.moveTo(-1 / 4F * viewWidth, waterHeight);
//繪制水波浪
mPath.quadTo(commandX, commandY, viewWidth + 1 / 4F * viewWidth, waterHeight);
//繪制波浪下方閉合區(qū)域
mPath.lineTo(viewWidth + 1 / 4F * viewWidth, viewHeight);
mPath.lineTo(-1 / 4F * viewWidth, viewHeight);
mPath.close();
//繪制路徑
canvas.drawPath(mPath, mPaint);
//繪制紅色水位高度輔助線
canvas.drawLine(0,waterHeight,viewWidth,waterHeight,paint);
//產(chǎn)生波浪左右涌動(dòng)的感覺
if (commandX >= viewWidth + 1 / 4F * viewWidth) {//控制點(diǎn)坐標(biāo)大于等于終點(diǎn)坐標(biāo)改標(biāo)識(shí)
isInc = false;
} else if (commandX <= -1 / 4F * viewWidth) {//控制點(diǎn)坐標(biāo)小于等于起點(diǎn)坐標(biāo)改標(biāo)識(shí)
isInc = true;
}
commandX = isInc ? commandX + 20 : commandX - 20;
//水位不斷加高 當(dāng)距離控件頂端還有1/8的高度時(shí)钙畔,不再上升
if (commandY >= 1 / 8f * viewHeight) {
commandY -= 2;
waterHeight -= 2;
}
//路徑重置
mPath.reset();
// 重繪
invalidate();
}
/**
* 測(cè)量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (wSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, hSpecSize);
} else if (hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wSpecSize, 300);
}
}
}
起始點(diǎn)坐標(biāo)為(-1 / 4F * viewWidth, waterHeight)
控制點(diǎn)(commandX, commandY)
終點(diǎn)(viewWidth + 1 / 4F * viewWidth, waterHeight)
起始點(diǎn)和終點(diǎn)的X
軸超出了BezierView
控件的大小交掏,是為了讓水波浪看起來更加自然
3. 紙飛機(jī)
<p>
將貝塞爾曲線和屬性動(dòng)畫結(jié)合使用,使飛機(jī)曲線飛行
3.1 De Casteljau 德卡斯特里奧算法
<p>
二階計(jì)算公式:
B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
-
t
曲線長(zhǎng)度比例 -
p0
起始點(diǎn) -
P1
控制點(diǎn) -
P2
終止點(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;
}
三階計(jì)算公式:
B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1]
-
t
曲線長(zhǎng)度比例 -
P0
起始點(diǎn) -
P1
控制點(diǎn)1 -
P2
控制點(diǎn)2 -
P3
終止點(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;
}
關(guān)于這個(gè)算法刃鳄,可以在看看德卡斯特里奧算法——找到Bezier曲線上的一個(gè)點(diǎn)
3.2 紙飛機(jī)代碼
<p>
使用屬性動(dòng)畫盅弛,需要用到估值器,估值器中需要計(jì)算飛機(jī)的飛行軌跡上的每一個(gè)點(diǎn)的坐標(biāo)叔锐,用到了De Casteljau
算法
估值器代碼:
public class BezierEvaluator implements TypeEvaluator<PointF> {
private PointF mPointF;
public BezierEvaluator(PointF mPointF) {
this.mPointF = mPointF;
}
@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
return calculateBezierPointForQuadratic(fraction, startValue, mPointF, endValue);
}
/**
* B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
*
* @param t 曲線長(zhǎng)度比例
* @param p0 起始點(diǎn)
* @param p1 控制點(diǎn)
* @param p2 終止點(diǎn)
* @return t對(duì)應(yīng)的點(diǎn)
*/
private 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;
}
}
自定義View
代碼:
public class PaperFlyView extends View implements View.OnClickListener {
private Bitmap flyBitmap;
private float flyX, flyY;
private float commandPointX, commandPointY; //控制點(diǎn)坐標(biāo)
private float startPointX, startPointY; //動(dòng)畫起始位置
private float endPointX, endPointY;//動(dòng)畫結(jié)束位置
public PaperFlyView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.paperfly);
Matrix m = new Matrix();
m.setScale(0.125f, 0.125f);
flyBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, false);
bitmap.recycle();
//控制點(diǎn) 坐標(biāo)
commandPointX = 1080;
commandPointY = 1080;
//設(shè)置點(diǎn)擊監(jiān)聽
setOnClickListener(this);
}
/**
* 拿到控件的寬和高后 根據(jù)寬高設(shè)置繪制位置挪鹏,動(dòng)畫開始,結(jié)束位置
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
flyX = 2 * flyBitmap.getWidth();
flyY = h - 3 * flyBitmap.getHeight();
//動(dòng)畫開始位置
startPointX = flyX;
startPointY = flyY;
//動(dòng)畫結(jié)束位置
endPointX = w / 2 - flyBitmap.getWidth();
endPointY = 3 * flyBitmap.getHeight();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(flyBitmap, flyX, flyY, null);
}
/**
* 點(diǎn)擊事件
*/
@Override
public void onClick(View v) {
//估值器
BezierEvaluator bezierEvaluator = new BezierEvaluator(new PointF(commandPointX, commandPointY));
//設(shè)置屬性動(dòng)畫
PointF startPointF = new PointF(startPointX, startPointY);
PointF endPointF = new PointF(endPointX, endPointY);
ValueAnimator anim = ValueAnimator.ofObject(bezierEvaluator, startPointF, endPointF);
anim.setDuration(1000);
//在動(dòng)畫過程中愉烙,更新繪制的位置 位置的軌跡就是貝塞爾曲線
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
PointF point = (PointF) valueAnimator.getAnimatedValue();
flyX = point.x;
flyY = point.y;
invalidate();
}
});
anim.setInterpolator(new AccelerateDecelerateInterpolator());//加速減速插值器
anim.start();
}
/**
* 測(cè)量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (wSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, hSpecSize);
} else if (hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wSpecSize, 300);
}
}
}
代碼中控制點(diǎn)讨盒,屬性動(dòng)畫開始和結(jié)束的點(diǎn),都是隨意設(shè)置的
補(bǔ)充 3.3
在3.2
中步责,只有動(dòng)畫開啟返顺,卻并沒有處理動(dòng)畫關(guān)閉。如果動(dòng)畫的時(shí)間比較久蔓肯,當(dāng)動(dòng)畫運(yùn)行了一半遂鹊,View
所在的Actiivty
被關(guān)掉,還是需要考慮將動(dòng)畫關(guān)閉的蔗包,不及時(shí)處理秉扑,可能會(huì)造成內(nèi)存泄露
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (null != anim && anim.isRunning()){
anim.cancel();
}
}
當(dāng)View
所在的Activity
關(guān)閉或者View
被remove
掉,會(huì)調(diào)用onDetachedFromWindow()
方法调限。對(duì)應(yīng)的便是onAttachectedToWindow()
方法舟陆,當(dāng)View
所在的Activity
啟動(dòng)時(shí)误澳,會(huì)調(diào)用
4.最后
<p>
學(xué)習(xí)過程基本就是嚴(yán)重借鑒愛哥和徐醫(yī)生兩個(gè)大神博客中的案例,修改
使用貝塞爾曲線秦躯,個(gè)人感覺基本思想就是確定約束點(diǎn):起點(diǎn)忆谓,控制點(diǎn),終點(diǎn)踱承。中間的計(jì)算過程盡量交給Path
本人很菜陪毡,有錯(cuò)誤,請(qǐng)指出
共勉 : )