HenCoder Android 開發進階: 自定義 View 1-1 繪製基礎

從今天開始,HenCoder 就正式開講知識技能了。按照個人計劃,第一季是 UI,UI 一共分爲三部分:繪製、佈局和觸摸反饋。本期是繪製部分的第一期。繪製大概會用 5~6 期的時間講完,整個 UI 的繪製、佈局和觸摸反饋三部分大概會用十來期。更新頻率大約爲每週一更(不承諾喲)。html

若是你不知道 HenCoder 是什麼,能夠先看這裏:java

HenCoder:給高級 Android 工程師的進階手冊android

自定義繪製概述

二話不說,我反手就是一個視頻:(視頻掛了,先直接點到優酷去看吧:優酷連接git

首先總結一下視頻中的關鍵點:github

  • 自定義繪製的方式是重寫繪製方法,其中最經常使用的是 onDraw()
  • 繪製的關鍵是 Canvas 的使用
    • Canvas 的繪製類方法: drawXXX() (關鍵參數:Paint)
    • Canvas 的輔助類方法:範圍裁切和幾何變換
  • 可使用不一樣的繪製方法來控制遮蓋關係

概念已經在視頻裏所有講出來了,知識點並很少,但你可能也看出來了,我講得並不細。這是由於知識點雖然很少,但細節仍是不少的,僅僅靠一節分享不可能講完。我按照順序把這些知識分紅了 4 個級別,拆成幾節來說,你按照這 4 個級別的順序學習下來,就可以平滑地逐步進階。算法

自定義繪製知識的四個級別

  1. Canvas 的 drawXXX() 系列方法及 Paint 最多見的使用canvas

    Canvas.drawXXX() 是自定義繪製最基本的操做。掌握了這些方法,你才知道怎麼繪製內容,例如怎麼畫圓、怎麼畫方、怎麼畫圖像和文字。組合繪製這些內容,再配合上 Paint 的一些常見方法來對繪製內容的顏色和風格進行簡單的配置,就可以應付大部分的繪製需求了。數組

    今天這篇分享我要講的就是這些內容。也就是說,你在看完這篇文章並作完練習以後,上面這幾幅圖你就會繪製出來了。從今之後,你也不多再須要僞裝一本正經地對設計師說「不行這個圖技術上實現不了」,也不用心驚膽戰得等待設計師的那句「那 iOS 怎麼能夠」了。ide

  2. Paint 的徹底攻略佈局

    Paint 能夠作的事,不僅是設置顏色,也不僅是我在視頻裏講的實心空心、線條粗細、有沒有陰影,它能夠作的風格設置真的是很是多、很是細。例如:

    拐角要什麼形狀?

    開不開雙線性過濾?

    加不加特效?

能夠調節的很是多,我就不一一列舉了。當你掌握到這個級別,就真的不會有什麼東西會是 iOS 能作到但你作不到的了。就算設計師再設計出了很難作的東西,作不出來的也再也不會是大家 Android 組了。

  1. Canvas 對繪製的輔助——範圍裁切和幾何變換。

    範圍裁切:

    幾何變換:

    大多數時候,它們並不會被用到,但一旦用到,一般都是很炫酷的效果。範圍裁切和幾何變換都是用於輔助的,它們自己並不酷,讓它們變酷的是設計師們的想象力與創造力。而你要作的,是把他們的想象力與創造力變成現實。

  2. 使用不一樣的繪製方法來控制繪製順序

    控制繪製順序解決的並非「作不到」的問題,而是性能問題。一樣的一種效果,你不用繪製順序的控制每每也能作到,但須要用多個 View 甚至是多層 View 才能拼湊出來,所以代價是 UI 的性能;而使用繪製順序的控制的話,一個 View 就所有搞定了。

自定義繪製的知識,大概就分爲上面這四個級別。在你把這四個級別依次掌握了以後,你就是一個自定義繪製的高手了。它們具體的細節,我將分紅幾篇來說。今天這篇就是第一篇: Canvas.drawXXX() 系列方法及 Paint 最基本的使用。我要正式開始嘍?

一切的開始:onDraw()

自定義繪製的上手很是容易:提早建立好 Paint 對象,重寫 onDraw(),把繪製代碼寫在 onDraw() 裏面,就是自定義繪製最基本的實現。大概就像這樣:

Paint paint = new Paint();

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

    // 繪製一個圓
    canvas.drawCircle(300, 300, 200, paint);
}複製代碼

就這麼簡單。因此關於 onDraw() 其實沒什麼好說的,一個很普通的方法重寫,惟一須要注意的是別漏寫了 super.onDraw()

Canvas.drawXXX() 和 Paint 基礎

drawXXX() 系列方法和 Paint 的基礎掌握了,就可以應付簡單的繪製需求。它們主要包括:

  1. Canvas 類下的全部 draw- 打頭的方法,例如 drawCircle() drawBitmap()
  2. Paint 類的幾個最經常使用的方法。具體是:
    • Paint.setStyle(Style style) 設置繪製模式
    • Paint.setColor(int color) 設置顏色
    • Paint.setStrokeWidth(float width) 設置線條寬度
    • Paint.setTextSize(float textSize) 設置文字大小
    • Paint.setAntiAlias(boolean aa) 設置抗鋸齒開關

對於比較習慣於自學的人(我就是這樣的人),你看到這裏就已經能夠去 Google 的官方文檔裏,打開 CanvasPaint 的頁面,把上面的這兩類方法學習一下,而後今天的內容就算結束了。固然,這篇文章也能夠關掉了。

下面的內容就是展開講解上面的這兩類方法。

Canvas.drawColor(@ColorInt int color) 顏色填充

這是最基本的 drawXXX() 方法:在整個繪製區域統一塗上指定的顏色。

例如 drawColor(Color.BLACK) 會把整個區域染成純黑色,覆蓋掉原有內容; drawColor(Color.parse("#88880000") 會在原有的繪製效果上加一層半透明的紅色遮罩。

drawColor(Color.BLACK);  // 純黑複製代碼

drawColor(Color.parse("#88880000"); // 半透明紅色複製代碼

相似的方法還有 drawRGB(int r, int g, int b)drawARGB(int a, int r, int g, int b) ,它們和 drawColor(color) 只是使用方式不一樣,做用都是同樣的。

canvas.drawRGB(100, 200, 100);
canvas.drawARGB(100, 100, 200, 100);複製代碼

這類顏色填充方法通常用於在繪製以前設置底色,或者在繪製以後爲界面設置半透明蒙版。

drawCircle(float centerX, float centerY, float radius, Paint paint) 畫圓

前兩個參數 centerX centerY 是圓心的座標,第三個參數 radius 是圓的半徑,單位都是像素,它們共同構成了這個圓的基本信息(即用這幾個信息能夠構建出一個肯定的圓);第四個參數 paint 我在視頻裏面已經說過了,它提供基本信息以外的全部風格信息,例如顏色、線條粗細、陰影等。

canvas.drawCircle(300, 300, 200, paint);複製代碼

那位說:「你等會兒!先別日後講,你剛纔說圓心的座標,我想問座標系在哪兒呢?沒座標系你跟我聊什麼座標啊。」

我想說:問得好(強行插入劇情)。在 Android 裏,每一個 View 都有一個本身的座標系,彼此之間是不影響的。這個座標系的原點是 View 左上角的那個點;水平方向是 x 軸,右正左負;豎直方向是 y 軸,下正上負(注意,是下正上負,不是上正下負,和上學時候學的座標系方向不同)。也就是下面這個樣子。

因此一個 View 的座標 (x, y) 處,指的就是相對它的左上角那個點的水平方向 x 像素、豎直方向 y 像素的點。例如,(300, 300) 指的就是左上角的點向右 300 、向下 300 的位置; (100, -50) 指的就是左上角的點向右 100 、向上 50 的位置。

也就是說, canvas.drawCircle(300, 300, 200, paint) 這行代碼繪製出的圓,在 View 中的位置和尺寸應該是這樣的:

圓心座標和半徑,這些都是圓的基本信息,也是它的獨有信息。什麼叫獨有信息?就是隻有它有,別人沒有的信息。你畫圓有圓心座標和半徑,畫方有嗎?畫橢圓有嗎?這就叫獨有信息。獨有信息都是直接做爲參數寫進 drawXXX() 方法裏的(好比 drawCircle(centerX, centerY, radius, paint) 的前三個參數)。

而除此以外,其餘的都是公有信息。好比圖形的顏色、空心實心這些,你無論是畫圓仍是畫方都有可能用到的,這些信息則是統一放在 paint 參數裏的。

插播一: Paint.setColor(int color)

例如,你要畫一個紅色的圓,並非寫成 canvas.drawCircle(300, 300, 200, RED, paint) 這樣,而是像下面這樣:

paint.setColor(Color.RED); // 設置爲紅色
canvas.drawCircle(300, 300, 200, paint);複製代碼

Paint.setColor(int color)Paint 最經常使用的方法之一,用來設置繪製內容的顏色。你不止能夠用它畫紅色的圓,也能夠用它來畫紅色的矩形、紅色的五角星、紅色的文字。

插播二: Paint.setStyle(Paint.Style style)

而若是你想畫的不是實心圓,而是空心圓(或者叫環形),也可使用 paint.setStyle(Paint.Style.STROKE) 來把繪製模式改成畫線模式。

paint.setStyle(Paint.Style.STROKE); // Style 修改成畫線模式
canvas.drawCircle(300, 300, 200, paint);複製代碼

setStyle(Style style) 這個方法設置的是繪製的 StyleStyle 具體來講有三種: FILL, STROKEFILL_AND_STROKEFILL 是填充模式,STROKE 是畫線模式(即勾邊模式),FILL_AND_STROKE 是兩種模式一併使用:既畫線又填充。它的默認值是 FILL,填充模式。

插播三: Paint.setStrokeWidth(float width)

STROKEFILL_AND_STROKE 下,還可使用 paint.setStrokeWidth(float width) 來設置線條的寬度:

paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(20); // 線條寬度爲 20 像素
canvas.drawCircle(300, 300, 200, paint);複製代碼

插播四: 抗鋸齒

在繪製的時候,每每須要開啓抗鋸齒來讓圖形和文字的邊緣更加平滑。開啓抗鋸齒很簡單,只要在 new Paint() 的時候加上一個 ANTI_ALIAS_FLAG 參數就行:

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);複製代碼

另外,你也可使用 Paint.setAntiAlias(boolean aa) 來動態開關抗鋸齒。

抗鋸齒的效果以下:

能夠看出,沒有開啓抗鋸齒的時候,圖形會有毛片現象,啊不,毛邊現象。因此必定記得要打開抗鋸齒喲!

能夠跳過的冷知識

好奇的人可能會問:抗鋸齒既然這麼有用,爲何不默認開啓,或者乾脆把這個開關取消,自動讓全部繪製都開啓抗鋸齒?

短答案:由於抗鋸齒並不必定適合全部場景。

長答案:所謂的毛邊或者鋸齒,發生的緣由並非不少人所想象的「繪製太粗糙」「像素計算能力不足」;一樣,抗鋸齒的原理也並非選擇了更精細的算法來算出了更平滑的圖形邊緣。
實質上,鋸齒現象的發生,只是因爲圖形分辨率太低,致使人眼察覺出了畫面中的像素顆粒而已。換句話說,就算不開啓抗鋸齒,圖形的邊緣也已是最完美的了,而並非一個粗略計算的粗糙版本。
那麼,爲何抗鋸齒開啓以後的圖形邊緣會更加平滑呢?由於抗鋸齒的原理是:修改圖形邊緣處的像素顏色,從而讓圖形在肉眼看來具備更加平滑的感受。一圖勝千言,上圖:


上面這個是把前面那兩個圓放大後的局部效果。看到沒有?未開啓抗鋸齒的圓,全部像素都是一樣的黑色,而開啓了抗鋸齒的圓,邊緣的顏色被略微改變了。這種改變可讓人眼有邊緣平滑的感受,但從某種角度講,它也形成了圖形的顏色失真。
因此,抗鋸齒好很差?好,大多數狀況下它都應該是開啓的;但在極少數的某些時候,你還真的須要把它關閉。「某些時候」是何時?到你用到的時候天然就知道了。

除了圓,Canvas 還能夠繪製一些別的簡單圖形。它們的使用方法和 drawCircle() 大同小異,我就只對它們的 API 作簡單的介紹,再也不作詳細的講解。

drawRect(float left, float top, float right, float bottom, Paint paint) 畫矩形

left, top, right, bottom 是矩形四條邊的座標。

paint.setStyle(Style.FILL);
canvas.drawRect(100, 100, 500, 500, paint);

paint.setStyle(Style.STROKE);
canvas.drawRect(700, 100, 1100, 500, paint);複製代碼

另外,它還有兩個重載方法 drawRect(RectF rect, Paint paint)drawRect(Rect rect, Paint paint) ,讓你能夠直接填寫 RectFRect 對象來繪製矩形。

drawPoint(float x, float y, Paint paint) 畫點

xy 是點的座標。點的大小能夠經過 paint.setStrokeWidth(width) 來設置;點的形狀能夠經過 paint.setStrokeCap(cap) 來設置:ROUND 畫出來是圓形的點,SQUAREBUTT 畫出來是方形的點。(點還有形狀?是的,反正 Google 是這麼說的,你要問問 Google 去,我也很懵逼。)

注:Paint.setStrokeCap(cap) 能夠設置點的形狀,但這個方法並非專門用來設置點的形狀的,而是一個設置線條端點形狀的方法。端點有圓頭 (ROUND)、平頭 (BUTT) 和方頭 (SQUARE) 三種,具體會在下一節裏面講。

paint.setStrokeWidth(20);
paint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawPoint(50, 50, paint);複製代碼

paint.setStrokeWidth(20);
paint.setStrokeCap(Paint.Cap.SQUARE;
canvas.drawPoint(50, 50, paint);複製代碼

好像有點像 FILL 模式下的 drawCircle()drawRect() ?事實上確實是這樣的,它們和 drawPoint() 的繪製效果沒有區別。各位在使用的時候按我的習慣和實際場景來吧,哪一個方便和順手用哪一個。

drawPoints(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint) 畫點(批量)

一樣是畫點,它和 drawPoint() 的區別是能夠畫多個點。pts 這個數組是點的座標,每兩個成一對;offset 表示跳過數組的前幾個數再開始記座標;count 表示一共要繪製幾個點。說這麼多你可能越讀越暈,你仍是本身試試吧,這是個看着複雜用着簡單的方法。

float[] points = {0, 0, 50, 50, 50, 100, 100, 50, 100, 100, 150, 50, 150, 100};
// 繪製四個點:(50, 50) (50, 100) (100, 50) (100, 100)
canvas.drawPoints(points, 2 /* 跳過兩個數,即前兩個 0 */,
          8 /* 一共繪製 8 個數(4 個點)*/, paint);複製代碼

drawOval(float left, float top, float right, float bottom, Paint paint) 畫橢圓

只能繪製橫着的或者豎着的橢圓,不能繪製斜的(斜的卻是也能夠,但不是直接使用 drawOval(),而是配合幾何變換,後面會講到)。left, top, right, bottom 是這個橢圓的左、上、右、下四個邊界點的座標。

paint.setStyle(Style.FILL);
canvas.drawOval(50, 50, 350, 200, paint);

paint.setStyle(Style.STROKE);
canvas.drawOval(400, 50, 700, 200, paint);複製代碼

另外,它還有一個重載方法 drawOval(RectF rect, Paint paint),讓你能夠直接填寫 RectF 來繪製橢圓。

drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 畫線

startX, startY, stopX, stopY 分別是線的起點和終點座標。

canvas.drawLine(200, 200, 800, 500, paint);複製代碼

因爲直線不是封閉圖形,因此 setStyle(style) 對直線沒有影響。

drawLines(float[] pts, int offset, int count, Paint paint) / drawLines(float[] pts, Paint paint) 畫線(批量)

drawLines()drawLine() 的複數版。

float[] points = {20, 20, 120, 20, 70, 20, 70, 120, 20, 120, 120, 120, 150, 20, 250, 20, 150, 20, 150, 120, 250, 20, 250, 120, 150, 120, 250, 120};
canvas.drawLines(points, paint);複製代碼

咦,不當心打出兩個漢字。——是漢字吧?

drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint) 畫圓角矩形

left, top, right, bottom 是四條邊的座標,rxry 是圓角的橫向半徑和縱向半徑。

canvas.drawRoundRect(100, 100, 500, 300, 50, 50, paint);複製代碼

另外,它還有一個重載方法 drawRoundRect(RectF rect, float rx, float ry, Paint paint),讓你能夠直接填寫 RectF 來繪製圓角矩形。

drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 繪製弧形或扇形

drawArc() 是使用一個橢圓來描述弧形的。left, top, right, bottom 描述的是這個弧形所在的橢圓;startAngle 是弧形的起始角度(x 軸的正向,即正右的方向,是 0 度的位置;順時針爲正角度,逆時針爲負角度),sweepAngle 是弧形劃過的角度;useCenter 表示是否鏈接到圓心,若是不鏈接到圓心,就是弧形,若是鏈接到圓心,就是扇形。

paint.setStyle(Paint.Style.FILL); // 填充模式
canvas.drawArc(200, 100, 800, 500, -110, 100, true, paint); // 繪製扇形
canvas.drawArc(200, 100, 800, 500, 20, 140, false, paint); // 繪製弧形
paint.setStyle(Paint.Style.STROKE); // 畫線模式
canvas.drawArc(200, 100, 800, 500, 180, 60, false, paint); // 繪製不封口的弧形複製代碼

到此爲止,以上就是 Canvas 全部的簡單圖形的繪製。除了簡單圖形的繪製, Canvas 還可使用 drawPath(Path path) 來繪製自定義圖形。

drawPath(Path path, Paint paint) 畫自定義圖形

這個方法有點複雜,須要展開說一下。

前面的這些方法,都是繪製某個給定的圖形,而 drawPath() 能夠繪製自定義圖形。當你要繪製的圖形比較特殊,使用前面的那些方法作不到的時候,就可使用 drawPath() 來繪製。

drawPath(path) 這個方法是經過描述路徑的方式來繪製圖形的,它的 path 參數就是用來描述圖形路徑的對象。path 的類型是 Path ,使用方法大概像下面這樣:

public class PathView extends View {

    Paint paint = new Paint();
    Path path = new Path(); // 初始化 Path 對象

    ......

    {
      // 使用 path 對圖形進行描述(這段描述代碼沒必要看懂)
      path.addArc(200, 200, 400, 400, -225, 225);
      path.arcTo(400, 200, 600, 400, -180, 225, false);
      path.lineTo(400, 542);
    }

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

      canvas.drawPath(path, paint); // 繪製出 path 描述的圖形(心形),大功告成
    }
}複製代碼

Path 能夠描述直線、二次曲線、三次曲線、圓、橢圓、弧形、矩形、圓角矩形。把這些圖形結合起來,就能夠描述出不少複雜的圖形。下面我就說一下具體的怎麼把這些圖形描述出來。

Path 有兩類方法,一類是直接描述路徑的,另外一類是輔助的設置或計算。

Path 方法第一類:直接描述路徑。

這一類方法還能夠細分爲兩組:添加子圖形和畫線(直線或曲線)

第一組: addXxx() ——添加子圖形
addCircle(float x, float y, float radius, Direction dir) 添加圓

x, y, radius 這三個參數是圓的基本信息,最後一個參數 dir 是畫圓的路徑的方向。

路徑方向有兩種:順時針 (CW clockwise) 和逆時針 (CCW counter-clockwise) 。對於普通狀況,這個參數填 CW 仍是填 CCW 沒有影響。它只是在須要填充圖形 (Paint.StyleFILLFILL_AND_STROKE) ,而且圖形出現自相交時,用於判斷填充範圍的。好比下面這個圖形:

是應該填充成這樣呢:

仍是應該填充成這樣呢:

想用哪一種方式來填充,均可以由你來決定。具體怎麼作,下面在講 Path.setFillType() 的時候我會詳細介紹,而在這裏你能夠先忽略 dir 這個參數。

在用 addCircle()Path 中新增一個圓以後,調用 canvas.drawPath(path, paint) ,就能畫一個圓出來。就像這樣:

path.addCircle(300, 300, 200, Path.Direction.CW);

...

canvas.drawPath(path, paint);複製代碼

能夠看出,path.AddCircle(x, y, radius, dir) + canvas.drawPath(path, paint) 這種寫法,和直接使用 canvas.drawCircle(x, y, radius, paint) 的效果是同樣的,區別只是它的寫法更復雜。因此若是隻畫一個圓,不必用 Path,直接用 drawCircle() 就好了。drawPath() 通常是在繪製組合圖形時纔會用到的。

其餘的 Path.add-() 方法和這相似,例如:

addOval(float left, float top, float right, float bottom, Direction dir) / addOval(RectF oval, Direction dir) 添加橢圓
addRect(float left, float top, float right, float bottom, Direction dir) / addRect(RectF rect, Direction dir) 添加矩形
addRoundRect(RectF rect, float rx, float ry, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir) / addRoundRect(RectF rect, float[] radii, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float[] radii, Direction dir) 添加圓角矩形
addPath(Path path) 添加另外一個 Path

上面這幾個方法和 addCircle() 的使用都差很少,再也不作過多介紹。

第二組:xxxTo() ——畫線(直線或曲線)

這一組和第一組 addXxx() 方法的區別在於,第一組是添加的完整封閉圖形(除了 addPath() ),而這一組添加的只是一條線。

lineTo(float x, float y) / rLineTo(float x, float y) 畫直線

當前位置向目標位置畫一條直線, xy 是目標位置的座標。這兩個方法的區別是,lineTo(x, y) 的參數是絕對座標,而 rLineTo(x, y) 的參數是相對當前位置的相對座標 (前綴 r 指的就是 relatively 「相對地」)。

當前位置:所謂當前位置,即最後一次調用畫 Path 的方法的終點位置。初始值爲原點 (0, 0)。

paint.setStyle(Style.STROKE);
path.lineTo(100, 100); // 由當前位置 (0, 0) 向 (100, 100) 畫一條直線
path.rLineTo(100, 0); // 由當前位置 (100, 100) 向正右方 100 像素的位置畫一條直線複製代碼

quadTo(float x1, float y1, float x2, float y2) / rQuadTo(float dx1, float dy1, float dx2, float dy2) 畫二次貝塞爾曲線

這條二次貝塞爾曲線的起點就是當前位置,而參數中的 x1, y1x2, y2 則分別是控制點和終點的座標。和 rLineTo(x, y) 同理,rQuadTo(dx1, dy1, dx2, dy2) 的參數也是相對座標

貝塞爾曲線:貝塞爾曲線是幾何上的一種曲線。它經過起點、控制點和終點來描述一條曲線,主要用於計算機圖形學。概念老是說着容易聽着難,總之使用它能夠繪製不少圓潤又好看的圖形,但要把它熟練掌握、靈活使用倒是不容易的。不過還好的是,通常狀況下,貝塞爾曲線並無什麼用處,只在少數場景下繪製一些特殊圖形的時候纔會用到,因此若是你還沒掌握自定義繪製,能夠先把貝塞爾曲線放一放,稍後再學也徹底沒問題。至於怎麼學,貝塞爾曲線的知識網上一搜一大把,我這裏就不講了。

cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) / rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 畫三次貝塞爾曲線

和上面這個 quadTo() rQuadTo() 的二次貝塞爾曲線同理,cubicTo()rCubicTo() 是三次貝塞爾曲線,再也不解釋。

moveTo(float x, float y) / rMoveTo(float x, float y) 移動到目標位置

不管是直線仍是貝塞爾曲線,都是以當前位置做爲起點,而不能指定起點。但你能夠經過 moveTo(x, y)rMoveTo() 來改變當前位置,從而間接地設置這些方法的起點。

paint.setStyle(Style.STROKE);
path.lineTo(100, 100); // 畫斜線
path.moveTo(200, 100); // 我移~~
path.lineTo(200, 0); // 畫豎線複製代碼

moveTo(x, y) 雖然不添加圖形,但它會設置圖形的起點,因此它是很是重要的一個輔助方法。

另外,第二組還有兩個特殊的方法: arcTo()addArc()。它們也是用來畫線的,但並不使用當前位置做爲弧線的起點。

arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(RectF oval, float startAngle, float sweepAngle) 畫弧形

這個方法和 Canvas.drawArc() 比起來,少了一個參數 useCenter,而多了一個參數 forceMoveTo

少了 useCenter ,是由於 arcTo() 只用來畫弧形而不畫扇形,因此再也不須要 useCenter 參數;而多出來的這個 forceMoveTo 參數的意思是,繪製是要「擡一下筆移動過去」,仍是「直接拖着筆過去」,區別在因而否留下移動的痕跡。

paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.arcTo(100, 100, 300, 300, -90, 90, true); // 強制移動到弧形起點(無痕跡)複製代碼

paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.arcTo(100, 100, 300, 300, -90, 90, false); // 直接連線連到弧形起點(有痕跡)複製代碼

addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) / addArc(RectF oval, float startAngle, float sweepAngle)

又是一個弧形的方法。一個叫 arcTo ,一個叫 addArc(),都是弧形,區別在哪裏?其實很簡單: addArc() 只是一個直接使用了 forceMoveTo = true 的簡化版 arcTo()

paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.addArc(100, 100, 300, 300, -90, 90);複製代碼

close() 封閉當前子圖形

它的做用是把當前的子圖形封閉,即由當前位置向當前子圖形的起點繪製一條直線。

paint.setStyle(Style.STROKE);
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);
// 子圖形未封閉複製代碼

paint.setStyle(Style.STROKE);
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);
path.close(); // 使用 close() 封閉子圖形。等價於 path.lineTo(100, 100)複製代碼

close()lineTo(起點座標) 是徹底等價的。

「子圖形」:官方文檔裏叫作 contour 。但因爲在這個場景下我找不到這個詞合適的中文翻譯(直譯的話叫作「輪廓」),因此我換了個便於中國人理解的詞:「子圖形」。前面說到,第一組方法是「添加子圖形」,所謂「子圖形」,指的就是一次不間斷的連線。一個 Path 能夠包含多個子圖形。當使用第一組方法,即 addCircle() addRect() 等方法的時候,每一次方法調用都是新增了一個獨立的子圖形;而若是使用第二組方法,即 lineTo() arcTo() 等方法的時候,則是每一次斷線(即每一次「擡筆」),都標誌着一個子圖形的結束,以及一個新的子圖形的開始。

另外,不是全部的子圖形都須要使用 close() 來封閉。當須要填充圖形時(即 Paint.StyleFILLFILL_AND_STROKEPath 會自動封閉子圖形。

paint.setStyle(Style.FILL);
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);
// 這裏只繪製了兩條邊,但因爲 Style 是 FILL ,因此繪製時會自動封口複製代碼

以上就是 Path 的第一類方法:直接描述路徑的。

Path 方法第二類:輔助的設置或計算

這類方法的使用場景比較少,我在這裏就很少講了,只講其中一個方法: setFillType(FillType fillType)

Path.setFillType(Path.FillType ft) 設置填充方式

前面在說 dir 參數的時候提到, Path.setFillType(fillType) 是用來設置圖形自相交時的填充算法的:

方法中填入不一樣的 FillType 值,就會有不一樣的填充效果。FillType 的取值有四個:

  • EVEN_ODD
  • WINDING (默認值)
  • INVERSE_EVEN_ODD
  • INVERSE_WINDING

其中後面的兩個帶有 INVERSE_ 前綴的,只是前兩個的反色版本,因此只要把前兩個,即 EVEN_ODDWINDING,搞明白就能夠了。

EVEN_ODDWINDING 的原理有點複雜,直接講出來的話信息量太大,因此我先給一個簡單粗暴版的總結,你感覺一下: WINDING 是「全填充」,而 EVEN_ODD 是「交叉填充」:

之因此叫「簡單粗暴版」,是由於這些只是一般情形下的效果;而若是要準確瞭解它們在全部狀況下的效果,就得先知道它們的原理,即它們的具體算法。

EVEN_ODD 和 WINDING 的原理

EVEN_ODD

即 even-odd rule (奇偶原則):對於平面中的任意一點,向任意方向射出一條射線,這條射線和圖形相交的次數(相交纔算,相切不算哦)若是是奇數,則這個點被認爲在圖形內部,是要被塗色的區域;若是是偶數,則這個點被認爲在圖形外部,是不被塗色的區域。還以左右相交的雙圓爲例:

射線的方向無所謂,同一個點射向任何方向的射線,結果都是同樣的,不信你能夠試試。

從上圖能夠看出,射線每穿過圖形中的一條線,內外狀態就發生一次切換,這就是爲何 EVEN_ODD 是一個「交叉填充」的模式。

WINDING

即 non-zero winding rule (非零環繞數原則):首先,它須要你圖形中的全部線條都是有繪製方向的:

而後,一樣是從平面中的點向任意方向射出一條射線,但計算規則不同:以 0 爲初始值,對於射線和圖形的全部交點,遇到每一個順時針的交點(圖形從射線的左邊向右穿過)把結果加 1,遇到每一個逆時針的交點(圖形從射線的右邊向左穿過)把結果減 1,最終把全部的交點都算上,獲得的結果若是不是 0,則認爲這個點在圖形內部,是要被塗色的區域;若是是 0,則認爲這個點在圖形外部,是不被塗色的區域。

EVEN_ODD 相同,射線的方向並不影響結果。

因此,我前面的那個「簡單粗暴」的總結,對於 WINDING 來講並不徹底正確:若是你全部的圖形都用相同的方向來繪製,那麼 WINDING 確實是一個「全填充」的規則;但若是使用不一樣的方向來繪製圖形,結果就不同了。

圖形的方向:對於添加子圖形類方法(如 Path.addCircle() Path.addRect())的方向,由方法的 dir 參數來控制,這個在前面已經講過了;而對於畫線類的方法(如 Path.lineTo() Path.arcTo())就更簡單了,線的方向就是圖形的方向。

因此,完整版的 EVEN_ODDWINDING 的效果應該是這樣的:

INVERSE_EVEN_ODDINVERSE_WINDING ,只是把這兩種效果進行反轉而已,你懂了 EVEN_ODDWINDING ,天然也就懂 INVERSE_EVEN_ODDINVERSE_WINDING 了,我就不講了。

好,花了好長的篇幅來說 drawPath(path)Path,終於講完了。同時, Canvas 對圖形的繪製就也講完了。圖形簡單時,使用 drawCircle() drawRect() 等方法來直接繪製;圖形複雜時,使用 drawPath() 來繪製自定義圖形。

除此以外, Canvas 還能夠繪製 Bitmap 和文字。

drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 畫 Bitmap

繪製 Bitmap 對象,也就是把這個 Bitmap 中的像素內容貼過來。其中 lefttop 是要把 bitmap 繪製到的位置座標。它的使用很是簡單。

drawBitmap(bitmap, 200, 100, paint);複製代碼

它的重載方法:

drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) /
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) /
drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)

drawBitmap 還有一個兄弟方法 drawBitmapMesh(),能夠繪製具備網格拉伸效果的 Bitmap。 drawBitmapMesh() 的使用場景較少,因此不講了,若是有興趣你能夠本身研究一下。

drawText(String text, float x, float y, Paint paint) 繪製文字

界面裏全部的顯示內容,都是繪製出來的,包括文字。 drawText() 這個方法就是用來繪製文字的。參數 text 是用來繪製的字符串,xy 是繪製的起點座標。

canvas.drawText(text, 200, 100, paint);複製代碼

插播五: Paint.setTextSize(float textSize)

經過 Paint.setTextSize(textSize),能夠設置文字的大小。

paint.setTextSize(18);
canvas.drawText(text, 100, 25, paint);
paint.setTextSize(36);
canvas.drawText(text, 100, 70, paint);
paint.setTextSize(60);
canvas.drawText(text, 100, 145, paint);
paint.setTextSize(84);
canvas.drawText(text, 100, 240, paint);複製代碼

設置文字的位置和尺寸,這些只是繪製文字最基本的操做。文字的繪製具備極高的定製性,不過因爲它的定製性實在過高了,因此我會在後面專門用一期來說文字的繪製。這一期就很少講了。

嗯……就這樣吧。繪製部分第一節, CanvasdrawXXX() 系列方法和 Paint 的基本使用,就到這裏。

練習項目

爲了不轉頭就忘,強烈建議你趁熱打鐵,作一下這個練習項目:HenCoderPracticeDraw1

下期預告

下期是 Paint 的全面介紹,內容會比這期難,但也會更有趣,各位作好準備吧。最後再貼一次下節內容的部分圖片做爲預告:

感謝

感謝參與這期預發佈內測的讀者:

jiahuaAndroidhuajinyuanquhuainan、Van Gogh、停停走走、JK森、街 景、Tiger、意琦行_、MadisonRongAaronYiczwathou、別對生活說抱歉、hml、四葉花

讚揚

你給不給我錢,我都會認真作、全心作。因此給錢以前請慎重考慮,肯定你是要讚揚,而不是購買服務。

相關文章
相關標籤/搜索