Android繪圖最終篇之大戰貝塞爾三次曲線

張風捷特烈發表於2018-11-20

零、前言

1.可以說貝塞爾曲線是一把 "石中劍",能夠拔出它,會讓你的繪圖如虎添翼。
2.今天要與貝塞爾曲線大戰三百回合,將它加入我的繪圖大軍麾下。
3.自此Android繪圖五虎將:Canvas,Path,Paint,Color,貝塞爾便集結完成。
4.本專案原始碼見文尾捷文規範第一條,檢視原始碼在view包,分析工具在analyze包


一、貝塞爾三次曲線初體驗

1.無網格,不曲線,廢話不多說,上網格+座標系
/**
 * 作者:張風捷特烈<br/>
 * 時間:2018/11/16 0016:9:04<br/>
 * 郵箱:1981462002@qq.com<br/>
 * 說明:貝塞爾三次曲線初體驗
 */
public class SimpleCubicView extends View {
    private Point mCoo = new Point(500, 500);//座標系
    private Picture mCooPicture;//座標系canvas元件
    private Picture mGridPicture;//網格canvas元件
    private Paint mHelpPint;//輔助畫筆
    
    private Paint mPaint;//主畫筆
    private Path mPath;//主路徑

    public SimpleCubicView(Context context) {
        this(context, null);
    }

    public SimpleCubicView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();//初始化
    }

    private void init() {
        //初始化主畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
        mPaint.setStrokeWidth(5);
        //初始化主路徑
        mPath = new Path();
        
        //初始化輔助
        mHelpPint = HelpDraw.getHelpPint(Color.RED);
        mCooPicture = HelpDraw.getCoo(getContext(), mCoo);
        mGridPicture = HelpDraw.getGrid(getContext());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(mCoo.x, mCoo.y);
        //TODO ----drawSomething
        canvas.restore();
        HelpDraw.draw(canvas, mGridPicture, mCooPicture);
    }
}
複製程式碼
2.分析一段三次貝塞爾

一段三次貝塞爾曲線是由四個點控制的,四個點分別是幹嘛的,且看分析:

//準備成員變數---四個點
Point p0 = new Point(0, 0);
Point p1 = new Point(200, 200);
Point p2 = new Point(300, -100);
Point p3 = new Point(500, 300);

//onDraw中:
mPath.moveTo(p0.x, p0.y);
mPath.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
canvas.drawPath(mPath, mPaint);
複製程式碼

結果1.png

也許這樣看不出什麼關係:現在把四個控制點也畫出來(紅色):

mHelpPint.setStrokeWidth(10);
HelpDraw.drawPos(canvas, mHelpPint, p0, p1, p2, p3);
複製程式碼

結果2.png

是不是有點意思了--在加兩條線:

mHelpPint.setStrokeWidth(2);
HelpDraw.drawLines(canvas, mHelpPint, p0, p1, p2, p3);
複製程式碼

結果3.png

小結:p0:第一點p3:最終點p1:控制點1p2:控制點2


二、動態效果:任意一段三次貝塞爾曲線的最優雅實現形式

以前看過別人的任意一段三次貝塞爾曲線,感覺體驗太差,切換個點還要點按鈕,
下面我實現四個點任意拖動的三次貝塞爾曲線,可謂是非常優雅的,讓你明白點域的判斷

三次貝塞爾測試.gif

1.判斷一個點是否在一個圓形區域
/**
 * 判斷出是否在某點的半徑為r圓範圍內
 *
 * @param src 目標點
 * @param dst 主動點
 * @param r   半徑
 */
public static boolean judgeCircleArea(Point src, Point dst, float r) {
    return disPos2d(src.x, src.y, dst.x, dst.y) <= r;
}
/**
 * 兩點間距離函式
 */
public static float disPos2d(float x1, float y1, float x2, float y2) {
    return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
複製程式碼
2.觸控事件動態改變點位:
//新增成員變數
Point src = new Point(0, 0);

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            src.x = (int) event.getX() - mCoo.x;
            src.y = (int) event.getY() - mCoo.y;
            break;
        case MotionEvent.ACTION_MOVE:
            if (judgeCircleArea(src, p0, 30)) {
                setPos(event, p0);
            }
            if (judgeCircleArea(src, p1, 30)) {
                setPos(event, p1);
            }
            if (judgeCircleArea(src, p2, 30)) {
                setPos(event, p2);
            }
            if (judgeCircleArea(src, p3, 30)) {
                setPos(event, p3);
            }
            mPath.reset();
            src.x = (int) event.getX() - mCoo.x;
            src.y = (int) event.getY() - mCoo.y;
            invalidate();
            break;
    }
    return true;
}

/**
 * 設定點位
 * @param event 事件
 * @param p 點位
 */
private void setPos(MotionEvent event, Point p) {
    p.x = (int) event.getX() - mCoo.x;
    p.y = (int) event.getY() - mCoo.y;
}
複製程式碼

好了,這樣就行了,是不是一種還沒開始就結束的感覺。


三、貝塞爾曲線實戰1:(初級:運動)

1.映象:

先選取感覺滿意的半邊,記錄四個點位:

左半.png

Point c1p0 = new Point(0, 0);
Point c1p1 = new Point(300, 0);
Point c1p2 = new Point(150, -200);
Point c1p3 = new Point(300, -200);
複製程式碼
2.如何實現下面的效果呢?

貝塞爾單段映象.gif

在原來的基礎上在畫一段貝塞爾曲線,要求:新控制點1(記為:c2p1)和c1p2關於c1p3.x對稱
點關於豎線對稱的原理:(c2p1.x+c1p2.x)/2 = c1p3.x c2p1.y = c1p2.y,轉換一下:c2p1.x=c1p3.x*2-c1p2.x
新控制點2(記為:c2p2)和c1p1關於對稱c1p3.x以及新結尾點(記為:c2p3)和c1p0關於c1p3.x對稱即可

private void reflectY( Point p0, Point p1, Point p2, Point p3, Path path) {
    path.cubicTo(p3.x * 2 - p2.x, p2.y, p3.x * 2 - p1.x, p1.y, p3.x * 2 - p0.x, p0.y);
}
複製程式碼
3.凸出來的一塊慢慢變平的動畫

想象一下,只需要才c1p2和c1p3一起向下移動就行了,要運動,二話不說,ValueAnimator走起
好吧,有點像做俯臥撐,實現起來也挺簡單的:

動態修改.gif

//數字時間流
mAnimator = ValueAnimator.ofFloat(1, 0);
mAnimator.setDuration(2000);
mAnimator.setRepeatMode(ValueAnimator.REVERSE);
mAnimator.setRepeatCount(-1);
mAnimator.addUpdateListener(a -> {
    float rate = (float) a.getAnimatedValue();
    c1p2.y = -(int) (rate * 200);
    c1p3.y = -(int) (rate * 200);
    mPath.reset();
    invalidate();
});
複製程式碼

4.隨便玩玩

原始碼在文尾,檔案是Lever1CubicView.java,大家可以下載,執行自己玩玩,加深一下對貝塞爾三次曲線的感覺

隨便玩玩.gif

好了,開胃菜結束了,下面進入正餐,你沒看錯,好戲才剛剛開始。


四、高階:三階貝塞爾的優雅使用:

注意:前方高能,非戰鬥人員請儘快準備瓜子,飲料,花生米...

1.三階貝塞爾畫圓:

看下圖,你可能會滿臉不屑地說:"切,我用canvas分分秒描畫你信不信?"
老大,我信...且往下看

圓.png

2.如何優雅地繪製多條貝塞爾曲線

下面是四條貝塞爾曲線繪製的圓,看圖就知道優勢在於任意改變形狀
但如果把點位都放在mPath.cubicTo()裡,多幾條線就亂成一鍋粥了,最好統一管理一下
第一個想到的是每條線的三個點都抽成三個成員變數,不過還是很難維護,這個問題一直困擾我
今天突然想到二維陣列不是挺好嗎?二維每個裡面兩個點。

圓分析.png

//單位圓(即半徑為1)控制線長
private static float rate = 0.551915024494f;
/**
 * 單位圓(即半徑為1)的貝塞爾曲線點位
 */
private static final float[][] CIRCLE_ARRAY = {
        //0---第一段線
        {-1, rate},//控制點1
        {1 - rate, 1},//控制點2
        {1, 1},//終點
        //1---第二段線
        {1 + rate, 1},//控制點1
        {2, rate},//控制點2
        {2, 0},//終點
        //2---第二段線
        {2, -rate},//控制點1
        {1 + rate, -1},//控制點2
        {1, -1},//終點
        //3---第四段線
        {1 - rate, -1},//控制點1
        {0, -rate},//控制點2
        {0, 0}//終點
};
複製程式碼
2.繪製迴圈一下就行了

看網上一些繪製方法,點都很亂,看著費勁也晦澀。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();
    canvas.translate(mCoo.x, mCoo.y);

    mPaint.setStyle(Paint.Style.STROKE);
    mPath.lineTo(0, 0);
    for (int i = 0; i < CIRCLE_ARRAY.length / 3; i++) {
        mPath.cubicTo(
                r * CIRCLE_ARRAY[3*i][0], r * CIRCLE_ARRAY[3*i][1],
                r * CIRCLE_ARRAY[3*i + 1][0], r * CIRCLE_ARRAY[3*i + 1][1],
                r * CIRCLE_ARRAY[3*i + 2][0], r * CIRCLE_ARRAY[3*i + 2][1]);
    }
    canvas.drawPath(mPath, mPaint);
    canvas.restore();
}
複製程式碼
3.既然能控制,那來玩玩唄

讓它變形倒不是什麼難事,關鍵是為了明顯些新增輔助點線真是要命,總算是完美展現給大家了

圓的形變.gif

//數字時間流
mAnimator = ValueAnimator.ofFloat(1, 0);
mAnimator.setDuration(2000);
mAnimator.setRepeatMode(ValueAnimator.REVERSE);
mAnimator.setRepeatCount(-1);
mAnimator.addUpdateListener(a -> {
    runNum = (float) a.getAnimatedValue();
    mPath.reset();
    invalidate();
});

//繪製時動態改變

for (int i = 0; i < CIRCLE_ARRAY.length / 3; i++) {
    mPath.cubicTo(
            r * runNum * CIRCLE_ARRAY[3 * i][0], r * runNum * CIRCLE_ARRAY[3 * i][1],
            r * runNum * CIRCLE_ARRAY[3 * i + 1][0], r * runNum * CIRCLE_ARRAY[3 * i + 1][1],
            r * CIRCLE_ARRAY[3 * i + 2][0], r * CIRCLE_ARRAY[3 * i + 2][1]);
}
複製程式碼
4.愛心---剛才是瞎玩的,現在要認真了:

只要控制第三段線的尾部,向下移的話,你應該能想到什麼吧

心形.gif

mPath.cubicTo(//第一段
        r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1],
        r * CIRCLE_ARRAY[1][0], r * CIRCLE_ARRAY[1][1],
        r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]);
mPath.cubicTo(//第二段
        r * CIRCLE_ARRAY[3][0], r * CIRCLE_ARRAY[3][1],
        r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1],
        r * CIRCLE_ARRAY[5][0], r * CIRCLE_ARRAY[5][1]);
mPath.cubicTo(//第三段
        r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1],
        r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1],
        r * CIRCLE_ARRAY[8][0], r * (runNum) * CIRCLE_ARRAY[8][1]);//<----動我試試
mPath.cubicTo(//第四段
        r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1],
        r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1],
        r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
複製程式碼

你也許會說好胖啊,瘦一點可以不
將第一段的控制點2和第二段的控制點1往上移動一點
一共就這麼九個主要點位,任你擺弄,你get到了嗎?

心形優化.gif

mPath.cubicTo(//第一段
        r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1],
        r * CIRCLE_ARRAY[1][0], r * CIRCLE_ARRAY[1][1] - ((1 - runNum) * 0.3f) * r,
        r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]);
mPath.cubicTo(//第二段
        r * CIRCLE_ARRAY[3][0], r * CIRCLE_ARRAY[3][1] - ((1 - runNum) * 0.3f) * r,
        r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1],
        r * CIRCLE_ARRAY[5][0], r * CIRCLE_ARRAY[5][1]);
mPath.cubicTo(//第三段
        r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1],
        r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1],
        r * CIRCLE_ARRAY[8][0], r * CIRCLE_ARRAY[8][1] + ((1 - runNum) * 0.6f) * r);
mPath.cubicTo(//第四段
        r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1],
        r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1],
        r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
複製程式碼

4.想變扁/寬怎麼辦?

下側三個點一起平移

三點下移.gif

mPath.cubicTo(//第一段
        r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1],
        r * CIRCLE_ARRAY[1][0], r * CIRCLE_ARRAY[1][1]+ (1 - runNum) * 0.6f * r,
        r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]+ (1 - runNum) * 0.6f * r);
mPath.cubicTo(//第二段
        r * CIRCLE_ARRAY[3][0], r * CIRCLE_ARRAY[3][1]+ (1 - runNum) * 0.6f * r,
        r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1],
        r * CIRCLE_ARRAY[5][0], r * CIRCLE_ARRAY[5][1]);
mPath.cubicTo(//第三段
        r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1] ,
        r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1] ,
        r * CIRCLE_ARRAY[8][0], r * CIRCLE_ARRAY[8][1]) ;
mPath.cubicTo(//第四段
        r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1],
        r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1],
        r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
複製程式碼

再讓下面變尖一點呢

三點下移尖底.gif

mPath.cubicTo(//第一段
        r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1],
        r * CIRCLE_ARRAY[1][0], r * CIRCLE_ARRAY[1][1]+ (1 - runNum) * 0.6f * r
                - ((1 - runNum) * 0.3f) * r,
        r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]+ (1 - runNum) * 0.6f * r);
mPath.cubicTo(//第二段
        r * CIRCLE_ARRAY[3][0], r * CIRCLE_ARRAY[3][1]+ (1 - runNum) * 0.6f * r
                - ((1 - runNum) * 0.3f) * r,
        r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1],
        r * CIRCLE_ARRAY[5][0], r * CIRCLE_ARRAY[5][1]);
mPath.cubicTo(//第三段
        r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1] ,
        r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1] ,
        r * CIRCLE_ARRAY[8][0], r * CIRCLE_ARRAY[8][1]) ;
mPath.cubicTo(//第四段
        r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1],
        r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1],
        r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
複製程式碼
5.控制點長度變化:UFO的由來...

改變座標,將線1控制點2和線2的控制點1加長

加長控制點.gif

mPath.cubicTo(//第一段
        r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1],
        r * CIRCLE_ARRAY[1][0] - (1 - runNum) * 4f * r, r * CIRCLE_ARRAY[1][1],
        r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]);
mPath.cubicTo(//第二段
        r * CIRCLE_ARRAY[3][0]+ (1 - runNum) * 4f * r, r * CIRCLE_ARRAY[3][1],
        r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1],
        r * CIRCLE_ARRAY[5][0], r * CIRCLE_ARRAY[5][1]);
mPath.cubicTo(//第三段
        r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1],
        r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1],
        r * CIRCLE_ARRAY[8][0], r * CIRCLE_ARRAY[8][1]);
mPath.cubicTo(//第四段
        r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1],
        r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1],
        r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
複製程式碼
6.觸控事件小試

當然你也可以不用ValueAnimate,用觸控事件來控制這些點也是相同的道理。

觸控事件.gif

mPath.cubicTo(//第一段
        r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1],
        r * CIRCLE_ARRAY[1][0], r * CIRCLE_ARRAY[1][1],
        r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]);
mPath.cubicTo(//第二段
        r * CIRCLE_ARRAY[3][0], r * CIRCLE_ARRAY[3][1],
        r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1],
        r * CIRCLE_ARRAY[5][0] + src.x - 2*r, r * CIRCLE_ARRAY[5][1]+ src.y);
mPath.cubicTo(//第三段
        r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1],
        r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1],
        r * CIRCLE_ARRAY[8][0], r * CIRCLE_ARRAY[8][1]);
mPath.cubicTo(//第四段
        r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1],
        r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1],
        r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
複製程式碼

好了,就演示這麼多,你可以把原始碼拷過去自己玩玩,原始碼檔案Lever2CubicView.java
總結一下,一條貝塞爾曲線關鍵就是那三個點,能控制住,貝塞爾曲線可就在你股掌之間。
貝塞爾三次曲線還有很多逆天級別的操作,能力有限,日後有需求再研究吧
把圓形貝塞爾玩轉之後,基本上就能對付了。貝塞爾曲線水很深,只有你想不到,沒有它做不到。


後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1--github 2018-11-20 Android繪圖最終篇之大戰貝塞爾三次曲線
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章