Android 路徑繪制藝術(shù)——貝塞爾曲線

?


?

目錄

1)什么是貝塞爾曲線
2)貝塞爾曲線圖解
3)Android繪制貝塞爾曲線
4)繪制水波紋效果

?

概述

什么是貝塞爾曲線?

貝塞爾曲線的數(shù)學(xué)基礎(chǔ)是早在 1912 年就廣為人知的伯恩斯坦多項(xiàng)式。但直到 1959 年,當(dāng)時(shí)就職于雪鐵龍的法國(guó)數(shù)學(xué)家 Paul de Casteljau 才開始對(duì)它進(jìn)行圖形化應(yīng)用的嘗試,并提出了一種數(shù)值穩(wěn)定的 de Casteljau 算法。然而貝塞爾曲線的得名,卻是由于 1962 年另一位就職于雷諾的法國(guó)工程師 Pierre Bézier 的廣泛宣傳。他使用這種只需要很少的控制點(diǎn)就能夠生成復(fù)雜平滑曲線的方法,來(lái)輔助汽車車體的工業(yè)設(shè)計(jì)。

只要你使用過(guò)圖像處理工具,肯定對(duì)“鋼筆”這個(gè)詞不陌生,它本質(zhì)上就是運(yùn)用貝塞爾曲線來(lái)作為計(jì)算基礎(chǔ)繪制出來(lái)的路徑:

鋼筆

?

貝塞爾曲線原理

貝塞爾曲線是怎么描繪出這樣一條弧線的呢,其實(shí)主要是依靠頂點(diǎn)間的比例來(lái)計(jì)算,以二階貝塞爾為例,示意圖如下:

二階計(jì)算示意圖

可以看到一共有6個(gè)點(diǎn),假設(shè)此時(shí)AD占AB的25%,那么在BC上也有這么一個(gè)點(diǎn)F,使得BF:BC也是25%,連接DF,在DF上面再找出使得DG:DF=25%的點(diǎn)G,以這個(gè)為基本公式,繪制出D點(diǎn)從A運(yùn)動(dòng)到B的過(guò)程中,計(jì)算出來(lái)一系列的G點(diǎn)形成的弧線,即為二階貝塞爾曲線的路徑,動(dòng)態(tài)效果圖如下:

二階貝塞爾動(dòng)圖

三階貝塞爾曲線其實(shí)就是在二階的基礎(chǔ)上,再增加一條邊線,如下:


三階貝塞爾示意圖

可以看到,多出了一條CD線,同樣是需要滿足AE:AB = BF:BC = CG:CD,計(jì)算出E、F、G之后,連接EF和FG,可以得到兩條直線,接著就按照二階的計(jì)算方式繼續(xù)計(jì)算,得到點(diǎn)O的位置,可以看出三階是在二階的基礎(chǔ)上再套一層,所以才稱之為三階貝塞爾,動(dòng)態(tài)效果圖如下:


三階貝塞爾動(dòng)圖

依此類推,還會(huì)有四階、五階等等更復(fù)雜的貝塞爾曲線,但在Android開發(fā)中只提供了二階和三階的API,因此我們只探討這兩種的繪制方式。

?

繪制貝塞爾曲線

在Android中,Path類提供了四個(gè)繪制貝塞爾曲線的方法:

二階貝塞爾繪制API:
public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
三階貝塞爾繪制API:
public void quadTo(float x1, float y1, float x2, float y2, float x3, float y3)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2, float dx3, float dy3)

可以看到二階與三階的區(qū)別就在于多了一組參數(shù),首先看下二階,剛才已經(jīng)分析了二階貝塞爾的繪制一共有3個(gè)重要的頂點(diǎn),可以理解為起始點(diǎn)(示意圖中的A),控制點(diǎn)(示意圖中的B),終點(diǎn)(示意圖中的C),這里傳入兩個(gè)頂點(diǎn),分別代表著控制點(diǎn)和終點(diǎn),那么問(wèn)題來(lái)了,起始點(diǎn)呢?起始點(diǎn)就是Path上一次的終點(diǎn)(比如moveTo移動(dòng)到的點(diǎn)),如果沒(méi)有指定(即之前還從未移動(dòng)過(guò)Path),則默認(rèn)以控件左上角為起始點(diǎn),舉個(gè)例子,我們繪制一段簡(jiǎn)單的貝塞爾:

/**
 * Created by YANG on 2019/2/23.
 */
public class BezierView extends View {

    private Paint mPaint;
    private Path mBezierPath;

    private Path mPointPath;

    private Point mStartPoint;
    private Point mControlPoint;
    private Point mEndPoint;

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

    public BezierView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BezierView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);

        mBezierPath = new Path();
        mPointPath = new Path();

        mStartPoint = new Point();
        mStartPoint.set(100, 300);
        mControlPoint = new Point();
        mControlPoint.set(300, 100);
        mEndPoint = new Point();
        mEndPoint.set(500, 500);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //貝塞爾
        mBezierPath.moveTo(mStartPoint.x, mStartPoint.y);
        mBezierPath.quadTo(mControlPoint.x, mControlPoint.y, mEndPoint.x, mEndPoint.y);

        //連接線
        mPointPath.moveTo(mStartPoint.x, mStartPoint.y);
        mPointPath.lineTo(mControlPoint.x, mControlPoint.y);
        mPointPath.lineTo(mEndPoint.x, mEndPoint.y);

        //繪制起始點(diǎn)、控制點(diǎn)、終點(diǎn)的連線
        canvas.drawPath(mPointPath, mPaint);

        //繪制貝塞爾
        mPaint.setColor(Color.RED);
        canvas.drawPath(mBezierPath, mPaint);
    }
}

一共聲明了3個(gè)點(diǎn),且首先調(diào)用moveTo(mStartPoint.x, mStartPoint.x)將起始點(diǎn)移動(dòng)到(300,300),接著調(diào)用quadTo將控制點(diǎn)和終點(diǎn)傳進(jìn)去,就可以得到一條貝塞爾曲線(紅色部分):

二階繪制

因此quadTo傳進(jìn)去的參數(shù)是控制點(diǎn)和終點(diǎn)的坐標(biāo)位置,那Path的rQuadTo又有什么用呢?其實(shí)rQuadTo功能上跟quadTo是一樣的,但是傳進(jìn)去的是相對(duì)距離,也就是說(shuō)相對(duì)于起始點(diǎn)的位移,比如我們?cè)趧偛诺睦又性偌狱c(diǎn)東西,追加一段曲線mBezierPath.rQuadTo(200, 300, 400, -200);

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //貝塞爾
        mBezierPath.moveTo(mStartPoint.x, mStartPoint.y);
        mBezierPath.quadTo(mControlPoint.x, mControlPoint.y, mEndPoint.x, mEndPoint.y);
        mBezierPath.rQuadTo(200, 300, 400, -200);

        //連接線
        mPointPath.moveTo(mStartPoint.x, mStartPoint.y);
        mPointPath.lineTo(mControlPoint.x, mControlPoint.y);
        mPointPath.lineTo(mEndPoint.x, mEndPoint.y);

        //繪制起始點(diǎn)、控制點(diǎn)、終點(diǎn)的連線
        canvas.drawPath(mPointPath, mPaint);

        //繪制貝塞爾
        mPaint.setColor(Color.RED);
        canvas.drawPath(mBezierPath, mPaint);
    }

效果如圖:


二階繪制波浪線

可以看到第二段曲線的起點(diǎn)是第一段曲線的終點(diǎn),且可以發(fā)現(xiàn),rQuadTo傳的控制點(diǎn)(200,300)并非坐標(biāo),而是相對(duì)于第一段曲線的終點(diǎn)(500,500)來(lái)計(jì)算,即(500+200, 500+300)才是第二段曲線控制點(diǎn)的真正坐標(biāo),同理第二段曲線終點(diǎn)的坐標(biāo)是(500+40, 500-200)。

三階貝塞爾曲線的方法的使用方法跟二階貝塞爾曲線差不多,就不再?gòu)?fù)述了。

?

繪制水波紋效果

上面的例子繪制了一段簡(jiǎn)單的波浪線,這其實(shí)就是我們繪制水波紋效果的基礎(chǔ),就相當(dāng)于完成了一個(gè)浪,如果有很多個(gè)水浪就可以組合成此起彼伏的效果:

public class BezierView extends View{

    private Paint paint;
    private Path mPath;
    private int mItemWidth = 600;

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

    public BezierView2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BezierView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);

        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        int halfItem = mItemWidth / 2;
        mPath.moveTo(0, 300);
        for(int i=0; i<mItemWidth + getWidth(); i+=mItemWidth){
            mPath.rQuadTo(halfItem/2, -100, halfItem, 0);
            mPath.rQuadTo(halfItem/2, 100, halfItem, 0);
        }
        canvas.drawPath(mPath, paint);
    }
}

我們將每段波浪的寬度定義為600,因此每半段波浪的高度為300,首先將起點(diǎn)移動(dòng)到(0,300)處,即整個(gè)View最左側(cè)的一個(gè)點(diǎn),接著開始遍歷繪制后續(xù)多段波浪,mPath.rQuadTo(halfItem/2, -100, halfItem, 0);表示右移半個(gè)波浪, 并且上移100,即一個(gè)浪的最高點(diǎn),接著mPath.rQuadTo(halfItem/2, 100, halfItem, 0);再右移半個(gè)波浪,并且下移100,即一個(gè)浪的最低點(diǎn),這就形成了一段完整的波浪,然后以此循環(huán),直到超過(guò)View的最大寬度:
?

靜態(tài)水波紋

?
靜態(tài)效果完成了,如何讓它動(dòng)起來(lái)呢?就要結(jié)合ValueAnimator了,不斷改變整個(gè)波浪的起始點(diǎn):

/**
 * Created by YANG on 2019/2/23.
 */
public class BezierView2 extends View {

    private Paint paint;
    private Path mPath;
    private int mItemWidth = 600;

    private ValueAnimator mAnimator;
    private int mOffsetX;

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

    public BezierView2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BezierView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);

        mPath = new Path();

        mAnimator = ValueAnimator.ofInt(0, mItemWidth);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mOffsetX = (int) animation.getAnimatedValue();
                invalidate();
            }
        });

        mAnimator.setInterpolator(new LinearInterpolator());

        mAnimator.setDuration(1000);
        mAnimator.setRepeatCount(-1);
        mAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        int halfItem = mItemWidth / 2;
        //必須先減去一個(gè)浪的寬度,以便第一遍動(dòng)畫能夠剛好位移出一個(gè)波浪,形成無(wú)限波浪的效果
        mPath.moveTo(-mItemWidth + mOffsetX, halfItem);
        for (int i = -mItemWidth; i < mItemWidth + getWidth(); i += mItemWidth) {
            mPath.rQuadTo(halfItem / 2, -100, halfItem, 0);
            mPath.rQuadTo(halfItem / 2, 100, halfItem, 0);
        }
        canvas.drawPath(mPath, paint);
    }
}

注意,起點(diǎn)改為了mPath.moveTo(-mItemWidth + mOffsetX, halfItem),為何不是(mOffsetX, halfItem)呢,因?yàn)閙OffsetX的變化范圍是從0到mItemWidth,如果一開始不減去mItemWidth,就會(huì)導(dǎo)致啟動(dòng)動(dòng)畫之后波浪左邊總是會(huì)露出一段空白區(qū)域,整個(gè)動(dòng)畫銜接不起來(lái),無(wú)法形成無(wú)限循環(huán)的視覺(jué)效果。

最后,再給這個(gè)水波紋效果填充上“水”,設(shè)置畫筆填充模式為Paint.FILL,換個(gè)顏色,然后將我們的路徑閉合起來(lái):

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        int halfItem = mItemWidth / 2;
        //必須先減去一個(gè)浪的寬度,以便第一遍動(dòng)畫能夠剛好位移出一個(gè)波浪,形成無(wú)限波浪的效果
        mPath.moveTo(-mItemWidth + mOffsetX, halfItem);
        for (int i = -mItemWidth; i < mItemWidth + getWidth(); i += mItemWidth) {
            mPath.rQuadTo(halfItem / 2, -100, halfItem, 0);
            mPath.rQuadTo(halfItem / 2, 100, halfItem, 0);
        }

        //閉合路徑波浪以下區(qū)域
        mPath.lineTo(getWidth(), getHeight());
        mPath.lineTo(0, getHeight());
        mPath.close();
        
        canvas.drawPath(mPath, paint);
    }

?
最終效果如下:


水波紋效果圖

?

總結(jié)

之前一直有看到水波紋的效果,沒(méi)來(lái)得及細(xì)細(xì)研究,這次終于總結(jié)在一塊了,這里只是水波紋的基礎(chǔ)效果,還有很多拓展的方式,比如可以有多條水波紋疊加,水波紋進(jìn)度球等更炫酷的效果。當(dāng)然,貝塞爾曲線不單單可以做水波紋效果,還有很多其他的用法,類似于QQ的拖動(dòng)取消新消息提醒的效果,手寫路徑優(yōu)化等等,由于篇幅有限,下次再敘。

歡迎關(guān)注 Android小Y 的簡(jiǎn)書,更多Android精選自定義View

Android 玩轉(zhuǎn)PathMeasure之自定義支付結(jié)果動(dòng)畫
Android 自定義弧形旋轉(zhuǎn)菜單欄——衛(wèi)星菜單
Android 自定義帶入場(chǎng)動(dòng)畫的弧形百分比進(jìn)度條

GitHubGitHub-ZJYWidget
CSDN博客IT_ZJYANG
簡(jiǎn) 書Android小Y
在Github上建了一個(gè)集合炫酷自定義View的項(xiàng)目,里面有很多實(shí)用的自定義View源碼及demo,會(huì)長(zhǎng)期維護(hù),歡迎Star~ 如有不足之處或建議還望指正,相互學(xué)習(xí),相互進(jìn)步,如果覺(jué)得不錯(cuò)動(dòng)動(dòng)小手給個(gè)Star, 謝謝~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容