白話經典貝塞爾曲線及其在 Android 中的應用

仰簡發表於2019-05-10

一、前言

談到貝塞爾曲線可能不少人會浮現它高大上的數學公式。然而,在實際應用中,並不需要我們去完全理解或者推匯出公式才能應用得上。實際情況是,即使真的只是一個學渣,我們應該也能很輕鬆的掌握貝塞爾曲線的大致原理及其在開發中的實際應用。

二、白話貝塞爾曲線的原理

貝塞爾曲線有一階、二階、三階....一直到 N 階。實際應用中我們常用的是二階、三階,高階可以由低階來實現。我們們這裡以二階為例來講解一下貝塞爾曲線的基本原理。

二階貝塞爾曲線三要素

1 個起點,1 個終點,1 個控制點

這個是我們要知道的第一個知識點,而三階的話就是 2 個控制點,四階的話就是 3 個,以此類推,N 階的話就是 N - 1 個控制點。而起點和終點始終只有一個。

下面我們來手動畫一個貝塞爾曲線。

  1. 繪製 1 個起點,1 個終點和 1 個控制點,分別為 S 、E、C。然後將 SC、CE 分別連線。如下圖所示。

二階貝塞爾一.jpg

  1. 從點 S 向 C 出發找到一個 D 點,從 C 向 E 出發找到一個 F 點,使得

SD / SC = CF / CE

然後連線 DF。如下圖所示。

二階貝塞爾二.jpg

  1. 在 DF 之間找到點 M,使得

SD / SC = CF / CE = DM / DF

二階貝塞爾三.jpg

總結下: (1) 二階貝塞爾中,起初是 3 個點,然後我們再找 2 個點,然後再找 1 個點。這個點就是我們要找到的點。 (2) 我們需要由 S 向 C 出發,由 C 向 E 出現,找到所有的 D 和 F,再找到所有的 M。 (3) 將所有的 M 連線起來就構造出了最後的所需要的貝塞爾曲線了。

對於更高階的三階甚至N階,其過程是一樣的。再借用一個圖,來詳細觀察一下其構造的過程。

二階貝塞爾曲線.gif

關於貝塞爾曲線的原理,介紹這麼多就夠了,再說就是多餘。如果實在還不明白或者想深入的,推薦以下連結。當然,更建議自己多琢磨琢磨。

貝塞爾曲線 總結 貝塞爾曲線掃盲 貝塞爾曲線遊戲 bezier-circle

三、貝塞爾曲線在 Android 自定義 View 中的實戰

Android 中提供了一個 Path 類,其有 2 個方法 Path#quadTo() 和 Path#cubicTo()。分別用於構造二階和三階貝塞爾曲線的。其原型分別如下。

quadTo() 方法

/**
     * Add a quadratic bezier from the last point, approaching control point
     * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
     * this contour, the first point is automatically set to (0,0).
     *
     * @param x1 The x-coordinate of the control point on a quadratic curve
     * @param y1 The y-coordinate of the control point on a quadratic curve
     * @param x2 The x-coordinate of the end point on a quadratic curve
     * @param y2 The y-coordinate of the end point on a quadratic curve
     */
    public void quadTo(float x1, float y1, float x2, float y2) 
複製程式碼

cubicTo() 方法

 /**
     * Add a cubic bezier from the last point, approaching control points
     * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
     * made for this contour, the first point is automatically set to (0,0).
     *
     * @param x1 The x-coordinate of the 1st control point on a cubic curve
     * @param y1 The y-coordinate of the 1st control point on a cubic curve
     * @param x2 The x-coordinate of the 2nd control point on a cubic curve
     * @param y2 The y-coordinate of the 2nd control point on a cubic curve
     * @param x3 The x-coordinate of the end point on a cubic curve
     * @param y3 The y-coordinate of the end point on a cubic curve
     */
    public void cubicTo(float x1, float y1, float x2, float y2,
                        float x3, float y3)
複製程式碼

引數中,後面的點代表終點,中間的點代表控制點。看起來高大上的貝塞爾曲線,在 Android 中我們主要掌握這 2 個方法的應用基本就可拿下了。當然,實際開發過程中,還有其他問題需要解決,比如拆解,尋找起點,終點和控制點等有時候也是技術活。

下面來看兩個實際的案例。

完整的 demo github.com/ly20050516/…。對於不喜歡看文字講解的,可以直接下載原始碼進行除錯看效果。

3.1 經典案例 —— 流動的水波

先上效果圖。

wave.gif

主要步驟如下:

流動的水波.jpg

核心程式碼如下:

        // 重置 path
        path.reset();
        // 將 path 移到起點 (0,h)
        path.moveTo(0,h);
        // 繪製第 1 部分,終點為 (w / 2,h),控制點為 (w / 4,h + WAVE_AMPLITTUDE),得到一條下凹的曲線
        path.quadTo(w / 4,h + WAVE_AMPLITTUDE,w / 2,h);
        // 第 2 部分再以 (w / 2,h) 為起點,以 (w,h) 為終點,以 (w * 3 / 4,h - WAVE_AMPLITTUDE) 為控制點,得到一條上凸的曲線
        path.quadTo(w * 3 / 4,h - WAVE_AMPLITTUDE,w,h);
        // 第 3 部分和第 4 部分就是重複第 1 部分和第 2 部分。只是注意座標的計算
        path.quadTo(w * 5 / 4,h + WAVE_AMPLITTUDE,w * 3 / 2,h);
        path.quadTo(w * 7 / 4,h - WAVE_AMPLITTUDE,w * 2,h);
        // 然後將 path 封閉得到一填充區域
        path.lineTo(w * 2,getHeight());
        path.lineTo(0,getHeight());
        path.close();

        // 下面的 offset 由屬性動畫來控制其值,變化範圍為 (0,width)
        matrix.reset();
        // 隨著動畫的不斷更新來變換 path 的 offset,從而形成流動的動畫
        matrix.postTranslate(-offset,0);
        path.transform(matrix);

        // 最後繪製出需要的曲面,對,不是曲線了
        canvas.drawPath(path,paint);
複製程式碼

3.2 經典案例 —— 仿 QQ 拖拽清除 tips

效果圖如下

qq.gif

主要步驟:

仿 QQ 擦除.jpg

核心程式碼:

// 計算 2 點之間的距離
        float distance = (float) Math.sqrt(Math.pow((tipsViewMoveX - tipsViewX), 2) + Math.pow((tipsViewMoveY - tipsViewY), 2));
        // 圓的半徑隨著距離越來越遠變和越來越小
        radius = -distance / 15 + DEFAULT_RADIUS;

        if (radius <= 0) {
            isLimit = true;
            return;
        }

        if(tipsViewMoveX - tipsViewX == 0 || tipsViewMoveY - tipsViewY == 0) {
            return;
        }

        /**
         * 計算偏移量 offsetX 以及 offsetY
         *
         * 直線的斜率 k = (y2 - y1) / (x2 - x1) = tan?,所以這裡 Math.atan(k) 就是計算出來的角度,再根據角度分別計算出 offsetX 與 offsetY。
         *
         */
        float offsetX = (float) (radius * Math.sin(Math.atan((tipsViewMoveY - tipsViewY) / (tipsViewMoveX - tipsViewX))));
        float offsetY = (float) (radius * Math.cos(Math.atan((tipsViewMoveY - tipsViewY) / (tipsViewMoveX - tipsViewX))));

        float x1 = tipsViewX - offsetX;
        float y1 = tipsViewY + offsetY;

        float x2 = tipsViewMoveX - offsetX;
        float y2 = tipsViewMoveY + offsetY;

        float x3 = tipsViewMoveX + offsetX;
        float y3 = tipsViewMoveY - offsetY;

        float x4 = tipsViewX + offsetX;
        float y4 = tipsViewY - offsetY;

        // 重置 path
        path.reset();
        // 移到點 (x1,y1)
        path.moveTo(x1,y1);
        // 以 (x1,y1) 為起點,(x2,y2) 為終點,控制點為兩圓心的中心點,畫一條二階貝塞爾曲線
        path.quadTo(controllerX,controllerY,x2,y2);
        // 畫直線
        path.lineTo(x3,y3);
        // 再對稱畫一條二階貝塞爾曲線
        path.quadTo(controllerX,controllerY,x4,y4);
        // 畫直線,封閉區域
        path.lineTo(x1,y1);
複製程式碼

四、總結

貝塞爾曲線在 Android 中的應用本身並不難,主要掌握好 Path#quadTo() 和 Path#cubicTo() 這兩個方法的使用即可。難就難在對目標圖形的拆分以及計算起點,終點和控制點上。

相關文章