Android中Canvas繪圖基礎詳解(附原始碼下載)

孫群發表於2015-11-11

Android中,如果我們想繪製複雜的自定義View或遊戲,我們就需要熟悉繪圖API。Android通過Canvas類暴露了很多drawXXX方法,我們可以通過這些方法繪製各種各樣的圖形。Canvas繪圖有三個基本要素:Canvas、繪圖座標系以及Paint。Canvas是畫布,我們通過Canvas的各種drawXXX方法將圖形繪製到Canvas上面,在drawXXX方法中我們需要傳入要繪製的圖形的座標形狀,還要傳入一個畫筆Paint。drawXXX方法以及傳入其中的座標決定了要繪製的圖形的形狀,比如drawCircle方法,用來繪製圓形,需要我們傳入圓心的x和y座標,以及圓的半徑。drawXXX方法中傳入的畫筆Paint決定了繪製的圖形的一些外觀,比如是繪製的圖形的顏色,再比如是繪製圓面還是圓的輪廓線等。Android系統的設計吸收了很多已有系統的諸多優秀之處,比如Canvas繪圖。Canvas不是Android所特有的,Flex和Silverlight都支援Canvas繪圖,Canvas也是HTML5標準中的一部分,主流的現代瀏覽器都支援用JavaScript在Canvas上繪圖,如果你用過HTML5中的Canvas,你會發現Android的Canvas的繪圖API與其很相似。總之,Canvas繪圖不是Android所特有的。

為了演示Android中各種drawXXX方法的時候,我做了一個App,通過單擊相應的按鈕繪製相應的圖形,主介面如下所示:
這裡寫圖片描述


Canvas座標系與繪圖座標系

Canvas繪圖中牽扯到兩種座標系:Canvas座標系與繪圖座標系。

  • Canvas座標系
    Canvas座標系指的是Canvas本身的座標系,Canvas座標系有且只有一個,且是唯一不變的,其座標原點在View的左上角,從座標原點向右為x軸的正半軸,從座標原點向下為y軸的正半軸。

  • 繪圖座標系
    Canvas的drawXXX方法中傳入的各種座標指的都是繪圖座標系中的座標,而非Canvas座標系中的座標。預設情況下,繪圖座標系與Canvas座標系完全重合,即初始狀況下,繪圖座標系的座標原點也在View的左上角,從原點向右為x軸正半軸,從原點向下為y軸正半軸。但不同於Canvas座標系,繪圖座標系並不是一成不變的,可以通過呼叫Canvas的translate方法平移座標系,可以通過Canvas的rotate方法旋轉座標系,還可以通過Canvas的scale方法縮放座標系,而且需要注意的是,translate、rotate、scale的操作都是基於當前繪圖座標系的,而不是基於Canvas座標系,一旦通過以上方法對座標系進行了操作之後,當前繪圖座標系就變化了,以後繪圖都是基於更新的繪圖座標系了。也就是說,真正對我們繪圖有用的是繪圖座標系而非Canvas座標系。

為了更好的理解繪圖座標系,請看如下的程式碼:

    //繪製座標系
    private void drawAxis(Canvas canvas){
        int canvasWidth = canvas.getWidth();
        int canvasHeight = canvas.getHeight();
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeCap(Paint.Cap.ROUND);
        paint.setStrokeWidth(6 * density);

        //用綠色畫x軸,用藍色畫y軸

        //第一次繪製座標軸
        paint.setColor(0xff00ff00);//綠色
        canvas.drawLine(0, 0, canvasWidth, 0, paint);//繪製x軸
        paint.setColor(0xff0000ff);//藍色
        canvas.drawLine(0, 0, 0, canvasHeight, paint);//繪製y軸

        //對座標系平移後,第二次繪製座標軸
        canvas.translate(canvasWidth / 4, canvasWidth /4);//把座標系向右下角平移
        paint.setColor(0xff00ff00);//綠色
        canvas.drawLine(0, 0, canvasWidth, 0, paint);//繪製x軸
        paint.setColor(0xff0000ff);//藍色
        canvas.drawLine(0, 0, 0, canvasHeight, paint);//繪製y軸

        //再次平移座標系並在此基礎上旋轉座標系,第三次繪製座標軸
        canvas.translate(canvasWidth / 4, canvasWidth / 4);//在上次平移的基礎上再把座標系向右下角平移
        canvas.rotate(30);//基於當前繪圖座標系的原點旋轉座標系
        paint.setColor(0xff00ff00);//綠色
        canvas.drawLine(0, 0, canvasWidth, 0, paint);//繪製x軸
        paint.setColor(0xff0000ff);//藍色
        canvas.drawLine(0, 0, 0, canvasHeight, paint);//繪製y軸
    }

介面如下所示:
這裡寫圖片描述

第一次繪製繪圖座標系時,繪圖座標系預設情況下和Canvas座標系重合,所以繪製出的座標系緊貼View的上側和左側;
第二次首先將座標軸向右下角平移了一段距離,然後繪製出的座標系也就整體向右下角平移了;
第三次再次向右下角平移,並旋轉了30度,圖上傾斜的座標系即最後的繪圖座標系。


drawARGB

Canvas中的drawARGB可以用來對整個Canvas以某種統一的顏色整體繪製,四個引數分別是Alpha、Red、Green、Blue,取值都是0-255。
使用程式碼如下:

private void drawARGB(Canvas canvas){
        canvas.drawARGB(255, 139, 197, 186);
    }

介面如下所示:
這裡寫圖片描述


drawText

Canvas中用drawText方法繪製文字,程式碼如下所示:

private void drawText(Canvas canvas){
        int canvasWidth = canvas.getWidth();
        int halfCanvasWidth = canvasWidth / 2;
        float translateY = textHeight;

        //繪製正常文字
        canvas.save();
        canvas.translate(0, translateY);
        canvas.drawText("正常繪製文字", 0, 0, paint);
        canvas.restore();
        translateY += textHeight * 2;

        //繪製綠色文字
        paint.setColor(0xff00ff00);//設定字型為綠色
        canvas.save();
        canvas.translate(0, translateY);//將畫筆向下移動
        canvas.drawText("繪製綠色文字", 0, 0, paint);
        canvas.restore();
        paint.setColor(0xff000000);//重新設定為黑色
        translateY += textHeight * 2;

        //設定左對齊
        paint.setTextAlign(Paint.Align.LEFT);//設定左對齊
        canvas.save();
        canvas.translate(halfCanvasWidth, translateY);
        canvas.drawText("左對齊文字", 0, 0, paint);
        canvas.restore();
        translateY += textHeight * 2;

        //設定居中對齊
        paint.setTextAlign(Paint.Align.CENTER);//設定居中對齊
        canvas.save();
        canvas.translate(halfCanvasWidth, translateY);
        canvas.drawText("居中對齊文字", 0, 0, paint);
        canvas.restore();
        translateY += textHeight * 2;

        //設定右對齊
        paint.setTextAlign(Paint.Align.RIGHT);//設定右對齊
        canvas.save();
        canvas.translate(halfCanvasWidth, translateY);
        canvas.drawText("右對齊文字", 0, 0, paint);
        canvas.restore();
        paint.setTextAlign(Paint.Align.LEFT);//重新設定為左對齊
        translateY += textHeight * 2;

        //設定下劃線
        paint.setUnderlineText(true);//設定具有下劃線
        canvas.save();
        canvas.translate(0, translateY);
        canvas.drawText("下劃線文字", 0, 0, paint);
        canvas.restore();
        paint.setUnderlineText(false);//重新設定為沒有下劃線
        translateY += textHeight * 2;

        //繪製加粗文字
        paint.setFakeBoldText(true);//將畫筆設定為粗體
        canvas.save();
        canvas.translate(0, translateY);
        canvas.drawText("粗體文字", 0, 0, paint);
        canvas.restore();
        paint.setFakeBoldText(false);//重新將畫筆設定為非粗體狀態
        translateY += textHeight * 2;

        //文字繞繪製起點順時針旋轉
        canvas.save();
        canvas.translate(0, translateY);
        canvas.rotate(20);
        canvas.drawText("文字繞繪製起點旋轉20度", 0, 0, paint);
        canvas.restore();
    }

介面如下所示:
這裡寫圖片描述

對以上程式碼進行一下說明:

  1. Android中的畫筆有兩種Paint和TextPaint,我們可以Paint來畫其他的圖形:點、線、矩形、橢圓等。TextPaint繼承自Paint,是專門用來畫文字的,由於TextPaint繼承自Paint,所以也可以用TextPaint畫點、線、面、矩形、橢圓等圖形。

  2. 我們在上面的程式碼中將canvas.translate()和canvas.rotate()放到了canvas.save()和canvas.restore()之間,這樣做的好處是,在canvas.save()呼叫時,將當前座標系儲存下來,將當前座標系的矩陣Matrix入棧儲存,然後通過translate或rotate等對座標系進行變換,然後進行繪圖,繪圖完成後,我們通過呼叫canvas.restore()將之前儲存的Matrix出棧,這樣就將當前繪圖座標系恢復到了canvas.save()執行的時候狀態。如果熟悉OpenGL開發,對這種模式應該很瞭解。

  3. 通過呼叫paint.setColor(0xff00ff00)將畫筆設定為綠色,paint的setColor方法需要傳入一個int值,通常情況下我們寫成16進位制0x的形式,第一個位元組儲存Alpha通道,第二個位元組儲存Red通道,第三個位元組儲存Green通道,第四個位元組儲存Blue通道,每個位元組的取值都是從00到ff。如果對這種設定顏色的方式不熟悉,也可以呼叫paint.setARGB(int a, int r, int g, int b)方法設定畫筆的顏色,不過paint.setColor(int color)的方式更簡潔。

  4. 通過呼叫paint.setTextAlign()設定文字的對齊方式,該對齊方式是相對於繪製文字時的畫筆的座標來說的,在本例中,我們繪製文字時畫筆在Canvas寬度的中間。在drawText()方法執行時,需要傳入一個x和y座標,假設該點為P點,P點表示我們從P點繪製文字。當對齊方式為Paint.Align.LEFT時,繪製的文字以P點為基準向左對齊,這是預設的對齊方式;當對齊方式為Paint.Align.CENTER時,繪製的文字以P點為基準居中對齊;當對齊方式為Paint.Align.RIGHT時,繪製的文字以P點為基準向右對齊。

  5. 通過呼叫paint.setUnderlineText(true)繪製帶有下劃線的文字。

  6. 通過呼叫paint.setFakeBoldText(true)繪製粗體文字。

  7. 通過rotate旋轉座標系,我們可以繪製傾斜文字。


drawPoint

Canvas中用drawPoint方法繪製點,程式碼如下所示:

private void drawPoint(Canvas canvas){
        int canvasWidth = canvas.getWidth();
        int canvasHeight = canvas.getHeight();
        int x = canvasWidth / 2;
        int deltaY = canvasHeight / 3;
        int y = deltaY / 2;
        paint.setColor(0xff8bc5ba);//設定顏色
        paint.setStrokeWidth(50 * density);//設定線寬,如果不設定線寬,無法繪製點

        //繪製Cap為BUTT的點
        paint.setStrokeCap(Paint.Cap.BUTT);
        canvas.drawPoint(x, y, paint);

        //繪製Cap為ROUND的點
        canvas.translate(0, deltaY);
        paint.setStrokeCap(Paint.Cap.ROUND);
        canvas.drawPoint(x, y, paint);

        //繪製Cap為SQUARE的點
        canvas.translate(0, deltaY);
        paint.setStrokeCap(Paint.Cap.SQUARE);
        canvas.drawPoint(x, y, paint);
    }

介面如下所示:
這裡寫圖片描述

下面對以上程式碼進行說明:

  1. Paint的setStrokeWidth方法可以控制所畫線的寬度,通過Paint的getStrokeWidth方法可以得到所畫線的寬度,預設情況下,線寬是0。其實strokeWidth不僅對畫線有影響,對畫點也有影響,由於預設的線寬是0,所以預設情況下呼叫drawPoint方法無法在Canvas上畫出點,為了讓大家清楚地看到所畫的點,我用Paint的setStrokeWidth設定了一個比較大的線寬,這樣我們看到的點也就比較大。

  2. Paint有個setStrokeCap方法可以設定所畫線段的時候兩個端點的形狀,即所畫線段的帽端的形狀,在下面講到drawLine方法時會詳細說明,其實setStrokeCap方法也會影響所畫點的形狀。Paint的setStrokeCap方法可以有三個取值:Paint.Cap.BUTT、Paint.Cap.ROUND和Paint.Cap.SQUARE。

  3. 預設情況下Paint的getStrokeCap的返回值是Paint.Cap.BUTT,預設畫出來的點就是一個正方形,上圖第一個點即是用BUTT作為帽端畫的。

  4. 我們可以呼叫setStrokeCap方法設定Paint的strokeCap為Paint.Cap.ROUND時,畫筆畫出來的點就是一個圓形,上圖第二個點即是用ROUND作為帽端畫的。

  5. 呼叫呼叫setStrokeCap方法設定Paint的strokeCap為Paint.Cap.SQUARE時,畫筆畫出來的電也是一個正方形,與用BUTT畫出來的效果在外觀上相同,上圖最後一個點即時用SQUARE作為帽端畫的。


drawLine

Canvas通過drawLine方法繪製一條線段,通過drawLines方法繪製多段線,使用程式碼如下所示:
這裡寫圖片描述

下面對以上程式碼進行說明:

  1. drawLine方法接收四個數值,即起點的x和y以及終點的x和y,繪製一條線段。

  2. drawLines方法接收一個float陣列pts,需要注意的是在用drawLines繪圖時,其每次從pts陣列中取出四個點繪製一條線段,然後再取出後面四個點繪製一條線段,所以要求pts的長度需要是4的倍數。假設我們有四個點,分別是p1、p2、p3、p4,我們依次將其座標放到pts陣列中,即pts = {p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y},那麼用drawLines繪製pts時,你會發現p1和p2之間畫了一條線段,p3和p4之間畫了一條線段,但是p2和p3之間沒有畫線段,這樣大家就應該能明白drawLines每次都需要從pts陣列中取出4個值繪製一條線段的意思了。

  3. 通過呼叫Paint的setStrokeWidth方法設定線的寬度。

  4. 上面在講drawPoint時提到了strokeCap對所繪製點的形狀的影響,通過drawLine繪製的線段也受其影響,體現在繪製的線段的兩個端點的形狀上。

    • Paint.Cap.BUTT
      當用BUTT作為帽端時,所繪製的線段恰好在起點終點位置處戛然而止,兩端是方形,上圖中第一條加粗的線段就是用BUTT作為帽端繪製的。

    • Paint.Cap.ROUND
      當用ROUND作為帽端時,所繪製的線段的兩端端點會超出起點和終點一點距離,並且兩端是圓形狀,上圖中第二條加粗的線段就是用ROUND作為帽端繪製的。

    • Paint.Cap.SQUARE
      當用SQUARE作為帽端時,所繪製的線段的兩端端點也會超出起點和終點一點距離,兩端點的形狀是方形,上圖中最後一條加粗的線段就是用SQUARE作為帽端繪製的。


drawRect

Canvas通過drawRect方法繪製矩形,使用程式碼如下所示:

private void drawRect(Canvas canvas){
        int canvasWidth = canvas.getWidth();
        int canvasHeight = canvas.getHeight();

        //預設畫筆的填充色是黑色
        int left1 = 10;
        int top1 = 10;
        int right1 = canvasWidth / 3;
        int bottom1 = canvasHeight /3;
        canvas.drawRect(left1, top1, right1, bottom1, paint);

        //修改畫筆顏色
        paint.setColor(0xff8bc5ba);//A:ff,R:8b,G:c5,B:ba
        int left2 = canvasWidth / 3 * 2;
        int top2 = 10;
        int right2 = canvasWidth - 10;
        int bottom2 = canvasHeight / 3;
        canvas.drawRect(left2, top2, right2, bottom2, paint);
    }

介面如下所示:
這裡寫圖片描述

其方法簽名是drawRect(float left, float top, float right, float bottom, Paint paint),left和right表示矩形的左邊和右邊分別到繪圖座標系y軸正半軸的距離,top和bottom表示矩形的上邊和下邊分別到繪圖座標系x軸正半軸的距離。


drawCircle

Canvas中用drawCircle方法繪製圓形,使用程式碼如下所示:

private void drawCircle(Canvas canvas){
        paint.setColor(0xff8bc5ba);//設定顏色
        paint.setStyle(Paint.Style.FILL);//預設繪圖為填充模式
        int canvasWidth = canvas.getWidth();
        int canvasHeight = canvas.getHeight();
        int halfCanvasWidth = canvasWidth / 2;
        int count = 3;
        int D = canvasHeight / (count + 1);
        int R = D / 2;

        //繪製圓
        canvas.translate(0, D / (count + 1));
        canvas.drawCircle(halfCanvasWidth, R, R, paint);

        //通過繪製兩個圓形成圓環
        //1. 首先繪製大圓
        canvas.translate(0, D + D / (count + 1));
        canvas.drawCircle(halfCanvasWidth, R, R, paint);
        //2. 然後繪製小圓,讓小圓覆蓋大圓,形成圓環效果
        int r = (int)(R * 0.75);
        paint.setColor(0xffffffff);//將畫筆設定為白色,畫小圓
        canvas.drawCircle(halfCanvasWidth, R, r, paint);

        //通過線條繪圖模式繪製圓環
        canvas.translate(0, D + D / (count + 1));
        paint.setColor(0xff8bc5ba);//設定顏色
        paint.setStyle(Paint.Style.STROKE);//繪圖為線條模式
        float strokeWidth = (float)(R * 0.25);
        paint.setStrokeWidth(strokeWidth);
        canvas.drawCircle(halfCanvasWidth, R, R, paint);
    }

介面如下所示:
這裡寫圖片描述

下面對以上程式碼進行說明:

  1. 其方法簽名是drawCircle (float cx, float cy, float radius, Paint paint),在使用時需要傳入圓心的座標以及半徑,當然還有畫筆Paint物件。

  2. 當我們在呼叫drawCircle、drawOval、drawArc、drawRect等方法時,我們既可以繪製對應圖形的填充面,也可以只繪製該圖形的輪廓線,控制的關鍵在於畫筆Paint中的style。Paint通過setStyle方法設定要繪製的型別,style有取三種值:Paint.Style.FILL、Paint.Style.STROKE和Paint.Style.FILL_AND_STROKE。

    • 當style為FILL時,繪製是填充面,FILL是Paint預設的style;
    • 當style為STROKE時,繪製的是圖形的輪廓線;
    • 當style為FILL_AND_STROKE時,同時繪製填充面和輪廓線,不過這種情況用的不多,因為填充面和輪廓線是用同一種顏色繪製的,區分不出輪廓線的效果。
  3. 在Paint的style是FILL時,我們通過drawCircle繪製出圓面,如上圖中的第一個圖形所示。

  4. 我們可以通過繪製兩個圓面的方式繪製出圓環的效果。首先將畫筆設定為某一顏色,且style設定為FILL狀態,通過drawCircle繪製一個大的圓面;然後將畫筆Paint的顏色改為白色或其他顏色,並減小半徑再次通過drawCircle繪製一個小圓,這樣就用小圓遮蓋了大圓的一部分,未遮蓋的部分便自然形成了圓環的效果,如上圖中的第二個圖形所示。

  5. 除了上述方法,我們還有一種辦法繪製圓環的效果。我們首先將畫筆Paint的style設定為STROKE模式,表示畫筆處於畫線條模式,而非填充模式。然後為了讓圓環比較明顯有一定的寬度,我們需要呼叫Paint的setStrokeWidth方法設定線寬。最後呼叫drawCircle方法繪製出寬度比較大的圓的輪廓線,也就形成了圓環效果,如上圖中的最後一個圖形所示。此處需要說明的是,當我們用STROKE模式畫圓時,輪廓線是以實際圓的邊界為分界線分別向內向外擴充1/2的線寬的距離,比如圓的半徑是100,線寬是20,那麼在STROKE模式下繪製出的圓環效果相當於半徑為110的大圓和半徑為90的小圓形成的效果,100 + 20 / 2 = 110, 100 - 20/2 = 90。


drawOval

Canvas中提供了drawOval方法繪製橢圓,其使用程式碼如下所示:

private void drawOval(Canvas canvas){
        int canvasWidth = canvas.getWidth();
        int canvasHeight = canvas.getHeight();
        float quarter = canvasHeight / 4;
        float left = 10 * density;
        float top = 0;
        float right = canvasWidth - left;
        float bottom= quarter;
        RectF rectF = new RectF(left, top, right, bottom);

        //繪製橢圓形輪廓線
        paint.setStyle(Paint.Style.STROKE);//設定畫筆為畫線條模式
        paint.setStrokeWidth(2 * density);//設定線寬
        paint.setColor(0xff8bc5ba);//設定線條顏色
        canvas.translate(0, quarter / 4);
        canvas.drawOval(rectF, paint);

        //繪製橢圓形填充面
        paint.setStyle(Paint.Style.FILL);//設定畫筆為填充模式
        canvas.translate(0, (quarter + quarter / 4));
        canvas.drawOval(rectF, paint);

        //畫兩個橢圓,形成輪廓線和填充色不同的效果
        canvas.translate(0, (quarter + quarter / 4));
        //1. 首先繪製填充色
        paint.setStyle(Paint.Style.FILL);//設定畫筆為填充模式
        canvas.drawOval(rectF, paint);//繪製橢圓形的填充效果
        //2. 將線條顏色設定為藍色,繪製輪廓線
        paint.setStyle(Paint.Style.STROKE);//設定畫筆為線條模式
        paint.setColor(0xff0000ff);//設定填充色為藍色
        canvas.drawOval(rectF, paint);//設定橢圓的輪廓線
    }

其介面如下所示:
這裡寫圖片描述

下面對以上程式碼進行說明:

  1. 其方法簽名是public void drawOval (RectF oval, Paint paint),RectF有四個欄位,分別是left、top、right、bottom,
    這四個值對應了橢圓的左、上、右、下四個點到相應座標軸的距離,具體來說,left和right表示橢圓的最左側的點和最右側的點到繪圖座標系的y軸的距離,top和bottom表示橢圓的最頂部的點和最底部的點到繪圖座標系的x軸的距離,這四個值就決定了橢圓的形狀,right與left的差值即為橢圓的長軸,bottom與top的差值即為橢圓的短軸,如下圖所示:
    這裡寫圖片描述

  2. 通過Paint的setStyle方法將畫筆的style設定成STROKE,即畫線條模式,這種情況下,用畫筆畫出來的是橢圓的輪廓線,而非填充面,如上圖中的第一個圖形所示。

  3. 當將畫筆Paint的style設定為FILL時,即填充模式,這種情況下,用畫筆畫出來的是橢圓的填充面,如上圖中的第二個圖形所示。

  4. 如果我們想繪製帶有其他顏色輪廓線的橢圓面,我們需要繪製兩個橢圓。首先以FILL模式畫一個橢圓的填充面,然後更改畫筆顏色,以STROKE模式畫橢圓的輪廓線,如上圖中的最後一個圖形所示。這樣從外觀上看,好像是橢圓面與橢圓的輪廓線顏色不同。


drawArc

Canvas中提供了drawArc方法用於繪製弧,這裡的弧指兩種:弧面和弧線,弧面即用弧圍成的填充面,弧線即為弧面的輪廓線。其使用程式碼如下所示:

private void drawArc(Canvas canvas){
        int canvasWidth = canvas.getWidth();
        int canvasHeight = canvas.getHeight();
        int count = 5;
        float ovalHeight = canvasHeight / (count + 1);
        float left = 10 * density;
        float top = 0;
        float right = canvasWidth - left;
        float bottom= ovalHeight;
        RectF rectF = new RectF(left, top, right, bottom);

        paint.setStrokeWidth(2 * density);//設定線寬
        paint.setColor(0xff8bc5ba);//設定顏色
        paint.setStyle(Paint.Style.FILL);//預設設定畫筆為填充模式

        //繪製用drawArc繪製完整的橢圓
        canvas.translate(0, ovalHeight / count);
        canvas.drawArc(rectF, 0, 360, true, paint);

        //繪製橢圓的四分之一,起點是鐘錶的3點位置,從3點繪製到6點的位置
        canvas.translate(0, (ovalHeight + ovalHeight / count));
        canvas.drawArc(rectF, 0, 90, true, paint);

        //繪製橢圓的四分之一,將useCenter設定為false
        canvas.translate(0, (ovalHeight + ovalHeight / count));
        canvas.drawArc(rectF, 0, 90, false, paint);

        //繪製橢圓的四分之一,只繪製輪廓線
        paint.setStyle(Paint.Style.STROKE);//設定畫筆為線條模式
        canvas.translate(0, (ovalHeight + ovalHeight / count));
        canvas.drawArc(rectF, 0, 90, true, paint);

        //繪製帶有輪廓線的橢圓的四分之一
        //1. 先繪製橢圓的填充部分
        paint.setStyle(Paint.Style.FILL);//設定畫筆為填充模式
        canvas.translate(0, (ovalHeight + ovalHeight / count));
        canvas.drawArc(rectF, 0, 90, true, paint);
        //2. 再繪製橢圓的輪廓線部分
        paint.setStyle(Paint.Style.STROKE);//設定畫筆為線條模式
        paint.setColor(0xff0000ff);//設定輪廓線條為藍色
        canvas.drawArc(rectF, 0, 90, true, paint);
    }

介面如下所示:
這裡寫圖片描述

下面對以上程式碼進行說明:

  1. 用drawArc畫的弧指的是橢圓弧,即橢圓的一部分。當然,如果橢圓的長軸和和短軸相等,這時候我們就可以用drawArc方法繪製圓弧。其方法簽名是:

    public void drawArc (RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)

    • oval是RecF型別的物件,其定義了橢圓的形狀。
    • startAngle指的是繪製的起始角度,鐘錶的3點位置對應著0度,如果傳入的startAngle小於0或者大於等於360,那麼用startAngle對360進行取模後作為起始繪製角度。
    • sweepAngle指的是從startAngle開始沿著鐘錶的順時針方向旋轉掃過的角度。如果sweepAngle大於等於360,那麼會繪製完整的橢圓弧。如果sweepAngle小於0,那麼會用sweepAngle對360進行取模後作為掃過的角度。
    • useCenter是個boolean值,如果為true,表示在繪製完弧之後,用橢圓的中心點連線弧上的起點和終點以閉合弧;如果值為false,表示在繪製完弧之後,弧的起點和終點直接連線,不經過橢圓的中心點。
  2. 在程式碼中我們一開始設定的Paint的style為FILL,即填充模式。通過上面的描述我們知道,drawOval方法可以看做是drawArc方法的一種特例。如果在drawArc方法中sweepAngle為360,無論startAngle為多少,drawArc都會繪製一個橢圓,如上圖中第一個圖形,我們用canvas.drawArc(rectF, 0, 360, true, paint)繪製了一個完整的橢圓,就像用drawOval畫出的那樣。

  3. 當我們呼叫方法canvas.drawArc(rectF, 0, 90, true, paint)時, 我們指定了起始角度為0,然後順時針繪製90度,即我們會繪製從3點到6點這90度的弧,如上圖中第二個圖形所示,我們繪製了一個橢圓的右下角的四分之一的弧面,需要注意的是我們此處設定的useCenter為true,所以弧上的起點(3點位置)和終點(6點位置)都和橢圓的中心連線了形成了。

  4. 當我們呼叫方法canvas.drawArc(rectF, 0, 90, false, paint)時,我們還是繪製橢圓右下角的弧面,不過這次我們將useCenter設定成了false,如上圖中的第三個圖形所示,弧上的起點(3點位置)和終點(6點位置)直接相連閉合了,而沒有經過橢圓的中心點。

  5. 上面介紹到的繪圖都是在畫筆Paint處於FILL狀態下繪製的。我們可以通過paint.setStyle(Paint.Style.STROKE)方法將畫筆的style改為STROKE,即繪製線條模式。然後我們再次執行canvas.drawArc(rectF, 0, 90, true, paint),初始角度為0,掃過90度的區域,useCenter為true,繪製的效果見上圖中第四個圖形,此時我們只繪製了橢圓的輪廓線。需要注意的,由於Paint預設的線寬為0,所以在繪製之前要確保掉用過Paint.setStrokeWidth()方法以設定畫筆的線寬。

  6. 如果我們想繪製出帶有其他顏色輪廓線的弧面時,該怎麼辦呢?我們可以分兩步完成:首先,將畫筆Paint的style設定為FILL模式,通過drawArc方法繪製出弧面。然後,將畫筆Paint的style設定為STROKE模式,並通過paint的setColor()方法改變畫筆的顏色,最後drawArc方法繪製出弧線。這樣我們就能繪製出帶有其他顏色輪廓線的弧面了,如上圖中最後一個圖形所示。


drawPath

Canvas通過drawPath方法可以繪製Path。那Path是什麼呢?Path致以過來是路徑的意思,在Android中,Path是一種線條的組合圖形,其可以由直線、二次曲線、三次曲線、橢圓的弧等組成。Path既可以畫線條,也可以畫填充面。其使用程式碼如下所示:

private void drawPath(Canvas canvas){
        int canvasWidth = canvas.getWidth();
        int deltaX = canvasWidth / 4;
        int deltaY = (int)(deltaX * 0.75);

        paint.setColor(0xff8bc5ba);//設定畫筆顏色
        paint.setStrokeWidth(4);//設定線寬

        /*--------------------------用Path畫填充面-----------------------------*/
        paint.setStyle(Paint.Style.FILL);//設定畫筆為填充模式
        Path path = new Path();
        //向Path中加入Arc
        RectF arcRecF = new RectF(0, 0, deltaX, deltaY);
        path.addArc(arcRecF, 0, 135);
        //向Path中加入Oval
        RectF ovalRecF = new RectF(deltaX, 0, deltaX * 2, deltaY);
        path.addOval(ovalRecF, Path.Direction.CCW);
        //向Path中新增Circle
        path.addCircle((float)(deltaX * 2.5), deltaY / 2, deltaY / 2, Path.Direction.CCW);
        //向Path中新增Rect
        RectF rectF = new RectF(deltaX * 3, 0, deltaX * 4, deltaY);
        path.addRect(rectF, Path.Direction.CCW);
        canvas.drawPath(path, paint);

        /*--------------------------用Path畫線--------------------------------*/
        paint.setStyle(Paint.Style.STROKE);//設定畫筆為線條模式
        canvas.translate(0, deltaY * 2);
        Path path2 = path;
        canvas.drawPath(path2, paint);

        /*-----------------使用lineTo、arcTo、quadTo、cubicTo畫線--------------*/
        paint.setStyle(Paint.Style.STROKE);//設定畫筆為線條模式
        canvas.translate(0, deltaY * 2);
        Path path3 = new Path();
        //用pointList記錄不同的path的各處的連線點
        List<Point> pointList = new ArrayList<Point>();
        //1. 第一部分,繪製線段
        path3.moveTo(0, 0);
        path3.lineTo(deltaX / 2, 0);//繪製線段
        pointList.add(new Point(0, 0));
        pointList.add(new Point(deltaX / 2, 0));
        //2. 第二部分,繪製橢圓右上角的四分之一的弧線
        RectF arcRecF1 = new RectF(0, 0, deltaX, deltaY);
        path3.arcTo(arcRecF1, 270, 90);//繪製圓弧
        pointList.add(new Point(deltaX, deltaY / 2));
        //3. 第三部分,繪製橢圓左下角的四分之一的弧線
        //注意,我們此處呼叫了path的moveTo方法,將畫筆的移動到我們下一處要繪製arc的起點上
        path3.moveTo(deltaX * 1.5f, deltaY);
        RectF arcRecF2 = new RectF(deltaX, 0, deltaX * 2, deltaY);
        path3.arcTo(arcRecF2, 90, 90);//繪製圓弧
        pointList.add(new Point((int)(deltaX * 1.5), deltaY));
        //4. 第四部分,繪製二階貝塞爾曲線
        //二階貝塞爾曲線的起點就是當前畫筆的位置,然後需要新增一個控制點,以及一個終點
        //再次通過呼叫path的moveTo方法,移動畫筆
        path3.moveTo(deltaX * 1.5f, deltaY);
        //繪製二階貝塞爾曲線
        path3.quadTo(deltaX * 2, 0, deltaX * 2.5f, deltaY / 2);
        pointList.add(new Point((int)(deltaX * 2.5), deltaY / 2));
        //5. 第五部分,繪製三階貝塞爾曲線,三階貝塞爾曲線的起點也是當前畫筆的位置
        //其需要兩個控制點,即比二階貝賽爾曲線多一個控制點,最後也需要一個終點
        //再次通過呼叫path的moveTo方法,移動畫筆
        path3.moveTo(deltaX * 2.5f, deltaY / 2);
        //繪製三階貝塞爾曲線
        path3.cubicTo(deltaX * 3, 0, deltaX * 3.5f, 0, deltaX * 4, deltaY);
        pointList.add(new Point(deltaX * 4, deltaY));

        //Path準備就緒後,真正將Path繪製到Canvas上
        canvas.drawPath(path3, paint);

        //最後繪製Path的連線點,方便我們大家對比觀察
        paint.setStrokeWidth(10);//將點的strokeWidth要設定的比畫path時要大
        paint.setStrokeCap(Paint.Cap.ROUND);//將點設定為圓點狀
        paint.setColor(0xff0000ff);//設定圓點為藍色
        for(Point p : pointList){
            //遍歷pointList,繪製連線點
            canvas.drawPoint(p.x, p.y, paint);
        }
    }

介面如下所示:
這裡寫圖片描述

下面對以上程式碼進行說明:

  1. Canvas的drawPath()方法接收Path和Paint兩個引數。當Paint的style是FILL時,我們可以用darwPath來畫填充面。Path類提供了addArc、addOval、addCircle、addRect等方法,可以通過這些方法可以向Path新增各種閉合圖形,Path甚至還提供了addPath方法讓我們將一個Path物件新增到另一個Path物件中作為其一部分。當我們通過Path的addXXX方法向Path中新增了各種圖形後,我們就可以呼叫canvas.drawPath(path, paint)繪製出Path了,如上圖中第一行中的幾個圖形所示。

  2. 我們可以通過呼叫Paint的setStyle()方法將畫筆Paint設定為STROKE,即線條模式, 然後我們再次執行canvas.darwPath()方法繪製同一個Path物件,我們這次繪製的就只是Path的輪廓線了,如上圖中第二行中的幾個圖形所示。

  3. Path物件還有很多xxTo方法,比如lineTo、arcTo、quadTo、cubicTo等,通過這些方法,我們可以方便的從畫筆位置繪製到指定座標的連續線條,如上圖中最後一行的幾個線狀圖形所示。我們用了lineTo、arcTo、quadTo、cubicTo這四種方法畫了五段線條,下面會解釋,並且單獨通過呼叫drawPoint畫出了每段線條的兩個端點,方便大家觀察。

    • moveTo方法用於設定下一個線條的起始點,可以認為是移動了畫筆,但說移動畫筆不嚴格,後面會解釋,此處大家暫且這麼理解。

    • lineTo的方法簽名是public void lineTo (float x, float y),Path的lineTo方法會從當前畫筆的位置到我們指定的座標構建一條線段,然後將其新增到Path物件中,如上圖中最後一行圖形中的第一條線段所示。

    • arcTo的方法簽名是public void arcTo (RectF oval, float startAngle, float sweepAngle),oval、startAngle與sweepAngle的引數與之前提到的darwArc方法對應的形參意義相同,在此不再贅述。Path的arcTo方法會構建一條弧線並新增到Path物件中,如上圖中最後一行圖形中的第二條和第三條線狀圖形所示,這兩條弧線都是通過Path的arcTo方法新增的。

    • quadTo是用來畫二階貝塞爾曲線的,即拋物線,其方法簽名是public void quadTo (float x1, float y1, float x2, float y2),如果對貝塞爾曲線的相關概念不瞭解,推薦大家讀一下博文《貝塞爾曲線初探》 。下面借用該博文中的一張圖說一下二階貝塞爾曲線:
      這裡寫圖片描述
      二階貝塞爾曲線的繪製一共需要三個點,一個起點,一個終點,還要有一箇中間的控制點。我們畫筆的位置就相當於上圖中P0的位置,quadTo中的前兩個引數x1和y1指定了控制點P1的座標,後面兩個引數x2和y2指定了終點P2的座標。上圖中最後一行的第四個線狀圖形就是用quadTo繪製的二階貝塞爾曲線。

    • cubicTo跟quadTo類似,不過是用來畫三階貝塞爾曲線的,其方法簽名是public void cubicTo (float x1, float y1, float x2, float y2, float x3, float y3)。我們還是借用一下上述博文《貝塞爾曲線初探》中的另一張圖片來解釋一下三階貝塞爾曲線:
      這裡寫圖片描述
      三階貝塞爾曲線的繪製需要四個點,一個起點,一個終點,以及兩個中間的控制點,也就是說它比二階貝塞爾曲線要多一個控制點。我們畫筆的位置就相當於上圖中P0的位置,cubicTo中的前兩個引數x1和y1指定了第一個控制點P1的座標,引數x2和y2指定了第二個控制點P2的座標,最後兩個引數x3和y3指定了終點P3的座標。上圖中最後一行的最後一個線狀圖形就是用cubicTo繪製的三階貝塞爾曲線。

  4. 上面提到Path的moveTo方法移動了畫筆的位置,這樣說不準確,因為Path和Paint沒有任何關係,準確的說法是移動了Path的當前點,當我們呼叫lineTo、arcTo、quadTo、cubicTo等方法時,首先要從當前點開始繪製。對於lineTo、quadTo、cubicTo這三個方法來說,Path的當前點作為了這三個方法繪製的線條中的起始點,但是對於arcTo方法來說卻不同。當我們呼叫arcTo方法時,首先會從Path的當前點畫一條直線到我們所畫弧的起始點,所以在使用Path的arcTo方法前要注意通過呼叫Path的moveTo方法使當前點與所畫弧的起點重合,否則有可能你就會看到多了一條當前點到弧的起點的線段。moveTo可以移動當前點,當呼叫了lineTo、arcTo、quadTo、cubicTo等方法時,當前點也會移動,當前點就變成了所繪製的線條的最後一個點。

  5. 上面提到了moveTo、lineTo、arcTo、quadTo、cubicTo的方法中傳入的座標都是繪圖座標系中的座標,即繪圖座標系中的絕對座標。其實我們可以用相對座標呼叫這些型別功能的方法。Path因此提供了對應的rMoveTo、rLineTo、rQuadTo、rCubicTo方法,其形參列表與對應的方法相同,只不過裡面傳入的座標不是相對於當前點的相對座標,即傳入的座標是相對於當前點的偏移值。

  6. lineTo、arcTo、quadTo、cubicTo等方法只是向Path中新增相應的線條,只有執行了canvas.drawPath(path3, paint)方法時,我們才能將Path繪製到Canvas上。


drawBitmap

Canvas中提供了drawBitmap方法用於繪製Bitmap,其使用程式碼如下所示:

private void drawBitmap(Canvas canvas){
        //如果bitmap不存在,那麼就不執行下面的繪製程式碼
        if(bitmap == null){
            return;
        }

        //直接完全繪製Bitmap
        canvas.drawBitmap(bitmap, 0, 0, paint);

        //繪製Bitmap的一部分,並對其拉伸
        //srcRect定義了要繪製Bitmap的哪一部分
        Rect srcRect = new Rect();
        srcRect.left = 0;
        srcRect.right = bitmap.getWidth();
        srcRect.top = 0;
        srcRect.bottom = (int)(0.33 * bitmap.getHeight());
        float radio = (float)(srcRect.bottom - srcRect.top)  / bitmap.getWidth();
        //dstRecF定義了要將繪製的Bitmap拉伸到哪裡
        RectF dstRecF = new RectF();
        dstRecF.left = 0;
        dstRecF.right = canvas.getWidth();
        dstRecF.top = bitmap.getHeight();
        float dstHeight = (dstRecF.right - dstRecF.left) * radio;
        dstRecF.bottom = dstRecF.top + dstHeight;
        canvas.drawBitmap(bitmap, srcRect, dstRecF, paint);
    }

介面如下所示:
這裡寫圖片描述

我在res/drawable目錄下放置了一張android的圖片,下面對上面的程式碼進行說明:

  1. Canvas的drawBitmap有多個過載方法,最簡單的方法簽名是:

    public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)

    該方法除了傳入bitmap物件外,還需要傳入left和top,left和top組成了一個座標,決定了在Canvas中從哪個地方繪製Bitmap。在我們的程式碼中,left和top都設定為0,所以我們就在Canvas的左上角繪製了bitmap。

  2. drawBitmap還有一個比較實用的方法,其方法簽名是:

    public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)

    該方法有兩個功能:1.只繪製原有bitmap物件的一部分,2.還可以將要繪製的bitmap縮放到指定的區域。

    • 只繪製原有bitmap物件的一部分
      我們知道Bitmap是一個矩形,其是有寬度和高度的,也就說以bitmap物件本身作為座標系(原點在bitmap左上角),我們可以構建一個Rect物件,如果滿足left為0,top為0,right為bitmap的寬度,bottom為bitmap的高度,那麼就說名我們要繪製整個Bitmap。但是有時候我們只想繪製Bitmap的一部分,例如我們上面的圖中所示,我們想只繪製Android影象的頭部區域怎麼辦呢?辦法是我們構建一個Rect物件,定義我們要繪製Bitmap的哪些部位。
      比如我們通過程式碼srcRect.bottom = (int)(0.33 * bitmap.getHeight())指定了我們只繪製bitmap物件頭部1/3的位置,即Android影象的頭部,這樣我們用該指定的srcRect繪製bitmap時只繪製了其頭部位置。需要特別注意的是,srcRect中left、top、right、bottom的值都是以Bitmap本身的區域性座標系為基礎的。

    • 將要繪製的bitmap縮放到指定的區域
      有時候我們需要將原有的bitmap進行放大或縮小,如上圖所示,我們將原有圖片放大了,這怎麼做呢?我們需要指定RectF型別的引數dstRectF,以便告訴Android將srcRect中定義的bitmap縮放到哪裡。即Android會將srcRect中定義的bitmap縮放到dstRectF區域範圍內。需要注意的是,此處的dstRecF是繪圖座標系中的座標,不是Bitmap本身的區域性座標系。我們在程式碼中保證了dstRecF的長寬比與srcRect中的長寬比相同,這樣不會導致圖片長寬比例變形,效果見上圖中的第二個放大的圖形。

  3. 此處有一點需要說明,在繪圖結束退出Activity的時候,我們需要呼叫bitmap的recyle()方法,防止記憶體洩露,本程式在onDestroy()方法中執行了該方法。


總結

  1. Canvas通過drawXXX等一些列的繪圖方法決定了要繪製的圖形的外形,我們可以通過自由組合繪製出我們想要的效果。drawXXX方法中的座標都是基於當前繪圖座標系的座標,而非Canvas座標系,預設情況下二者重合。通過呼叫translate、rotate、scale等方法可以對繪圖座標系進行變換。

  2. 畫筆Paint控制著所繪製的圖形的具體外觀,Paint預設的字型大小為12px,在繪製文字時我們往往要考慮密度density設定合適的字型大小。畫筆的預設顏色為黑色,預設的style為FILL,預設的cap為BUTT,預設的線寬為0,參見下圖所示:
    這裡寫圖片描述

  3. 在畫面狀的圖形時,如果Paint的style是FILL,那麼繪製的就是填充面;如果是STROKE,那麼繪製的就是輪廓線。

程式碼為Android Studio工程,已上傳到CSDN,點此下載

感謝大家耐心讀完,希望文字對大家瞭解Canvas中的繪圖基礎有所幫助!

相關閱讀:
[GitHub開源]Android自定義View實現微信打飛機遊戲

相關文章