Android高級繪製——繪圖篇(三)路徑Path繪製以及貝塞爾曲線使用技巧

開篇

廢話不說了,直接開鑿吧!這篇這要說一下路徑(path)的繪製技巧以及 神一樣的存在(貝塞爾曲線)的繪製

基本繪製

  • 1、直線路徑

    void moveTo(float x1,float y1):直線的開始點,即將直線路徑的繪製點定在(x1,y1)的位置;
    void lineTo(float x2,float y2):直線的結束點,又是下一次繪製路徑的開始點,lineTo() 可以一直調用
    void close():如果連續畫了幾條直線,但沒有形成閉環,調用 close() 方法會自動將路徑的首尾連接起來,形成閉環。


onDraw 方法實現:
    private void init() {
        //初始化畫筆
        paint = new Paint();
        paint.setTextSize(100);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setTextSize(100);     //單位爲 sp
        paint.setStrokeWidth(10);
        path = new Path();


    }

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

        path.moveTo(100, 200);
        path.lineTo(200, 200);
        path.lineTo(300, 400);
        path.lineTo(0, 400);
        path.close();
        canvas.drawPath(path, paint);

    }
![image.png](https://upload-images.jianshu.io/upload_images/11455341-906b446bfd41a3b9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 2、矩形路徑

    void addRect (float left, float top, float right, float bottom, Path.Direction dir)
    void addRect (RectF rect, Path.Direction dir)


這裏的 Path 類創建矩形路徑的參數與前面 canvas 繪製矩形差不多,唯一不同的一點就是增加了 Path.Direction 參數 Path.Direction 取值: - Path.Direction.CCW:是counter-clockwise縮寫,指創建逆時針方向的矩形路徑 - Path.Direction.CW:是clockwise的縮寫,指創建順時針方向的矩形路徑 示例代碼:
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        RectF rect = new RectF(100, 200, 400, 400);
        path.addRect(rect, Path.Direction.CCW);

        canvas.drawPath(path, paint);


        RectF rect1 = new RectF(500, 200, 800, 400);
        path.addRect(rect1, Path.Direction.CW);

        canvas.drawPath(path, paint);
    }
![image.png](https://upload-images.jianshu.io/upload_images/11455341-13ecf0328552833b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 貌似從效果圖中根本看不出順時針和逆時針生成有任何區別,是滴,如果僅僅是做展示使用的話確實是沒有任何區別,但是如果配合 Text 一起使用的話,那區別就很大了,我們前面有介紹過在路徑上繪製 Text ,我們來做一下:
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        String text = "搞笑我們是認真的”;

        Path CCWPath = new Path();
        RectF rect = new RectF(100, 200, 400, 400);
        CCWPath.addRect(rect, Path.Direction.CCW);

        canvas.drawPath(CCWPath, paint);
        canvas.drawTextOnPath(text, CCWPath, 0, 0, paint);


        Path CWPath = new Path();
        RectF rect1 = new RectF(500, 200, 800, 400);
        CWPath.addRect(rect1, Path.Direction.CW);

        canvas.drawPath(CWPath, paint);
        canvas.drawTextOnPath(text, CWPath, 0, 0, paint);
    }
![image.png](https://upload-images.jianshu.io/upload_images/11455341-3f4d477d3fbf6c6a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 3、圓角矩形路徑

void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)

參數:

  • 第一個構造函數可以定製每個角的圓角大小

    • float[] radii:必須傳入 8 個數值,分四組,分別對應每個角所使用的橢圓的橫軸半徑和縱軸半徑,比如(x1,y1,x2,y2,x3,y3,x4,y4),其中,x1,y1 對應第一個角的(左上角)產生的橢圓的橫軸半徑和縱軸半徑,其他類推
  • 第二個構造函數:只能構建統一圓角大小

    • float rx:所產生圓角的橢圓的橫軸半徑;
    • float ry:所產生圓角的橢圓的縱軸半徑;

示例代碼:

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

        Path CCWPath = new Path();
        RectF rect = new RectF(100, 200, 400, 400);
        float[] radii = {30, 30, 50, 50, 70, 70, 90, 90};
        CCWPath.addRoundRect(rect, radii, Path.Direction.CCW);

        canvas.drawPath(CCWPath, paint);


        Path CWPath = new Path();
        RectF rect1 = new RectF(500, 200, 800, 400);
        CWPath.addRoundRect(rect1, 30, 30, Path.Direction.CW);

        canvas.drawPath(CWPath, paint);
    }


}

image.png

  • 4、圓形路徑

    void addCircle (float x, float y, float radius, Path.Direction dir)


參數:
- float x:圓心X軸座標
- float y:圓心Y軸座標
- float radius:圓半徑
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        path.addCircle(300, 300, 200, Path.Direction.CW);
        canvas.drawPath(path, paint);
    }


}
![](https://upload-images.jianshu.io/upload_images/11455341-e3e600753d1f8b7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 5、橢圓路徑

    void addOval (RectF oval, Path.Direction dir)


參數:
- RectF oval:生成橢圓所對應的矩形
- Path.Direction :生成方式,與矩形一樣,分爲順時針與逆時針,意義完全相同,不再重複
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        RectF rectF = new RectF(300, 300, 700, 500);
        path.addOval(rectF, Path.Direction.CW);
        canvas.drawPath(path, paint);


    }
![image.png](https://upload-images.jianshu.io/upload_images/11455341-c73633b147411492.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 6、圓弧路徑

    void addArc (RectF oval, float startAngle, float sweepAngle)


參數說明:
- RectF oval:弧是橢圓的一部分,這個參數就是生成橢圓所對應的矩形;
- float startAngle:開始的角度,X軸正方向爲0度
- float sweepAngel:持續的度數;
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        RectF rectF = new RectF(300, 300, 700, 500);
        path.addArc(rectF, 0, 180);
        canvas.drawPath(path, paint);
    }
![image.png](https://upload-images.jianshu.io/upload_images/11455341-f53a9845c364a91a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

Path之貝塞爾曲線以及水波紋效果

![Jietu20180422-162704.gif](https://upload-images.jianshu.io/upload_images/11455341-18fc596b755dec6f.gif?imageMogr2/auto-orient/strip) Path 中提供了四個函數供我們實現貝塞爾曲線,我們來看一下:
//二階貝賽爾  
public void quadTo(float x1, float y1, float x2, float y2)  
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)  
//三階貝賽爾  
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)  
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
這幾個函數具體怎麼使用,我們下面會詳細介紹,我們先來分析下貝塞爾曲線原理吧
  • 1、貝塞爾曲線來源
    在數學的數值分析領域中,貝賽爾曲線(Bézier曲線)是電腦圖形學中相當重要的參數曲線。更高維度的廣泛化貝塞爾曲線就稱作貝塞爾曲面,其中貝塞爾三角是一種特殊的實例。
    貝塞爾曲線於1962年,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來爲汽車的主體進行設計。貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau算法開發,以穩定數值的方法求出貝塞爾曲線。

  • 2、貝塞爾曲線公式

一階貝塞爾曲線

其公式可概括爲:

image.png

對應的動畫演示爲:

image

P0爲起點、P1爲終點,t表示當前時間,B(t)表示公式的結果值。
注意,曲線的意義就是公式結果B(t)隨時間的變化,其取值所形成的軌跡。在動畫中,黑色點表示在當前時間t下公式B(t)的取值。而紅色的那條線就不在各個時間點下不同取值的B(t)所形成的軌跡。

總而言之:對於一階貝賽爾曲線,大家可以理解爲在起始點和終點形成的這條直線上,勻速移動的點。

二階貝塞爾曲線

公式(同樣看不懂):
image.png

動畫演示:
image

在這裏P0是起始點,P2是終點,P1是控制點
假設將時間定在t=0.25的時刻,此時的狀態如下圖所示:
image.png

首先,P0點和P1點形成了一條貝賽爾曲線,還記得我們上面對一階貝賽爾曲線的總結麼:就是一個點在這條直線上做勻速運動;所以P0-P1這條直線上的移動的點就是Q0;
同樣,P1,P2形成了一條一階貝賽爾曲線,在這條一階貝賽爾曲線上,它們的隨時間移動的點是Q1
最後,動態點Q0和Q1又形成了一條一階貝賽爾曲線,在它們這條一階貝賽爾曲線動態移動的點是B
而B的移動軌跡就是這個二階貝賽爾曲線的最終形態。從上面的講解大家也可以知道,之所以叫它二階貝賽爾曲線是因爲,B的移動軌跡是建立在兩個一階貝賽爾曲線的中間點Q0,Q1的基礎上的。
在理解了二階貝賽爾曲線的形成原理以後,我們就不難理解三階貝賽爾曲線了

三階貝塞爾曲線

公式:

動畫演示
image
同樣,我們取其中一點來講解軌跡的形成原理,當t=0.25時,此時狀態如下:
image.png

同樣,P0是起始點,P3是終點;P1是第一個控制點,P2是第二個控制點;
首先,這裏有三條一階貝賽爾曲線,分別是P0-P1,P1-P2,P2-P3;
他們隨時間變化的點分別爲Q0,Q1,Q2
然後是由Q0,Q1,Q2這三個點,再次連接,形成了兩條一階貝賽爾曲線,分別是Q0—Q1,Q1—Q2;他們隨時間變化的點爲R0,R1
同樣,R0和R1同樣可以連接形成一條一階貝賽爾曲線,在R0—R1這條貝賽爾曲線上隨時間移動的點是B
而B的移動軌跡就是這個三階貝賽爾曲線的最終形狀。
從上面的解析大家可以看出,所謂幾階貝賽爾曲線,全部是由一條條一階貝賽爾曲線搭起來的;
在上圖中,形成一階貝賽爾曲線的直線是灰色的,形成二階貝賽爾曲線線是綠色的,形成三階貝賽爾曲線的線是藍色的。
在理解了上面的二階和三階貝賽爾曲線以後,我們再來看幾個貝賽爾曲線的動態圖

四階貝塞爾曲線

image

五階貝塞爾曲線

image

  • 3、貝塞爾曲線與 PhotoShop 鋼筆工具
    如果有些同學不懂PhotoShop,這篇文章可能就會有些難度了,本篇文章主要是利用PhotoShop的鋼筆工具來得出具體貝塞爾圖像的
    這麼屌的貝賽爾曲線,在專業繪圖工具PhotoShop中當然會有它的蹤影,它就是鋼筆工具,鋼筆工具所使用的路徑彎曲效果就是二階貝賽爾曲線。
    我來給大家演示一下鋼筆工具的用法:

image

我們拿最終成形的圖形來看一下爲什麼鋼筆工具是二階貝賽爾曲線:

image.png

右圖演示的假設某一點t=0.25時,動態點B的位置圖
同樣,這裏P0是起始點,P2是終點,P1是控制點;
P0-P1、P1-P2形成了第一層的一階貝賽爾曲線。它們隨時間的動態點分別是Q0,Q1
動態點Q0,Q1又形成了第二層的一階貝賽爾曲線,它們的動態點是B.而B的軌跡跟鋼筆工具的形狀是完全一樣的。所以鋼筆工具的拉伸效果是使用的二階貝賽爾曲線!
這個圖與上面二階貝賽爾曲線t=0.25時的曲線差不多,大家理解起來難度也不大。
這裏需要注意的是,我們在使用鋼筆工具時,拖動的是P5點。其實二階貝賽爾曲線的控制點是其對面的P1點,鋼筆工具這樣設計是當然是因爲操作起來比較方便。
好了,對貝賽爾曲線的知識講了那麼多,下面開始實戰了,看在代碼中,貝賽爾曲線是怎麼來做的。

Android 中貝塞爾曲線之 quadTo

在開篇中,我們已經提到,在 Path 類中有四個方法與貝塞爾曲線相關,分別是:

//二階貝賽爾  
public void quadTo(float x1, float y1, float x2, float y2)  
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)  
//三階貝賽爾  
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)  
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)  

在這四個函數中,quadTo、rQuadTo 是二階貝塞爾曲線,cubicTo、rCubicTo 是三階貝塞爾曲線,我們這篇文字以二階貝塞爾曲線的 quadTo、rQuadTo爲主,三階貝塞爾曲線 cubicTo、rCubicTo 用的使用方法與二階貝塞爾曲線類似,用處也比較少,這篇就不在細講了

- 1、quadTo 使用原理

這部分我們先來看看quadTo函數的用法,其定義如下:

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

參數中(x1,y1)是控制點座標,(x2,y2)是終點座標
大家可能會有一個疑問:有控制點和終點座標,那起始點是多少呢?
整條線的起始點是通過Path.moveTo(x,y)來指定的,而如果我們連續調用quadTo(),前一個quadTo()的終點,就是下一個quadTo()函數的起點;如果初始沒有調用Path.moveTo(x,y)來指定起始點,則默認以控件左上角(0,0)爲起始點;大家可能還是有點迷糊,下面我們就舉個例子來看看
我們利用quadTo()來畫下面的這條波浪線:

image.png

最關鍵的是如何來確定控制點的位置!前面講過,PhotoShop中的鋼筆工具是二階貝賽爾曲線,所以我們可以利用鋼筆工具來模擬畫出這條波浪線來輔助確定控制點的位置

image

下面我們來看看這個路徑軌跡中,控制點分別在哪個位置

image.png

我們先看P0-P2這條軌跡,P0是起點,假設位置座標是(100,300),P2是終點,假充位置座標是(300,300);在以P0爲起始點,P2爲終點這條二階貝賽爾曲線上,P1是控制點,很明顯P1大概在P0,P2中間的位置,所以它的X座標應該是200,關於Y座標,我們無法確定,但很明顯的是P1在P0,P2點的上方,也就是它的Y值比它們的小,所以根據鋼筆工具上面的位置,我們讓P1的比P0,P2的小100;所以P1的座標是(200,200)
同理,不難求出在P2,P4這條二階貝賽爾曲線上,它們的控制點P3的座標位置應該是(400,400);P3的X座標是400是,因爲P3點是P2,P4的中間點;與P3與P1距離P0-P2-P4這條直線的距離應該是相等的。P1距離P0-P2的值爲100;P3距離P2-P4的距離也應該是100,這樣不難算出P3的座標應該是(400,400);
下面開始是代碼部分了。

  • 2、示例代碼
    private void init() {
        //初始化畫筆
        paint = new Paint();
        paint.setTextSize(100);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setTextSize(100);     //單位爲 sp
        paint.setStrokeWidth(5);
        path = new Path();


    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        path.moveTo(100, 300);
        path.quadTo(200, 200, 300, 300);
        path.quadTo(400, 400, 500, 300);
        canvas.drawPath(path, paint);
    }

效果圖:
image.png

這裏最重要的就是在onDraw(Canvas canvas)中創建Path的過程,我們在上面已經提到,第一個起始點是需要調用path.moveTo(100,300)來指定的,之後後一個path.quadTo的起始點是以前一個path.quadTo的終點爲起始點的。有關控制點的位置如何查找,我們上面已經利用鋼筆工具給大家講解了,這裏就不再細講。
所以,大家在自定義控件的時候,要多跟UED溝通,看他們是如何來實現這個效果的,如果是用的鋼筆工具,那我們也可以效仿使用二階貝賽爾曲線來實現。

源碼在文章底部給出
通過這個例子希望大家知道兩點:
- 整條線的起始點是通過Path.moveTo(x,y)來指定的,如果初始沒有調用Path.moveTo(x,y)來指定起始點,則默認以控件左上角(0,0)爲起始點;
- 而如果我們連續調用quadTo(),前一個quadTo()的終點,就是下一個quadTo()函數的起點;

使用貝塞爾曲線繪製手指軌跡

要實現手指軌跡其實是非常簡單的,我們只需要在自定義中攔截OnTouchEvent,然後根據手指的移動軌跡來繪製Path即可。
要實現把手指的移動軌跡連接起來,最簡單的方法就是直接使用Path.lineTo()就能實現把各個點連接起來。

  • 1、實現方式一:Path.lineTo(x,y)

我們先來看看效果圖

Jietu20180420-112642.gif

(1)、自定義View——CustomView

public class CustomView extends View {

    private Paint paint;
    private Path path;
    private Path path1;

    public CustomView(Context context) {
        super(context);
        init();
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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

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

    private void init() {
        //初始化畫筆
        paint = new Paint();
        paint.setTextSize(100);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setTextSize(100);     //單位爲 sp
        paint.setStrokeWidth(5);
        path = new Path();


    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                path.moveTo(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_MOVE:
                path.lineTo(event.getX(), event.getY());
                postInvalidate();
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);
    }
}

當用戶點擊屏幕的時候,我們調用mPath.moveTo(event.getX(), event.getY());然後在用戶移動手指時使用mPath.lineTo(event.getX(), event.getY());將各個點串起來。然後調用postInvalidate()重繪; 
Path.moveTo()和Path.lineTo()的用法,上篇已經詳細介紹過,理解起來應該沒什麼難度,但這裏有兩個地方需要注意:

  • 第一:有關在case MotionEvent.ACTION_DOWN時return true的問題:return true表示當前控件已經消費了下按動作,之後的ACTION_MOVE、ACTION_UP動作也會繼續傳遞到當前控件中;如果我們在case MotionEvent.ACTION_DOWN時return false,那麼後序的ACTION_MOVE、ACTION_UP動作就不會再傳到這個控件來了。有關動作攔截的知識,後續會在這個系列中單獨來講,大家先期待下吧。
  • 第二:這裏重繪控件使用的是postInvalidate();而我們以前也有用Invalidate()函數的。這兩個函數的作用都是用來重繪控件的,但區別是Invalidate()一定要在UI線程執行,如果不是在UI線程就會報錯。而postInvalidate()則沒有那麼多講究,它可以在任何線程中執行,而不必一定要是主線程。其實在postInvalidate()就是利用handler給主線程發送刷新界面的消息來實現的,所以它是可以在任何線程中執行,而不會出錯。而正是因爲它是通過發消息來實現的,所以它的界面刷新可能沒有直接調Invalidate()刷的那麼快。
    所以在我們確定當前線程是主線程的情況下,還是以invalide()函數爲主。當我們不確定當前要刷新頁面的位置所處的線程是不是主線程的時候,還是用postInvalidate爲好;

  • (2)、使用Path.lineTo()所存在問題
    上面我們雖然實現了,畫出手指的移動軌跡,但我們仔細來看看畫出來的圖:

image.png

我們把S放大,明顯看出,在兩個點連接處有明顯的轉折,而且在S頂部位置橫縱座標變化比較快的位置,看起來跟圖片這大後的馬賽克一樣;利用Path繪圖,是不可能出現馬賽克的,因爲除了Bitmap以外的任何canvas繪圖全部都是矢量圖,也就是利用數學公式來作出來的圖,無論放在多大屏幕上,都不可能會出現馬賽克!這裏利用Path繪圖,在S頂部之所以看起來像是馬賽克是因爲這個S是由各個不同點之間連線寫出來的,而之間並沒有平滑過渡,所以當座標變化比較劇烈時,線與線之間的轉折就顯得特別明顯了。
所以要想優化這種效果,就得實現線與線之間的平滑過渡,很顯然,二階貝賽爾曲線就是幹這個事的。下面我們就利用我們新學的Path.quadTo函數來重新實現下移動軌跡效果。

  • 2、實現方式二(優化):使用Path.quadTo()函數實現過渡

我們上面講了,使用Path.lineTo()的最大問題就是線段轉折處不夠平滑。Path.quadTo()可以實現平滑過渡,但使用Path.quadTo()的最大問題是,如何找到起始點和結束點。
下圖中,有用綠點表示的三個點,連成的兩條直線,很明顯他們轉折處是有明顯摺痕的

image.png

下面我們在PhotoShop中利用鋼筆工具,看如何才能實現這兩條線之間的轉折

image

image.png

從這兩個線段中可以看出,我們使用Path.lineTo()的時候,是直接把手指觸點A,B,C給連起來。
而鋼筆工具要實現這三個點間的流暢過渡,就只能將這兩個線段的中間點做爲起始點和結束點,而將手指的倒數第二個觸點B做爲控制點。
大家可能會覺得,那這樣,在結束的時候,A到P0和P1到C1的這段距離豈不是沒畫進去?是的,如果Path最終沒有close的話,這兩段距離是被拋棄掉的。因爲手指間滑動時,每兩個點間的距離很小,所以P1到C之間的距離可以忽略不計。
下面我們就利用這種方法在photoshop中求證,在連接多個線段時,是否能行?

image.png

在這個圖形中,有很多點連成了彎彎曲曲的線段,我們利用上面我們講的,將兩個線段的中間做爲二階貝爾賽曲線的起始點和終點,把上一個手指的位置做爲控制點,來看看是否真的能組成平滑的連線
整個連接過程如動畫所示:

image

在最終的路徑中看來,各個點間的連線是非常平滑的。從這裏也可以看出,在爲了實現平滑效果,我們只能把開頭的線段一半和結束的線段的一半拋棄掉。
在講了原理之後,下面就來看看在代碼中如何來實現吧。

    private void init() {
        //初始化畫筆
        paint = new Paint();
        paint.setTextSize(100);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setTextSize(100);     //單位爲 sp
        paint.setStrokeWidth(5);
        path = new Path();


    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                eventX = event.getX();
                eventY = event.getY();
                path.moveTo(eventX, eventY);
                break;
            case MotionEvent.ACTION_MOVE:
                float endX = event.getX();
                float endY = event.getY();
                path.quadTo((endX - eventX) / 2 + eventX, (endY - eventY) / 2 + eventY, endX, endY);
                eventX = event.getX();
                eventY = event.getY();
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);
    }

最難的部分依然是onTouchEvent函數這裏

    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                eventX = event.getX();
                eventY = event.getY();
                path.moveTo(eventX, eventY);
                break;

        return true;

在ACTION_DOWN的時候,利用 mPath.moveTo(event.getX(),event.getY())將Path的初始位置設置到手指的觸點處,如果不調用mPath.moveTo的話,會默認是從(0,0)開始的。然後我們定義兩個變量eventX,eventY來表示手指的前一個點。我們通過上面的分析知道,這個點是用來做控制點的。最後return true讓ACTION_MOVE,ACTION_UP事件繼續向這個控件傳遞。

在ACTION_MOVE時:

            case MotionEvent.ACTION_MOVE:
                float endX = event.getX();
                float endY = event.getY();
                path.quadTo((endX - eventX) / 2 + eventX, (endY - eventY) / 2 + eventY, endX, endY);
                eventX = event.getX();
                eventY = event.getY();
                invalidate();
                break;

我們先找到結束點,我們說了結束點是這個線段的中間位置,所以很容易求出它的座標endX,endY;控制點是上一個手指位置即mPreX,mPreY;那有些同學可能會問了,那起始點是哪啊。在開篇講quadTo()函數時,就已經說過,第一個起始點是Path.moveTo(x,y)定義的,其它部分,一個quadTo的終點,是下一個quadTo的起始點。
所以這裏的起始點,就是上一個線段的中間點。所以,這樣就與鋼筆工具繪製過程完全對上了:把各個線段的中間點做爲起始點和終點,把終點前一個手指位置做爲控制點。

同樣把lineTo和quadTo實現的S拿來對比下:

image.png

從效果圖中可以明顯可以看出,通過quadTo實現的曲線更順滑

Ok啦,quadeTo的用法,到這裏就結束了,下部分再來講講rQuadTo的用法及波浪動畫效果

貝塞爾曲線實現水波紋效果

  • Path.rQuadTo():
    該函數聲明如下:

    public void rQuadTo(float dx1, float dy1, float dx2, float dy2)


其中:
  • dx1:控制點X座標,表示相對上一個終點X座標的位移座標,可爲負值,正值表示相加,負值表示相減;
  • dy1:控制點Y座標,相對上一個終點Y座標的位移座標。同樣可爲負值,正值表示相加,負值表示相減;
  • dx2:終點X座標,同樣是一個相對座標,相對上一個終點X座標的位移值,可爲負值,正值表示相加,負值表示相減;
  • dy2:終點Y座標,同樣是一個相對,相對上一個終點Y座標的位移值。可爲負值,正值表示相加,負值表示相減;

這四個參數都是傳遞的都是相對值,相對上一個終點的位移值。
比如,我們上一個終點座標是(300,400)那麼利用rQuadTo(100,-100,200,100);
得到的控制點座標是(300+100,400-100)即(400,300)
同樣,得到的終點座標是(300+200,400+100)即(500,500)

所以下面這兩段代碼是等價的:
利用quadTo定義絕對座標

path.moveTo(300,400);  
path.quadTo(400,300,500,500); 

與利用rQuadTo定義相對座標

path.moveTo(300,400);  
path.rQuadTo(100,-100,200,100)
  • 2、使用rQuadTo實現波浪線

在上篇中,我們使用quadTo實現了一個簡單的波浪線:

image.png

各個點的計算我們上面已經計算過了,

image.png

下面我們將它轉化爲rQuadTo來重新實現下:

private void init() {
        //初始化畫筆
        paint = new Paint();
        paint.setTextSize(100);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setTextSize(100);     //單位爲 sp
        paint.setStrokeWidth(5);
        path = new Path();


    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        path.moveTo(100,300);
        path.rQuadTo(100,-100,200,0);
        path.rQuadTo(100,100,200,0);
        canvas.drawPath(path, paint);
    }

image.png

簡單來講,就是將原來的:

path.moveTo(100,300);
path.quadTo(200,200,300,300);
path.quadTo(400,400,500,300);

轉化爲:

path.moveTo(100,300);
path.rQuadTo(100,-100,200,0);
path.rQuadTo(100,100,200,0);

  • 第一句:path.rQuadTo(100,-100,200,0);是建立在(100,300)這個點基礎上來計算相對座標的。
    所以
    控制點X座標=上一個終點X座標+控制點X位移 = 100+100=200;
    控制點Y座標=上一個終點Y座標+控制點Y位移 = 300-100=200;
    終點X座標 = 上一個終點X座標+終點X位移 = 100+200=300;
    終點Y座標 = 上一個終點Y座標+控制點Y位移 = 300+0=300;
    所以這句與path.quadTo(200,200,300,300);對等的

  • 第二句:path.rQuadTo(100,100,200,0);是建立在它的前一個終點即(300,300)的基礎上來計算相對座標的!
    所以
    控制點X座標=上一個終點X座標+控制點X位移 = 300+100=200;
    控制點Y座標=上一個終點Y座標+控制點Y位移 = 300+100=200;
    終點X座標 = 上一個終點X座標+終點X位移 = 300+200=500;
    終點Y座標 = 上一個終點Y座標+控制點Y位移 = 300+0=300;
    所以這句與path.quadTo(400,400,500,300);對等的

最終效果也是一樣的。
通過這個例子,只想讓大家明白一點:rQuadTo(float dx1, float dy1, float dx2, float dy2)中的位移座標,都是以上一個終點位置爲基準來做偏移的!

實現波浪效果

  • 1、實現全屏波紋
    上面我們已經能夠實現一個波形,只要我們再多實現幾個波形,就可以覆蓋整個屏幕了。

image.png

對應代碼如下:

public class CustomView extends View {

    private Paint paint;
    private Path path;
    private int mItemWaveLength = 400;
    private int originY = 300;
    private int diffY = 100;

    public CustomView(Context context) {
        super(context);
        init();
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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


    private void init() {
        //初始化畫筆
        paint = new Paint();
        paint.setColor(Color.RED);
        path = new Path();
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        path.reset();
        path.moveTo(-mItemWaveLength, originY);
        int halfWaveLen = mItemWaveLength / 2;
        for (int i = -mItemWaveLength; i <= getWidth() + mItemWaveLength; i += mItemWaveLength) {
            path.rQuadTo(halfWaveLen / 2, -diffY, halfWaveLen, 0);
            path.rQuadTo(halfWaveLen / 2, diffY, halfWaveLen, 0);
        }

        path.lineTo(getWidth(), getHeight());
        path.lineTo(0, getHeight());
        path.close();

        canvas.drawPath(path, paint);
    }
}

最難的部分依然是在onDraw函數中:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        path.reset();
        path.moveTo(-mItemWaveLength, originY);
        int halfWaveLen = mItemWaveLength / 2;
        for (int i = -mItemWaveLength; i <= getWidth() + mItemWaveLength; i += mItemWaveLength) {
            path.rQuadTo(halfWaveLen / 2, -diffY, halfWaveLen, 0);
            path.rQuadTo(halfWaveLen / 2, diffY, halfWaveLen, 0);
        }
        canvas.drawPath(path, paint);
    }

我們將mPath的起始位置向左移一個波長(一個凹凸波屬於一個波長):

mPath.moveTo(-mItemWaveLength,originY);

然後利用for循環畫出當前屏幕中可能容得下的所有波:

 for (int i = -mItemWaveLength; i <= getWidth() + mItemWaveLength; i += mItemWaveLength) {
            path.rQuadTo(halfWaveLen / 2, -diffY, halfWaveLen, 0);
            path.rQuadTo(halfWaveLen / 2, diffY, halfWaveLen, 0);
        }

mPath.rQuadTo(halfWaveLen/2,-diffY,halfWaveLen,0);畫的是一個波長中的前半個波,mPath.rQuadTo(halfWaveLen/2,diffY,halfWaveLen,0);畫的是一個波長中的後半個波。大家在這裏可以看到,屏幕左右都多畫了一個波長的圖形。這是爲了波形移動做準備的。
到這裏,我們是已經能畫出來一整屏幕的波形了,下面我們把整體波形閉合起來。並改變畫筆樣式爲填充

    private void init() {
        //初始化畫筆
        paint = new Paint();
        paint.setColor(Color.RED);
        path = new Path();
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        path.reset();
        path.moveTo(-mItemWaveLength + dx, originY);
        int halfWaveLen = mItemWaveLength / 2;
        for (int i = -mItemWaveLength; i <= getWidth() + mItemWaveLength; i += mItemWaveLength) {
            path.rQuadTo(halfWaveLen / 2, -diffY, halfWaveLen, 0);
            path.rQuadTo(halfWaveLen / 2, diffY, halfWaveLen, 0);
        }

        path.lineTo(getWidth(), getHeight());
        path.lineTo(0, getHeight());
        path.close();

        canvas.drawPath(path, paint);
    }
  • 2、實現移動動畫
    讓波紋動起來其實挺簡單,利用調用在path.moveTo的時候,將起始點向右移動即可實現移動,而且只要我們移動一個波長的長度,波紋就會重合,就可以實現無限循環了。
    爲此我們定義一個動畫:
public void startAnim(){  
    ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength);  
    animator.setDuration(2000);  
    animator.setRepeatCount(ValueAnimator.INFINITE);  
    animator.setInterpolator(new LinearInterpolator());  
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
        @Override  
        public void onAnimationUpdate(ValueAnimator animation) {  
            dx = (int)animation.getAnimatedValue();  
            postInvalidate();  
        }  
    });  
    animator.start();  
}  

動畫的長度爲一個波長,將當前值保存在類的成員變量dx中;
然後在畫圖的時候,在path.moveTo()中加上現在的移動值dx:mPath.moveTo(-mItemWaveLength+dx,originY);
完整的繪圖代碼如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    path.reset();
    path.moveTo(-mItemWaveLength + dx, originY);
    int halfWaveLen = mItemWaveLength / 2;
    for (int i = -mItemWaveLength; i <= getWidth() + mItemWaveLength; i += mItemWaveLength) {
        path.rQuadTo(halfWaveLen / 2, -diffY, halfWaveLen, 0);
        path.rQuadTo(halfWaveLen / 2, diffY, halfWaveLen, 0);
    }

    path.lineTo(getWidth(), getHeight());
    path.lineTo(0, getHeight());
    path.close();

    canvas.drawPath(path, paint);
}

Jietu20180422-161121.gif

如果把波長設置爲1000,就可以實現本段開篇的動畫了。
如果想讓波紋像開篇時那要同時向下移動,大家只需要在path.moveTo(x,y)的時候,通過同時移動y座標就可以了

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        path.reset();
        path.moveTo(-mItemWaveLength + dx, originY + dy);
        int halfWaveLen = mItemWaveLength / 2;
        for (int i = -mItemWaveLength; i <= getWidth() + mItemWaveLength; i += mItemWaveLength) {
            path.rQuadTo(halfWaveLen / 2, -diffY, halfWaveLen, 0);
            path.rQuadTo(halfWaveLen / 2, diffY, halfWaveLen, 0);
        }

        path.lineTo(getWidth(), getHeight());
        path.lineTo(0, getHeight());
        path.close();

        canvas.drawPath(path, paint);
        //dy控制波紋向下移動
        dy++;
        if (dy >= getHeight() - originY + diffY){
            mHanimator.cancel();
        }
    }

最終效果圖如下:
Jietu20180422-162704.gif

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