自帶美感的貝塞爾曲線原理與實戰——Android高級UI

目錄

一、前言

二、貝塞爾曲線的繪製規則

三、在canvas中如何繪製貝塞爾曲線

四、實戰

五、寫在最後

一、前言

貝塞爾曲線,想必大家或多或少都聽過這個詞,因爲其控制簡單,且其曲線更符合我們大衆的審美,所以在很多領域都有涉及,當然這些都不是我們今天要進行討論和分享的重點。今天要分享的是如何成爲自定義UI中的一把利器,先上兩張圖看看效果,然後開始我們的分享。

圓變心效果圖

乘風破浪的小船

文末會給出源碼,勿急勿急,弄懂原理,才能面對一切需求

二、貝塞爾曲線的繪製規則

想要講清楚多階貝塞爾曲線,我們先要從一階開始講起。我先來看下一階貝塞爾曲線的動態圖。

動畫demo的源碼,請移步github傳送門

1、一階貝塞爾曲線

一階貝塞爾曲線解析: 兩點控制一條直線,只是剛好一階的控制點是靜止的,所以當比例增加時(圖中左下角的u值即爲比例,比例的值 u = 左上的點到移動的小紅點的距離 / 整條線的長度),貝塞爾曲線上的點(即小紅點)一直都是在一條靜態的線上挪動,致使一階貝塞爾曲線的結果就是一條直線,只是和兩個控制點連接的直線重合了。

看到這你可能還有些雲裏霧裏,切勿心急,順着往下看,你會恍然大悟。

一階貝塞爾曲線

2、二階貝塞爾曲線

看完一階的,可能各位童鞋還沒感受到其魅力。我們進行升階,升至二階。二階貝塞爾曲線動態效果如下圖:

二階貝塞爾曲線動態圖

二階貝塞爾曲線解析: 我們需要藉助以下的一張靜態圖來講解
二階貝塞爾曲線
第一步: 先看二階的控制點和基線(藍色的點和線),然後按照比例值u,從 ABBC 上取 DE。具體爲

AD/AB = BE/BC = u(比例值)

第二步: 從第一步得出了 DE 兩個點,這兩個點便是 一階的控制點 (黃色點),將DE連起便是 一階的基線 (黃色線),然後按照和 第一步一樣比例值u,從 DE 上取 F。具體爲

DF/DE = u(比例值)

第三步: 從第二步得出的點 F 就是 最終貝塞爾曲線(紅色線)上當比例值爲u時的點。 當比例值u 從零到一變化時,D和E 在 AB和BC 上進行移動,從而讓 DE 直線會被“推動”起來。而 DE 直線上的點 F 也因u的變化而“移動起來”,這一連帶的推動,最終產生了 點F “走”過的軌跡(紅色線)便是最終的貝塞爾曲線。

值得一說:
貝塞爾曲線的繪製,無論多少階(一階除外),均需要進行降階,降至一階。在 “二階貝塞爾曲線解析” 這段文字中,從 第一步 到 第二步 的過程就是在降階。

從上面的三步中,我們可以得出如下三條結論:

  • 一階的基線從二階得來,二階的基線從三階得來(待會會繼續講三階,稍安勿躁),推而廣之,n階的基線便從(n+1)階得來
  • 除了最高階的控制點是固定的,降階過程中的控制點全都是按 比例值u 進行取點。所以在二階的例子中,我們可以得出以下這樣一個等式
AD/AB = BE/BC = DF/DE = u(比例值)
  • 貝塞爾曲線最終的路徑是由 一階基線 上的遊走的紅色小點形成的;

3、三階貝塞爾曲線

理解完二階,童鞋們大多能根據上面的 三條結論 得出如何繪製三階貝塞爾曲線,帶着你心中的猜想,我們繼續解析三階貝塞爾曲線,先來看下三階貝塞爾曲線的動態效果圖:

三階貝塞爾曲線動態圖

三階貝塞爾曲線解析: 按照二階的慣例,爲了方便理解,我們還是使用一張靜態圖,以下便是三階貝塞爾曲線的靜態圖(稍微凌亂了些,可以根據顏色進行區分)

三階貝塞爾曲線

第一步: 三階的基線ABBCCD(藍色線) ,然後按照 比例值u 分別取 EFG

第二步: 從第一步便得出 二階的控制點 EFG(黃色點),連線而得 EFFG 兩條二階基線,同樣按照 比例值u 分別取 HI

第三步: 從第二步得出了 一階的控制點 HI (綠色點),連線而得 HI 一階基線,按照 比例值u 取得 J,就是最終的貝塞爾曲線,當比例值u爲0.55時,所在的點。

值得一提

從三階我們可以知道所有點的比例值都是一樣的,具體如下

AE/AB = BF/BC = CG/CD = EH/EF = FI/FG = HJ/HI = 比例值u

4、七階貝塞爾曲線

前面看了一二三階的貝塞爾曲線,想必童鞋們已經知道這繪製的規律。接下來我們看看 七階貝塞爾曲線 的動態圖,其規則和三階是一樣的,都是從七階降至六階再到五階等等,這裏就不再贅述。

7階貝塞爾曲線

動畫demo的源碼,請移步github傳送門

三、在canvas中如何繪製貝塞爾曲線

1、二階貝塞爾曲線

二階貝塞爾曲線 在 Path 類中有提供現成的 API

public void quadTo(float x1, float y1, float x2, float y2)

如何使用?

我們藉助上面的二階貝塞爾曲線的靜態圖,進行講解。
二階貝塞爾曲線
假設我們要繪製圖中的這條紅色的二階貝塞爾曲線,只需進行如下代碼操作

// 初始化 路徑對象
Path path = new Path();
// 移動至第一個控制點 A(ax,ay)
path.moveTo(ax, ay);
// 填充二階貝塞爾曲線的另外兩個控制點 B(bx,by) 和 C(cx,cy),切記順序不能變
path.quadTo(bx, by, cx, cy);
// 將 貝塞爾曲線 繪製至畫布
canvas.drawPath(path, paint);

這段代碼繪製的效果和圖中是一樣的,我就不在貼圖了。

2、三階貝塞爾曲線

很幸運的是,三階貝塞爾曲線 在 Path 類中也有提供現成的 API

public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

我們藉助上面 三階貝塞爾曲線靜態圖 進行講解。
三階貝塞爾曲線
假設我們要繪製圖中的這條紅色的三階貝塞爾曲線,只需進行如下代碼操作

// 初始化 路徑對象
Path path = new Path();
// 移動至第一個控制點 A(ax,ay)
path.moveTo(ax, ay);
// 填充三階貝塞爾曲線的另外三個控制點:
// B(bx,by) C(cx,cy) D(dx,dy) 切記順序不能變
path.cubicTo(bx, by, cx, cy, dx, dy);
// 將 貝塞爾曲線 繪製至畫布
canvas.drawPath(path, paint);

3、多階貝塞爾曲線

看完二階和三階貝塞爾曲線的使用,是不是覺得非常的簡單。但是系統提供的API就止步三階貝塞爾曲線了,這是因爲高階在實際的開發過程中不是很常用,如果真的需要使用再高階的貝塞爾曲線,那就只能自己進行降階了。

我們藉助以下的二階貝塞爾曲線圖來推導我們的降階公式。

先確定幾個座標 A(ax, ay)、B(bx, by)、C(cx, cy)、D(dx, dy)、E(ex, ey)、F(fx, fy)

當然一開始我們只知道 A、B、C 三個點的座標,所以 D 的座標由 A、B 進行求出具體如下

D點的x軸座標:dx = ax + (bx-ax) * u  = (1-u) * ax + u * bx   (u ∈ [0,1])
D點的y軸座標:dy = ay + (by-ay) * u  = (1-u) * ay + u * by   (u ∈ [0,1])

同理,E 的座標由 B、C 進行求出,計算的邏輯完全一樣。具體如下

E點的x軸座標:ex = bx + (cx-bx) * u  = (1-u) * bx + u * cx   (u ∈ [0,1])
E點的y軸座標:ey = by + (cy-by) * u  = (1-u) * by + u * cy   (u ∈ [0,1])

當得出 D和E 點,就可以進行求 點F,邏輯還是一樣。具體如下

F點的x軸座標:fx = dx + (ex-dx) * u  = (1-u) * dx + u * ex   (u ∈ [0,1])
F點的y軸座標:fy = dy + (ey-dy) * u  = (1-u) * dy + u * ey   (u ∈ [0,1])

至此最終的點 F 的可繪製座標便得出。

推導公式

從以上的 計算公式 和 之前的 “三個結論”,藉助下圖我們可以得出一個公式

P0k = (1-u) * P0k-1 + u * P1k-1

Tips: x軸 和 y軸 的座標計算公式是一樣,所以我們這裏就使用 x軸 作爲代表,方便講解

通用公式,想必童鞋們已經想到算法中的一個詞叫 “遞歸”,的確沒錯,但細想一下還缺少一個 遞歸的終止條件 。我們再細想一下,其實終止條件就是 降階最開始依賴的控制點是固定不變的,或是說是我們程序猿給定的,所以不用計算直接返回該控制點的x軸或y軸即可。

最終的遞歸公式如下

Pik={Pik=0(1u)Pik1+uPi+1k1k=1,2,...ni=0,1,...,nkP_i^k = \begin{cases} P_i & k = 0 \\ (1-u)P_i^{k-1} + u P_{i+1}^{k-1}& k = 1,2,...n;i=0,1,...,n-k \end{cases}

公式說明:

1、k 表示階數,當 k=n 時,即相當於前面demo所講的一階控制點;當 k=0 時,表示最高階的控制點,即我們程序猿最初給定的那幾個控制點;

2、 i 表示點的下標,這個只是爲了便於區分,可參照上面的圖進行帶入理解;

3、u 表示比例值

將通用公式編寫成如下代碼,調用 buildBezierPoint 方法,即可獲得對應的最終的貝塞爾曲線,二階和三階也同樣適用。

/**
 * 構建貝塞爾曲線,具體點數由 參數frame 決定
 *
 * @param controlPointList 控制點的座標
 * @param frame            幀數
 * @return
 */
public static List<PointF> buildBezierPoint(List<PointF> controlPointList,
                                            int frame) {
    List<PointF> pointList = new ArrayList<>();

    // 此處注意,要用1f,否則得出結果爲0
    float delta = 1f / frame;

    // 階數,階數=繪製點數-1
    int order = controlPointList.size() - 1;

    // 循環遞增
    for (float u = 0; u <= 1; u += delta) {
        pointList.add(new PointF(BezierUtils.calculatePointCoordinate(BezierUtils.X_TYPE, u, order, 0, controlPointList),
                BezierUtils.calculatePointCoordinate(BezierUtils.Y_TYPE, u, order, 0, controlPointList)));
    }

    return pointList;

}

/**
 * 計算座標 [貝塞爾曲線的核心關鍵]
 *
 * @param type             {@link #X_TYPE} 表示x軸的座標, {@link #Y_TYPE} 表示y軸的座標
 * @param u                當前的比例
 * @param k                階數
 * @param p                當前座標(具體爲 x軸 或 y軸)
 * @param controlPointList 控制點的座標
 * @return
 */
public static float calculatePointCoordinate(@IntRange(from = X_TYPE, to = Y_TYPE) int type,
                                             float u,
                                             int k,
                                             int p,
                                             List<PointF> controlPointList) {

    /**
     * 公式解說:(p表示座標點,後面的數字只是區分)
     * 場景:有一條線p1到p2,p0在中間,求p0的座標
     *      p1◉--------○----------------◉p2
     *            u    p0
     *
     * 公式:p0 = p1+u*(p2-p1) 整理得出 p0 = (1-u)*p1+u*p2
     */

    // 一階貝塞爾,直接返回
    if (k == 1) {

        float p1;
        float p2;

        // 根據是 x軸 還是 y軸 進行賦值
        if (type == X_TYPE) {
            p1 = controlPointList.get(p).x;
            p2 = controlPointList.get(p + 1).x;
        } else {
            p1 = controlPointList.get(p).y;
            p2 = controlPointList.get(p + 1).y;
        }

        return (1 - u) * p1 + u * p2;

    } else {

        /**
         * 這裏應用了遞歸的思想:
         * 1階貝塞爾曲線的端點 依賴於 2階貝塞爾曲線
         * 2階貝塞爾曲線的端點 依賴於 3階貝塞爾曲線
         * ....
         * n-1階貝塞爾曲線的端點 依賴於 n階貝塞爾曲線
         *
         * 1階貝塞爾曲線 則爲 真正的貝塞爾曲線存在的點
         */
        return (1 - u) * calculatePointCoordinate(type, u, k - 1, p, controlPointList)
                + u * calculatePointCoordinate(type, u, k - 1, p + 1, controlPointList);

    }

}

四、實戰

經過漫長的理論,童鞋們早就摩拳搽掌,想用貝塞爾曲線前去挑戰設計師,少俠勿急,看完實戰我們再去碾壓?。

溫馨提示:

理論是進階中必不可少的部分,否則只知其然而不知其所以然。永遠只能是作爲使用別人代碼的使用者,而不是創造者,更無法體會到創造的快樂。

1、圓變心

文章最開始出現的就是以下這張效果圖,現在是時候進行擼起袖子開始打代碼了。

效果圖

動畫分析:

動態圖中,我們可以清楚的看出,從一個圓形慢慢變成心形,然後帶有一點彈性效果。這樣一分析,我們便需要三樣東西:圓、心、彈性效果公式,接下來就是逐個突破。

準備零件

(1)圓
此圓非彼圓,我們不能借助canvas直接使用drawCircle進行繪製,因爲這樣的圓我們無法控制。那要如何處理呢?當然是用貝塞爾曲線畫圓,因爲這樣一來這個 “圓”的控制點 就全都在我們的可控範圍內,因爲我持有了這些控制點就能進行座標的變動,進而改變曲線的形狀。

正當你在坐等這個 貝塞爾曲線畫圓的公式 時,我又要潑一盆冷水了,因爲根本就不存在這樣一個公式。但我們可以通過前面的理論找到一個 近似圓的貝塞爾曲線公式

至於 貝塞爾曲線 爲什麼無法畫出一個圓,有興趣的童鞋們自行百度和google吧,畢竟這個一兩行字無法解釋清楚。

我們可以通過 三階貝塞爾曲線 畫出一段圓弧,通過四段圓弧就能拼湊出一個完整的圓了。但是又來了一個問題,三階貝塞爾曲線有四個控制點,兩端的控制點容易取,中間的控制點如何取? 帶着這個疑問,我們來看下面這個動畫,當 控制點比例 從0到1增加過程中,藍色區域從方形慢慢的變得接近圓,然後溢出變成圓角方形紅色的圓圈是用canvas的drawCircle繪製,從一些貝塞爾曲線繪製圓的論證資料和這裏的動畫效果可以得出,當 控制點比例等於0.55時(保留兩位小數),最接近一個圓。 前面提到的 四段圓弧的貝塞爾曲線 ,在這裏使用了四種顏色,需要自己體驗效果的童鞋,請進傳送門

可能還有些童鞋對動態圖中的 控制點比例 不太理解,我們藉助下圖來解釋,圖中只留了一段圓弧,其他的三段是一樣的道理。具體比例公式如下

=ABED=CDAE控制點比例 =\frac{AB}{ED} = \frac{CD}{AE}

E爲圓心,AE和ED爲半徑,即AE=ED;所以AB=CD;

至此圓的問題解決了。我們繼續過關斬將。

(2)心
心要如何繪製呢?小盆友這裏給出一個小工具,我們可以通過自行拽動來獲取需要的圖形,然後打印座標(單位dp),拿到座標了就可以爲所欲爲了。工具效果圖如下,我們以拽動一個圓爲例:

拽動的動態效果圖

打印出來的座標點(單位爲dp):
座標肯定會有些許偏差,畢竟是手指拽動出來的,而且左右的心是不對稱的,所以需要進行微調一半心,然後另一半心進行對稱取座標。如此一來,心形也搞定了。

這個小工具可用於從圓變成另一種形狀,而不侷限於心形,或許說侷限於我們的想象力。下圖是小盆友隨便拽出來的一個圖,個人覺得有點像兔子?,哈哈,挺抽象的吧。感興趣的可以進傳送門

(3)彈性效果公式
終於到最後一個零件啦,彈性效果公式可以從一個網站嘗試得出

http://inloop.github.io/interpolator/

最終得到一個讓自己覺得還不錯的公式

float x = (float) animation.getAnimatedValue();
float factor = 0.15f;
double value = Math.pow(2, -10 * x) * Math.sin((x - factor / 4) * (2 * Math.PI) / factor) + 1;

組裝零件

零件都已經備好了,組裝起來就是我們看到的效果,因爲代碼比較簡單,就不再貼出來了,需要的請進傳送門

2、乘風破浪的小船

文章最開始出現的第二個就是以下這張效果圖,看完第一個實戰代碼,童鞋們心中大概有自己的思路了。這裏就不再講細節了,這裏只是分享下大概的思路。細節可以自行翻閱代碼,代碼在關鍵地方都寫了註釋,傳送門

效果圖

大致思路

(1)繪製藍色的波浪、淺藍色的波浪和小船的軌跡,這裏使用的是二階貝塞爾曲線
(2)將藍色和淺藍色的波浪的波浪繪製至canvas,但偏移量不同。
(3)使用 PathMeasure 測量小船軌跡,同時改變的座標和方向。

這樣便完成了這個 “乘風破浪的小船” 效果

關於 PathMeasure 怎麼使用,感興趣的童鞋,請入傳送門

3、粘性小紅點

粘性小紅點的效果算是見的比較多的,例如QQ的未讀消息便是這效果。這裏同樣也是使用二階貝塞爾曲線,至於細節同樣不給出,代碼中註釋也很齊全,需要的同學請入傳送門

效果圖

五、寫在最後

貝塞爾曲線是小盆友自定義UI中最喜歡的一把利器,因爲其線條的優美和控制的簡單。希望這篇文章也能讓你喜歡上她,並且揮舞起這把利器,做出更多的有個性的自定義組件。如果你從這篇文章有所收穫,請給我個❤️,並關注我吧。文章中如有理解錯誤或是晦澀難懂的語句,請評論區留言,我們進行討論共同進步。你的鼓勵是我前進的最大動力。

貝塞爾曲線Demo的 Github 入口::https://github.com/zincPower/UI2018/blob/e69783a81a6f387a5970327e4c0905ff943e1da7/class7_bezier/src/main/java/com/zinc/class7_bezier/activity/ClientActivity.java

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章