學習資料:
- 愛哥自定義控件其實很簡單5/12
- 徐醫生貝塞爾曲線開發的藝術
十分感謝兩位大神 :)
1. Bezier
<p>
Bezier
是一個法國的數學家的名字。在Path
中,lineTo()
方法是用來繪制直線的,quadTo()
和cubicTo()
來繪制曲線
Bezier
原理就是利用多個點的位置來確定出一條曲線。這多個點就是起點,終點,控制點。控制點可以沒有,也可以有多個。個人感覺,除了這些必要的點外,還可以虛擬出一個運動點
曲線可以看作是一個運動點的軌跡。在高中時,數學大題中,往往會有一道讓求一個點的運動軌跡,一般結果是一個橢圓或者圓的數學公式。Bezier
的繪制曲線,感覺也就是這個運動點的運動軌跡
1.1 一階貝塞爾曲線
<p>
沒有控制點,一階貝塞爾曲線
這時,只有起點P0
和終點P1
,運動點在P0,P1
間的運動軌跡就是一條線段
公式:
B(t)
就是運動點在t
時刻的坐標,p0
起點,p1
終點
對應的就是lineTo()
方法
圖和公式來自愛哥的博客
1.2 二階貝塞爾曲線
<p>
一個控制點,二階貝塞爾曲線
起點P0
和終點P2
,控制點就是P1
,運動點在P0,P1,P2
三個點的約束下,運動形成的軌跡就是紅色的曲線
公式:
二階對應的方法就是quadTo()
1.3 三階貝塞爾曲線
<p>
兩個個控制點,三階貝塞爾曲線
紅色就是運動點的軌跡,也就是最終會繪制的曲線
公式:
三階對應的方法就是cubicTo()
幸虧Path
類對計算過程做了封裝 : )
2. 模擬向杯子中倒水
<p>
主要的思路,就是起點,終點,控制點的Y
坐標不斷減小,屏幕頂部的``Y軸坐標為
0,向屏幕上方偏移,也就是水位上升。在水位上升的同時,控制點
X軸不斷變化,產生水波浪左右涌動的感覺;還要將水位線下方的區域用
mPath.close()`閉合,這樣才會有種水不斷在杯子中增多的感覺
代碼:
public class BezierView extends View {
private Paint mPaint;
private Path mPath;
private Paint paint;
private int viewWidth, viewHeight; //控件的寬和高
private float commandX, commandY; //控制點的坐標
private float waterHeight; //水位高度
private boolean isInc;// 判斷控制點是該右移還是左移
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;
// 控制點 開始時的Y坐標
commandY = 7 / 8f * viewHeight;
//終點一開始的Y坐標 ,也就是水位水平高度 , 紅色輔助線
waterHeight = 15 / 16F * viewHeight;
}
/**
* 繪制
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 起始點位置
mPath.moveTo(-1 / 4F * viewWidth, waterHeight);
//繪制水波浪
mPath.quadTo(commandX, commandY, viewWidth + 1 / 4F * viewWidth, waterHeight);
//繪制波浪下方閉合區域
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);
//產生波浪左右涌動的感覺
if (commandX >= viewWidth + 1 / 4F * viewWidth) {//控制點坐標大于等于終點坐標改標識
isInc = false;
} else if (commandX <= -1 / 4F * viewWidth) {//控制點坐標小于等于起點坐標改標識
isInc = true;
}
commandX = isInc ? commandX + 20 : commandX - 20;
//水位不斷加高 當距離控件頂端還有1/8的高度時,不再上升
if (commandY >= 1 / 8f * viewHeight) {
commandY -= 2;
waterHeight -= 2;
}
//路徑重置
mPath.reset();
// 重繪
invalidate();
}
/**
* 測量
*/
@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);
}
}
}
起始點坐標為(-1 / 4F * viewWidth, waterHeight)
控制點(commandX, commandY)
終點(viewWidth + 1 / 4F * viewWidth, waterHeight)
起始點和終點的X
軸超出了BezierView
控件的大小,是為了讓水波浪看起來更加自然
3. 紙飛機
<p>
將貝塞爾曲線和屬性動畫結合使用,使飛機曲線飛行
3.1 De Casteljau 德卡斯特里奧算法
<p>
二階計算公式:
B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
-
t
曲線長度比例 -
p0
起始點 -
P1
控制點 -
P2
終止點
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]
-
t
曲線長度比例 -
P0
起始點 -
P1
控制點1 -
P2
控制點2 -
P3
終止點
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;
}
關于這個算法,可以在看看德卡斯特里奧算法——找到Bezier曲線上的一個點
3.2 紙飛機代碼
<p>
使用屬性動畫,需要用到估值器,估值器中需要計算飛機的飛行軌跡上的每一個點的坐標,用到了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 曲線長度比例
* @param p0 起始點
* @param p1 控制點
* @param p2 終止點
* @return t對應的點
*/
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; //控制點坐標
private float startPointX, startPointY; //動畫起始位置
private float endPointX, endPointY;//動畫結束位置
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();
//控制點 坐標
commandPointX = 1080;
commandPointY = 1080;
//設置點擊監聽
setOnClickListener(this);
}
/**
* 拿到控件的寬和高后 根據寬高設置繪制位置,動畫開始,結束位置
*/
@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();
//動畫開始位置
startPointX = flyX;
startPointY = flyY;
//動畫結束位置
endPointX = w / 2 - flyBitmap.getWidth();
endPointY = 3 * flyBitmap.getHeight();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(flyBitmap, flyX, flyY, null);
}
/**
* 點擊事件
*/
@Override
public void onClick(View v) {
//估值器
BezierEvaluator bezierEvaluator = new BezierEvaluator(new PointF(commandPointX, commandPointY));
//設置屬性動畫
PointF startPointF = new PointF(startPointX, startPointY);
PointF endPointF = new PointF(endPointX, endPointY);
ValueAnimator anim = ValueAnimator.ofObject(bezierEvaluator, startPointF, endPointF);
anim.setDuration(1000);
//在動畫過程中,更新繪制的位置 位置的軌跡就是貝塞爾曲線
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();
}
/**
* 測量
*/
@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);
}
}
}
代碼中控制點,屬性動畫開始和結束的點,都是隨意設置的
補充 3.3
在3.2
中,只有動畫開啟,卻并沒有處理動畫關閉。如果動畫的時間比較久,當動畫運行了一半,View
所在的Actiivty
被關掉,還是需要考慮將動畫關閉的,不及時處理,可能會造成內存泄露
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (null != anim && anim.isRunning()){
anim.cancel();
}
}
當View
所在的Activity
關閉或者View
被remove
掉,會調用onDetachedFromWindow()
方法。對應的便是onAttachectedToWindow()
方法,當View
所在的Activity
啟動時,會調用
4.最后
<p>
學習過程基本就是嚴重借鑒愛哥和徐醫生兩個大神博客中的案例,修改
使用貝塞爾曲線,個人感覺基本思想就是確定約束點:起點,控制點,終點。中間的計算過程盡量交給Path
本人很菜,有錯誤,請指出
共勉 : )