編輯:關於Android編程
前段時間公司項目中有用到Bezier曲線的知識,在熟悉Bezier曲線原理和實現方式後,我突發奇想在Android客戶端實現Bezier曲線的構建動畫,於是有了BezierMaker這個項目。在講解代碼前,我們先了解一下Bezier曲線的原理。
項目源碼:https://github.com/venshine/BezierMaker
貝塞爾曲線於1962年,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來為汽車的主體進行設計。貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau算法開發,以穩定數值的方法求出Bezier曲線。
Bezier曲線是用一系列點來控制曲線狀態的,我們將這些點簡單分為兩類:
數據點:確定曲線的起始和結束位置 控制點:確定曲線的彎曲程度給定數據點P0、P1,線性Bezier曲線只是一條兩點之間的直線。
二階Bezier曲線的路徑由數據點 P 0、P 2和控制點P 1的函數B(t)追蹤。
為構建二階Bezier曲線,可以中介點Q<喎?/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPiAwus08c3Ryb25nPlE8L3N0cm9uZz4gMdf3zqrTyTDWwTG1xDxlbT50PC9lbT6jujxiciAvPg0K08k8c3Ryb25nPlA8L3N0cm9uZz4gMNbBPHN0cm9uZz5QPC9zdHJvbmc+IDG1xMGs0Pi14zxzdHJvbmc+UTwvc3Ryb25nPiAwo6zD6Mr20rvM9c/f0NRCZXppZXLH+s/foaM8YnIgLz4NCtPJPHN0cm9uZz5QPC9zdHJvbmc+IDHWwTxzdHJvbmc+UDwvc3Ryb25nPiAytcTBrND4teM8c3Ryb25nPlE8L3N0cm9uZz4gMaOsw+jK9tK7zPXP39DUQmV6aWVyx/rP36GjPGJyIC8+DQrTyTxzdHJvbmc+UTwvc3Ryb25nPiAw1sE8c3Ryb25nPlE8L3N0cm9uZz4gMbXEwazQ+LXjPHN0cm9uZz5CPC9zdHJvbmc+o6g8ZW0+dDwvZW0+o6mjrMPoyvbSu8z1tv6910Jlemllcsf6z9+hozwvcD4NCjxoNCBpZD0="三階bezier曲線">三階Bezier曲線
P 0、P 1、P 2、P 3四個點在平面或在三維空間中定義了三階Bezier曲線。曲線起始於P 0走向P 1,並從P 2的方向來到P 3。一般不會經過P 1或P 2;這兩個點只是在那裡提供方向。P 0和P 1之間的間距,決定了曲線在轉而趨進P 2之前,走向P 1方向的“長度有多長”。 N 階貝塞爾曲線可如下推斷。給定點P 0、P 1、…、P n,其Bezier曲線即 上面的公式可用如下遞歸表達,即N 階貝塞爾曲線是雙N-1 階貝塞爾曲線之間的插值。 注解: Bezier曲線的作用十分廣泛,在Android移動應用開發中有很多應用的場景。例如QQ小紅點拖拽效果、閱讀軟件的翻書效果、平滑折線圖的制作、很多炫酷的動畫效果等等。 看到這裡,大部分人應該都了解了Bezier曲線的原理。上面說到了Bezier曲線能夠實現很多炫酷的效果,是不是有點躍躍欲試了? 下面講解一下BezierView這個類的主要代碼,這個自定義類包含了實現Bezier曲線動畫的主要代碼。首先看一下init()初始化方法,代碼如下所示: init()方法主要作用是初始化一些必要的信息,包括繪制Bezier曲線的畫筆、數據點和控制點的坐標,繪制路徑以及狀態變量。可以看到,我們在程序中初始化了兩個數據點和一個控制點,代碼中我們把數據點也作為控制點處理。由上面講解的Bezier曲線的原理可以知道,一階(線性)Bezier曲線包含兩個控制點,二階Bezier曲線包含三個控制點,三階Bezier曲線包含四個控制點,可以依次類推N階Bezier曲線包含N+1個控制點。 這段代碼主要用於創建Bezier點集。熟悉Android Path用法的同學應該知道Path類中有兩個方法quadTo和cubicTo,通過這兩個方法可以畫出二階和三階Bezier曲線。因為我們這裡需要創建更高階的Bezier曲線,因此我采用德卡斯特裡奧算法(De Casteljau’s Algorithm)來實現Bezier曲線。 下面看一下德卡斯特裡奧算法的實現程序,通過遞歸實現N階Bezier曲線上的點。上面短短幾行代碼就能實現Bezier,是不是很簡單。在這裡我就不解釋這段代碼了,不懂的同學可以參考上一段解釋,看我翻譯的德卡斯特裡奧算法。 這段代碼不長,但是乍一看有三重for循環,很多人可能頭大了。其實聽我慢慢分析,你會感覺很簡單。這段代碼就是用來實現創建Bezier曲線過程中的折線點集。通過德卡斯特裡奧算法我們知道,二階Bezier曲線有三個控制點和一條折線,在t時刻通過三個點和一條折線確定曲線上唯一的點。接下來三階Bezier曲線在t時刻通過四個點和二條折線確定曲線上唯一的點。以此類推,N階Bezier曲線在t時刻通過N+1個點和N-1條折線確定曲線上唯一的點。理解上面的話,我們就能通過程序來實現折線點集。 下面我們來繪制Bezier曲線。這段代碼很長,但是很簡單。首先判斷狀態,只有運行且非觸摸狀態時,才可以繪制Bezier曲線。觸摸狀態時僅繪制控制點和控制點之間的連線。運行狀態,如果當前Bezier曲線移動起始點為空,則重置Path,重新設置Path的起始點。接下來for循環繪制控制點和控制點連線以及文本。接下來我們解析瞬時折線點,通過雙重for循環取出mInstantTangentPoints中的點,兩兩連接繪制折線。接下來通過Path連接Bezier曲線上某一時刻的點,繪制該點的路徑。其實onDraw裡的代碼只繪制了某一時刻的點,我們在最後通過handler方式改變t時刻,並重復調用onDraw繪制,一直到曲線繪制周期結束。這樣,一條完美的Bezier曲線就繪制好了。 再來看一下handler模塊。這段代碼通過一定速率控制並獲取曲線上的點以及折線上的點,保證曲線最後一幀繪制完成後結束調用。我們再來欣賞一下Bezier曲線的繪制效果,如下圖所示: 以上就是繪制Bezier曲線代碼的核心部分,有疑問的朋友可以在下面留言。高階Bezier曲線
Bezier曲線的作用
BezierView實戰講解
先欣賞一下最終的實現效果:
private void init() {
// 初始坐標
mControlPoints = new ArrayList<>(MAX_COUNT + 1);
int w = getResources().getDisplayMetrics().widthPixels;
mControlPoints.add(new PointF(w / 5, w / 5));
mControlPoints.add(new PointF(w / 3, w / 2));
mControlPoints.add(new PointF(w / 3 * 2, w / 4));
// 貝塞爾曲線畫筆
mBezierPaint = new Paint();
mBezierPaint.setColor(Color.RED);
mBezierPaint.setStrokeWidth(BEZIER_WIDTH);
mBezierPaint.setStyle(Paint.Style.STROKE);
mBezierPaint.setAntiAlias(true);
// 移動點畫筆
mMovingPaint = new Paint();
mMovingPaint.setColor(Color.BLACK);
mMovingPaint.setAntiAlias(true);
mMovingPaint.setStyle(Paint.Style.FILL);
// 控制點畫筆
mControlPaint = new Paint();
mControlPaint.setColor(Color.BLACK);
mControlPaint.setAntiAlias(true);
mControlPaint.setStyle(Paint.Style.STROKE);
// 切線畫筆
mTangentPaint = new Paint();
mTangentPaint.setColor(Color.parseColor(TANGENT_COLORS[0]));
mTangentPaint.setAntiAlias(true);
mTangentPaint.setStrokeWidth(TANGENT_WIDTH);
mTangentPaint.setStyle(Paint.Style.FILL);
// 固定線畫筆
mLinePaint = new Paint();
mLinePaint.setColor(Color.LTGRAY);
mLinePaint.setStrokeWidth(CONTROL_WIDTH);
mLinePaint.setAntiAlias(true);
mLinePaint.setStyle(Paint.Style.FILL);
// 點畫筆
mTextPointPaint = new Paint();
mTextPointPaint.setColor(Color.BLACK);
mTextPointPaint.setAntiAlias(true);
mTextPointPaint.setTextSize(TEXT_SIZE);
// 文字畫筆
mTextPaint = new Paint();
mTextPaint.setColor(Color.GRAY);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(TEXT_SIZE);
mBezierPath = new Path();
mState |= STATE_READY | STATE_TOUCH;
}
private ArrayList
不熟悉德卡斯特裡奧算法的同學可以參考我翻譯的一篇文章《德卡斯特裡奧算法——找到Bezier曲線上的一個點》。
在上面的方法中,我通過deCasteljauX和deCasteljauY方法創建某一時刻Bezier曲線上的點,再通過把參數t等分1000份,每個時刻的點連接在一起就能形成一條完整的Bezier曲線。有的同學看到這裡可能會問,為什麼要等分1000份啊?其實t等分多少取決於曲線的平滑程度。1000只是一個相對值,t等分越多,曲線越平滑,反之亦然。
private float deCasteljauX(int i, int j, float t) {
if (i == 1) {
return (1 - t) * mControlPoints.get(j).x + t * mControlPoints.get(j + 1).x;
}
return (1 - t) * deCasteljauX(i - 1, j, t) + t * deCasteljauX(i - 1, j + 1, t);
}
private ArrayList>> buildTangentPoints() {
ArrayList
ArrayList
定義一段折線的點集,ArrayList> morepoints;
定義一條折線的點集,ArrayList>> allpoints = new ArrayList<>();定義多條折線的點集。那麼如何確定折線段上的點呢?我們可以通過下面的公式來獲取:
B(t) = (1-t) * P0 + t * P1;
,其中P0為一條折線段的起點坐標,P1為終點坐標。這個公式的證明參考德卡斯特裡奧算法。看最裡層for循環,獲取t時刻每一段折線的點集。其中,如果還沒有一條折線產生,則取控制點坐標實現最外層折線,當最外層折線形成後,裡層的折線坐標依次取外層t時刻的折線上的點。中間層for循環實現每一條折線的點集,最外層for循環控制折線的層樹。例如三階Bezier曲線有兩層折線,外層折線包含兩條線段,裡層折線包含一條線段。
@Override
protected void onDraw(Canvas canvas) {
if (isRunning() && !isTouchable()) {
if (mBezierPoint == null) {
mBezierPath.reset();
mBezierPoint = mBezierPoints.get(0);
mBezierPath.moveTo(mBezierPoint.x, mBezierPoint.y);
}
// 控制點和控制點連線
int size = mControlPoints.size();
PointF point;
for (int i = 0; i < size; i++) {
point = mControlPoints.get(i);
if (i > 0) {
// 控制點連線
canvas.drawLine(mControlPoints.get(i - 1).x, mControlPoints.get(i - 1).y, point.x, point.y,
mLinePaint);
}
// 控制點
canvas.drawCircle(point.x, point.y, CONTROL_RADIUS, mControlPaint);
// 控制點文本
canvas.drawText("p" + i, point.x + CONTROL_RADIUS * 2, point.y + CONTROL_RADIUS * 2, mTextPointPaint);
// 控制點文本展示
canvas.drawText("p" + i + " ( " + new DecimalFormat("##0.0").format(point.x) + " , " + new DecimalFormat
("##0.0").format(point.y) + ") ", REGION_WIDTH, mHeight - (size - i) * TEXT_HEIGHT, mTextPaint);
}
// 切線
if (mTangent && mInstantTangentPoints != null && !isStop()) {
int tsize = mInstantTangentPoints.size();
ArrayList
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == HANDLER_WHAT) {
mR += mRate;
if (mR >= mBezierPoints.size()) {
removeMessages(HANDLER_WHAT);
mR = 0;
mState &= ~STATE_RUNNING;
mState &= ~STATE_STOP;
mState |= STATE_READY | STATE_TOUCH;
if (mLoop) {
start();
}
return;
}
if (mR != mBezierPoints.size() - 1 && mR + mRate >= mBezierPoints.size()) {
mR = mBezierPoints.size() - 1;
}
// Bezier點
mBezierPoint = new PointF(mBezierPoints.get(mR).x, mBezierPoints.get(mR).y);
// 切線點
if (mTangent) {
int size = mTangentPoints.size();
ArrayList
構建本地單元測試如果你的單元測試沒有依賴或者僅僅有簡單的Android依賴,你應當在本地開發器上運行你的測試。這種測試方法很高效,因為它讓你避免每次運行測試時加載目標Ap
1.ContentProvider簡介 在Android中有些數據(如通訊錄、音頻、視頻文件等)是要供很多應用程序使用的,為了更好地對外提供
前言 心好疼:昨晚寫完了這篇博客一半,今天編輯的時候網絡突然斷了,我的文章就這樣沒了,但是為了Developer的使用AS這款IDE可以快速上手,我還是繼續進行詳解
注:急速開發的人,可以直接看第三種實現方式1:修改theme,重啟activity(Google自家在內的很多應用都是采用此種方式實現夜間模式的)優點:正兒八經的夜間模式