Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 繪制N階Bezier曲線

Android 繪制N階Bezier曲線

編輯:關於Android編程

前段時間公司項目中有用到Bezier曲線的知識,在熟悉Bezier曲線原理和實現方式後,我突發奇想在Android客戶端實現Bezier曲線的構建動畫,於是有了BezierMaker這個項目。在講解代碼前,我們先了解一下Bezier曲線的原理。
項目源碼:https://github.com/venshine/BezierMaker

什麼是Bezier曲線?

貝塞爾曲線於1962年,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來為汽車的主體進行設計。貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau算法開發,以穩定數值的方法求出Bezier曲線。

Bezier曲線的原理

Bezier曲線是用一系列點來控制曲線狀態的,我們將這些點簡單分為兩類:

數據點:確定曲線的起始和結束位置 控制點:確定曲線的彎曲程度

線性Bezier曲線

給定數據點P0、P1,線性Bezier曲線只是一條兩點之間的直線。

線性Bezier曲線公式

一階Bezier矩陣

線性Bezier曲線演示動畫,*t*在[0,1]區間

二階Bezier曲線

二階Bezier曲線的路徑由數據點 P 0、P 2和控制點P 1的函數Bt)追蹤。

二階Bezier曲線公式

二階Bezier矩陣

二階Bezier曲線的結構

二階Bezier曲線演示動畫,*t*在[0,1]區間

為構建二階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方向的“長度有多長”。

三階Bezier曲線公式

三階Bezier矩陣

三階Bezier曲線的結構

三階貝塞爾曲線演示動畫,*t*在[0,1]區間

高階Bezier曲線

N 階貝塞爾曲線可如下推斷。給定點P 0、P 1、…、P n,其Bezier曲線即

N階Bezier曲線公式

上面的公式可用如下遞歸表達,即N 階貝塞爾曲線是雙N-1 階貝塞爾曲線之間的插值。

N階Bezier曲線遞歸公式

N階Bezier矩陣

M(k)]

m(i,j)

四次貝塞爾曲線演示動畫,*t*在[0,1]區間

五次貝塞爾曲線演示動畫,*t*在[0,1]區間

注解

開始於P 0並結叢於P n的曲線,即所謂的端點插值法屬性。 曲線是直線的充分必要條件是所有的控制點都位在曲線上。同樣的,貝塞爾曲線是直線的充分必要條件是控制點共線。 曲線的起始點(結叢點)相切於貝塞爾多邊形的第一節(最後一節)。 一條曲線可在任意點切割成兩條或任意多條子曲線,每一條子曲線仍是貝塞爾曲線。

Bezier曲線的作用

Bezier曲線的作用十分廣泛,在Android移動應用開發中有很多應用的場景。例如QQ小紅點拖拽效果、閱讀軟件的翻書效果、平滑折線圖的制作、很多炫酷的動畫效果等等。

BezierView實戰講解

看到這裡,大部分人應該都了解了Bezier曲線的原理。上面說到了Bezier曲線能夠實現很多炫酷的效果,是不是有點躍躍欲試了?
先欣賞一下最終的實現效果:
2-7階Bezier曲線演示動畫

下面講解一下BezierView這個類的主要代碼,這個自定義類包含了實現Bezier曲線動畫的主要代碼。首先看一下init()初始化方法,代碼如下所示:

    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;
    }

init()方法主要作用是初始化一些必要的信息,包括繪制Bezier曲線的畫筆、數據點和控制點的坐標,繪制路徑以及狀態變量。可以看到,我們在程序中初始化了兩個數據點和一個控制點,代碼中我們把數據點也作為控制點處理。由上面講解的Bezier曲線的原理可以知道,一階(線性)Bezier曲線包含兩個控制點,二階Bezier曲線包含三個控制點,三階Bezier曲線包含四個控制點,可以依次類推N階Bezier曲線包含N+1個控制點。

    private ArrayList buildBezierPoints() {
        ArrayList points = new ArrayList<>();
        int order = mControlPoints.size() - 1;
        float delta = 1.0f / FRAME;
        for (float t = 0; t <= 1; t += delta) {
            // Bezier點集
            points.add(new PointF(deCasteljauX(order, 0, t), deCasteljauY(order, 0, t)));
        }
        return points;
    }

這段代碼主要用於創建Bezier點集。熟悉Android Path用法的同學應該知道Path類中有兩個方法quadTo和cubicTo,通過這兩個方法可以畫出二階和三階Bezier曲線。因為我們這裡需要創建更高階的Bezier曲線,因此我采用德卡斯特裡奧算法(De Casteljau’s Algorithm)來實現Bezier曲線。
不熟悉德卡斯特裡奧算法的同學可以參考我翻譯的一篇文章《德卡斯特裡奧算法——找到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);
    }

下面看一下德卡斯特裡奧算法的實現程序,通過遞歸實現N階Bezier曲線上的點。上面短短幾行代碼就能實現Bezier,是不是很簡單。在這裡我就不解釋這段代碼了,不懂的同學可以參考上一段解釋,看我翻譯的德卡斯特裡奧算法。

private ArrayList>> buildTangentPoints() {
        ArrayList points;   // 1條線點集
        ArrayList> morepoints;    // 多條線點集
        ArrayList>> allpoints = new ArrayList<>();  // 所有點集
        PointF point;
        int order = mControlPoints.size() - 1;
        float delta = 1.0f / FRAME;
        for (int i = 0; i < order - 1; i++) {
            int size = allpoints.size();
            morepoints = new ArrayList<>();
            for (int j = 0; j < order - i; j++) {
                points = new ArrayList<>();
                for (float t = 0; t <= 1; t += delta) {
                    float p0x = 0;
                    float p1x = 0;
                    float p0y = 0;
                    float p1y = 0;
                    int z = (int) (t * FRAME);
                    if (size > 0) {
                        p0x = allpoints.get(i - 1).get(j).get(z).x;
                        p1x = allpoints.get(i - 1).get(j + 1).get(z).x;
                        p0y = allpoints.get(i - 1).get(j).get(z).y;
                        p1y = allpoints.get(i - 1).get(j + 1).get(z).y;
                    } else {
                        p0x = mControlPoints.get(j).x;
                        p1x = mControlPoints.get(j + 1).x;
                        p0y = mControlPoints.get(j).y;
                        p1y = mControlPoints.get(j + 1).y;
                    }
                    float x = (1 - t) * p0x + t * p1x;
                    float y = (1 - t) * p0y + t * p1y;
                    point = new PointF(x, y);
                    points.add(point);
                }
                morepoints.add(points);
            }
            allpoints.add(morepoints);
        }

        return allpoints;
    }

這段代碼不長,但是乍一看有三重for循環,很多人可能頭大了。其實聽我慢慢分析,你會感覺很簡單。這段代碼就是用來實現創建Bezier曲線過程中的折線點集。通過德卡斯特裡奧算法我們知道,二階Bezier曲線有三個控制點和一條折線,在t時刻通過三個點和一條折線確定曲線上唯一的點。接下來三階Bezier曲線在t時刻通過四個點和二條折線確定曲線上唯一的點。以此類推,N階Bezier曲線在t時刻通過N+1個點和N-1條折線確定曲線上唯一的點。理解上面的話,我們就能通過程序來實現折線點集。ArrayList points;定義一段折線的點集,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 tps;
                for (int i = 0; i < tsize; i++) {
                    tps = mInstantTangentPoints.get(i);
                    int tlen = tps.size();
                    for (int j = 0; j < tlen - 1; j++) {
                        mTangentPaint.setColor(Color.parseColor(TANGENT_COLORS[i]));
                        canvas.drawLine(tps.get(j).x, tps.get(j).y, tps.get(j + 1).x, tps.get(j + 1).y,
                                mTangentPaint);
                        canvas.drawCircle(tps.get(j).x, tps.get(j).y, CONTROL_RADIUS, mTangentPaint);
                        canvas.drawCircle(tps.get(j + 1).x, tps.get(j + 1).y, CONTROL_RADIUS, mTangentPaint);
                    }
                }
            }

            // Bezier曲線
            mBezierPath.lineTo(mBezierPoint.x, mBezierPoint.y);
            canvas.drawPath(mBezierPath, mBezierPaint);
            // Bezier曲線起始移動點
            canvas.drawCircle(mBezierPoint.x, mBezierPoint.y, CONTROL_RADIUS, mMovingPaint);
            // 時間展示
            canvas.drawText("t:" + (new DecimalFormat("##0.000").format((float) mR / FRAME)), mWidth - TEXT_HEIGHT *
                    3, mHeight - TEXT_HEIGHT, mTextPaint);

            mHandler.removeMessages(HANDLER_WHAT);
            mHandler.sendEmptyMessage(HANDLER_WHAT);
        }
        if (isTouchable()) {
            // 控制點和控制點連線
            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);
            }
        }
    }

下面我們來繪制Bezier曲線。這段代碼很長,但是很簡單。首先判斷狀態,只有運行且非觸摸狀態時,才可以繪制Bezier曲線。觸摸狀態時僅繪制控制點和控制點之間的連線。運行狀態,如果當前Bezier曲線移動起始點為空,則重置Path,重新設置Path的起始點。接下來for循環繪制控制點和控制點連線以及文本。接下來我們解析瞬時折線點,通過雙重for循環取出mInstantTangentPoints中的點,兩兩連接繪制折線。接下來通過Path連接Bezier曲線上某一時刻的點,繪制該點的路徑。其實onDraw裡的代碼只繪制了某一時刻的點,我們在最後通過handler方式改變t時刻,並重復調用onDraw繪制,一直到曲線繪制周期結束。這樣,一條完美的Bezier曲線就繪制好了。

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 instantpoints;
                    mInstantTangentPoints = new ArrayList<>();
                    for (int i = 0; i < size; i++) {
                        int len = mTangentPoints.get(i).size();
                        instantpoints = new ArrayList<>();
                        for (int j = 0; j < len; j++) {
                            float x = mTangentPoints.get(i).get(j).get(mR).x;
                            float y = mTangentPoints.get(i).get(j).get(mR).y;
                            instantpoints.add(new PointF(x, y));
                        }
                        mInstantTangentPoints.add(instantpoints);
                    }
                }
                if (mR == mBezierPoints.size() - 1) {
                    mState |= STATE_STOP;
                }
                invalidate();
            }
        }
    };

再來看一下handler模塊。這段代碼通過一定速率控制並獲取曲線上的點以及折線上的點,保證曲線最後一幀繪制完成後結束調用。我們再來欣賞一下Bezier曲線的繪制效果,如下圖所示:

七階Bezier曲線演示動畫

以上就是繪制Bezier曲線代碼的核心部分,有疑問的朋友可以在下面留言。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved