天天看點

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

連結: 原帖跳轉

自定義 View 1-1 繪制基礎

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

如果你不知道 HenCoder 是什麼,可以先看這裡:

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

自定義繪制概述

二話不說,我反手就是一個視訊:

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

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

概念已經在視訊裡全部講出來了,知識點并不多,但你可能也看出來了,我講得并不細。這是因為知識點雖然不多,但細節還是很多的,僅僅靠一節分享不可能講完。我按照順序把這些知識分成了 4 個級别,拆成幾節來講,你按照這 4 個級别的順序學習下來,就能夠平滑地逐漸進階。

自定義繪制知識的四個級别

  1. Canvas 的 drawXXX() 系列方法及

    Paint

    最常見的使用

    Canvas.drawXXX()

    是自定義繪制最基本的操作。掌握了這些方法,你才知道怎麼繪制内容,例如怎麼畫圓、怎麼畫方、怎麼畫圖像和文字。組合繪制這些内容,再配合上

    Paint

    的一些常見方法來對繪制内容的顔色和風格進行簡單的配置,就能夠應付大部分的繪制需求了。
    Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
    Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
    Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
    今天這篇分享我要講的就是這些内容。也就是說,你在看完這篇文章并做完練習之後,上面這幾幅圖你就會繪制出來了。從今以後,你也很少再需要假裝一本正經地對設計師說「不行這個圖技術上實作不了」,也不用心驚膽戰得等待設計師的那句「那 iOS 怎麼可以」了。
    Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
  2. Paint

    的完全攻略

Paint

可以做的事,不隻是設定顔色,也不隻是我在視訊裡講的實心空心、線條粗細、有沒有陰影,它可以做的風格設定真的是非常多、非常細。例如:

拐角要什麼形狀?
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
開不開雙線性過濾?
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
加不加特效?
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

可以調節的非常多,我就不一一列舉了。當你掌握到這個級别,就真的不會有什麼東西會是 iOS 能做到但你做不到的了。就算設計師再設計出了很難做的東西,做不出來的也不再會是你們 Android 組了。

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
  1. Canvas

    對繪制的輔助——範圍裁切和幾何變換。

範圍裁切:

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

幾何變換:

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

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

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
  1. 使用不同的繪制方法來控制繪制順序
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

控制繪制順序解決的并不是「做不到」的問題,而是性能問題。同樣的一種效果,你不用繪制順序的控制往往也能做到,但需要用多個 View 甚至是多層 View 才能拼湊出來,是以代價是 UI 的性能;而使用繪制順序的控制的話,一個 View 就全部搞定了。

自定義繪制的知識,大概就分為上面這四個級别。在你把這四個級别依次掌握了之後,你就是一個自定義繪制的高手了。它們具體的細節,我将分成幾篇來講。今天這篇就是第一篇:

Canvas.drawXXX()

系列方法及

Paint

最基本的使用。我要正式開始喽?

一切的開始:onDraw()

自定義繪制的上手非常容易:提前建立好

Paint

對象,重寫

onDraw()

,把繪制代碼寫在

onDraw()

裡面,就是自定義繪制最基本的實作。大概就像這樣:

Paint paint = new Paint();
                

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

<span class="token comment">// 繪制一個圓</span>
canvas<span class="token punctuation">.</span><span class="token function">drawCircle</span><span class="token punctuation">(</span><span class="token number">300</span><span class="token punctuation">,</span> <span class="token number">300</span><span class="token punctuation">,</span> <span class="token number">200</span><span class="token punctuation">,</span> paint<span class="token punctuation">)</span><span class="token punctuation">;</span>
           

}

Java

就這麼簡單。是以關于

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 的官方文檔裡,打開 Canvas 和 Paint 的頁面,把上面的這兩類方法學習一下,然後今天的内容就算結束了。當然,這篇文章也可以關掉了。

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

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

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

這是最基本的

drawXXX()

方法:在整個繪制區域統一塗上指定的顔色。

例如

drawColor(Color.BLACK)

會把整個區域染成純黑色,覆寫掉原有内容;

drawColor(Color.parse("#88880000")

會在原有的繪制效果上加一層半透明的紅色遮罩。

drawColor(Color.BLACK);  // 純黑
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
drawColor(Color.parse("#88880000"); // 半透明紅色
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

類似的方法還有

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);
                

Java

這類顔色填充方法一般用于在繪制之前設定底色,或者在繪制之後為界面設定半透明蒙版。

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

前兩個參數

centerX

centerY

是圓心的坐标,第三個參數

radius

是圓的半徑,機關都是像素,它們共同構成了這個圓的基本資訊(即用這幾個資訊可以建構出一個确定的圓);第四個參數

paint

我在視訊裡面已經說過了,它提供基本資訊之外的所有風格資訊,例如顔色、線條粗細、陰影等。

canvas.drawCircle(300, 300, 200, paint);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

那位說:「你等會兒!先别往後講,你剛才說圓心的坐标,我想問坐标系在哪兒呢?沒坐标系你跟我聊什麼坐标啊。」

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

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

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

也就是說,

canvas.drawCircle(300, 300, 200, paint)

這行代碼繪制出的圓,在 View 中的位置和尺寸應該是這樣的:

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

圓心坐标和半徑,這些都是圓的基本資訊,也是它的獨有資訊。什麼叫獨有資訊?就是隻有它有,别人沒有的資訊。你畫圓有圓心坐标和半徑,畫方有嗎?畫橢圓有嗎?這就叫獨有資訊。獨有資訊都是直接作為參數寫進

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);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

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);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

setStyle(Style style)

這個方法設定的是繪制的

Style

Style

具體來說有三種:

FILL

,

STROKE

FILL_AND_STROKE

FILL

是填充模式,

STROKE

是畫線模式(即勾邊模式),

FILL_AND_STROKE

是兩種模式一并使用:既畫線又填充。它的預設值是

FILL

,填充模式。

插播三: Paint.setStrokeWidth(float width)

STROKE

FILL_AND_STROKE

下,還可以使用

paint.setStrokeWidth(float width)

來設定線條的寬度:

paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(20); // 線條寬度為 20 像素
canvas.drawCircle(300, 300, 200, paint);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
插播四: 抗鋸齒

在繪制的時候,往往需要開啟抗鋸齒來讓圖形和文字的邊緣更加平滑。開啟抗鋸齒很簡單,隻要在

new Paint()

的時候加上一個

ANTI_ALIAS_FLAG

參數就行:

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
                

Java

另外,你也可以使用

Paint.setAntiAlias(boolean aa)

來動态開關抗鋸齒。

抗鋸齒的效果如下:

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

可以看出,沒有開啟抗鋸齒的時候,圖形會有毛片現象,啊不,毛邊現象。是以一定記得要打開抗鋸齒喲!

可以跳過的冷知識

好奇的人可能會問:抗鋸齒既然這麼有用,為什麼不預設開啟,或者幹脆把這個開關取消,自動讓所有繪制都開啟抗鋸齒?

短答案:因為抗鋸齒并不一定适合所有場景。

長答案:所謂的毛邊或者鋸齒,發生的原因并不是很多人所想象的「繪制太粗糙」「像素計算能力不足」;同樣,抗鋸齒的原理也并不是選擇了更精細的算法來算出了更平滑的圖形邊緣。

實質上,鋸齒現象的發生,隻是由于圖形分辨率過低,導緻人眼察覺出了畫面中的像素顆粒而已。換句話說,就算不開啟抗鋸齒,圖形的邊緣也已經是最完美的了,而并不是一個粗略計算的粗糙版本。

那麼,為什麼抗鋸齒開啟之後的圖形邊緣會更加平滑呢?因為抗鋸齒的原理是:修改圖形邊緣處的像素顔色,進而讓圖形在肉眼看來具有更加平滑的感覺。一圖勝千言,上圖:

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

上面這個是把前面那兩個圓放大後的局部效果。看到沒有?未開啟抗鋸齒的圓,所有像素都是同樣的黑色,而開啟了抗鋸齒的圓,邊緣的顔色被略微改變了。這種改變可以讓人眼有邊緣平滑的感覺,但從某種角度講,它也造成了圖形的顔色失真。

是以,抗鋸齒好不好?好,大多數情況下它都應該是開啟的;但在極少數的某些時候,你還真的需要把它關閉。「某些時候」是什麼時候?到你用到的時候自然就知道了。

除了圓,

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);

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

另外,它還有兩個重載方法

drawRect(RectF rect, Paint paint)

drawRect(Rect rect, Paint paint)

,讓你可以直接填寫

RectF

Rect

對象來繪制矩形。

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

x

y

是點的坐标。點的大小可以通過

paint.setStrokeWidth(width)

來設定;點的形狀可以通過

paint.setStrokeCap(cap)

來設定:

ROUND

畫出來是圓形的點,

SQUARE

BUTT

畫出來是方形的點。(點還有形狀?是的,反正 Google 是這麼說的,你要問問 Google 去,我也很懵逼。)

注:

Paint.setStrokeCap(cap)

可以設定點的形狀,但這個方法并不是專門用來設定點的形狀的,而是一個設定線條端點形狀的方法。端點有圓頭 (

ROUND

)、平頭 (

BUTT

) 和方頭 (

SQUARE

) 三種,具體會在下一節裡面講。
paint.setStrokeWidth(20);
paint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawPoint(50, 50, paint);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
paint.setStrokeWidth(20);
paint.setStrokeCap(Paint.Cap.SQUARE);
canvas.drawPoint(50, 50, paint);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

好像有點像

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);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

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);

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

另外,它還有一個重載方法

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);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
由于直線不是封閉圖形,是以

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);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
咦,不小心打出兩個漢字。——是漢字吧?

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

left

,

top

,

right

,

bottom

是四條邊的坐标,

rx

ry

是圓角的橫向半徑和縱向半徑。

canvas.drawRoundRect(100, 100, 500, 300, 50, 50, paint);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

另外,它還有一個重載方法

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); // 繪制不封口的弧形
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

到此為止,以上就是

Canvas

所有的簡單圖形的繪制。除了簡單圖形的繪制,

Canvas

還可以使用

drawPath(Path path)

來繪制自定義圖形。

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

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

前面的這些方法,都是繪制某個給定的圖形,而

drawPath()

可以繪制自定義圖形。當你要繪制的圖形比較特殊,使用前面的那些方法做不到的時候,就可以使用

drawPath()

來繪制。

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

drawPath(path)

這個方法是通過描述路徑的方式來繪制圖形的,它的

path

參數就是用來描述圖形路徑的對象。

path

的類型是

Path

,使用方法大概像下面這樣:

public class PathView extends View {
                 
<span class="token class-name">Paint</span> paint <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Paint</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Path</span> path <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Path</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 初始化 Path 對象</span>

<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>

<span class="token punctuation">{</span>
  <span class="token comment">// 使用 path 對圖形進行描述(這段描述代碼不必看懂)</span>
  path<span class="token punctuation">.</span><span class="token function">addArc</span><span class="token punctuation">(</span><span class="token number">200</span><span class="token punctuation">,</span> <span class="token number">200</span><span class="token punctuation">,</span> <span class="token number">400</span><span class="token punctuation">,</span> <span class="token number">400</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">225</span><span class="token punctuation">,</span> <span class="token number">225</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  path<span class="token punctuation">.</span><span class="token function">arcTo</span><span class="token punctuation">(</span><span class="token number">400</span><span class="token punctuation">,</span> <span class="token number">200</span><span class="token punctuation">,</span> <span class="token number">600</span><span class="token punctuation">,</span> <span class="token number">400</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">180</span><span class="token punctuation">,</span> <span class="token number">225</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  path<span class="token punctuation">.</span><span class="token function">lineTo</span><span class="token punctuation">(</span><span class="token number">400</span><span class="token punctuation">,</span> <span class="token number">542</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token annotation punctuation">@Override</span>
<span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">onDraw</span><span class="token punctuation">(</span><span class="token class-name">Canvas</span> canvas<span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onDraw</span><span class="token punctuation">(</span>canvas<span class="token punctuation">)</span><span class="token punctuation">;</span>
  
  canvas<span class="token punctuation">.</span><span class="token function">drawPath</span><span class="token punctuation">(</span>path<span class="token punctuation">,</span> paint<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 繪制出 path 描述的圖形(心形),大功告成</span>
<span class="token punctuation">}</span>
                

}

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

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.Style

FILL

FILL_AND_STROKE

) ,并且圖形出現自相交時,用于判斷填充範圍的。比如下面這個圖形:
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
是應該填充成這樣呢:
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
還是應該填充成這樣呢:
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
想用哪種方式來填充,都可以由你來決定。具體怎麼做,下面在講

Path.setFillType()

的時候我會詳細介紹,而在這裡你可以先忽略

dir

這個參數。

在用

addCircle()

Path

中新增一個圓之後,調用

canvas.drawPath(path, paint)

,就能畫一個圓出來。就像這樣:

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

...

canvas.drawPath(path, paint);

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

可以看出,

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) 畫直線

從目前位置向目标位置畫一條直線,

x

y

是目标位置的坐标。這兩個方法的差別是,

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 像素的位置畫一條直線
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

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

這條二次貝塞爾曲線的起點就是目前位置,而參數中的

x1

,

y1

x2

,

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); // 畫豎線
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

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); // 強制移動到弧形起點(無痕迹)
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.arcTo(100, 100, 300, 300, -90, 90, false); // 直接連線連到弧形起點(有痕迹)
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

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);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

close() 封閉目前子圖形

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

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

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
paint.setStyle(Style.STROKE);
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);
path.close(); // 使用 close() 封閉子圖形。等價于 path.lineTo(100, 100)
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

close()

lineTo(起點坐标)

是完全等價的。

「子圖形」:官方文檔裡叫做

contour

。但由于在這個場景下我找不到這個詞合适的中文翻譯(直譯的話叫做「輪廓」),是以我換了個便于中國人了解的詞:「子圖形」。前面說到,第一組方法是「添加子圖形」,所謂「子圖形」,指的就是一次不間斷的連線。一個

Path

可以包含多個子圖形。當使用第一組方法,即

addCircle()

addRect()

等方法的時候,每一次方法調用都是新增了一個獨立的子圖形;而如果使用第二組方法,即

lineTo()

arcTo()

等方法的時候,則是每一次斷線(即每一次「擡筆」),都标志着一個子圖形的結束,以及一個新的子圖形的開始。
另外,不是所有的子圖形都需要使用

close()

來封閉。當需要填充圖形時(即

Paint.Style

FILL

FILL_AND_STROKE

),

Path

會自動封閉子圖形。
paint.setStyle(Style.FILL);
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);
// 這裡隻繪制了兩條邊,但由于 Style 是 FILL ,是以繪制時會自動封口
                
Java
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

以上就是

Path

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

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

這類方法的使用場景比較少,我在這裡就不多講了,隻講其中一個方法:

setFillType(FillType fillType)

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

前面在說

dir

參數的時候提到,

Path.setFillType(fillType)

是用來設定圖形自相交時的填充算法的:

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

方法中填入不同的

FillType

值,就會有不同的填充效果。

FillType

的取值有四個:

  • EVEN_ODD

  • WINDING

    (預設值)
  • INVERSE_EVEN_ODD

  • INVERSE_WINDING

其中後面的兩個帶有

INVERSE_

字首的,隻是前兩個的反色版本,是以隻要把前兩個,即

EVEN_ODD

WINDING

,搞明白就可以了。

EVEN_ODD

WINDING

的原理有點複雜,直接講出來的話資訊量太大,是以我先給一個簡單粗暴版的總結,你感受一下:

WINDING

是「全填充」,而

EVEN_ODD

是「交叉填充」:

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

之是以叫「簡單粗暴版」,是因為這些隻是通常情形下的效果;而如果要準确了解它們在所有情況下的效果,就得先知道它們的原理,即它們的具體算法。

EVEN_ODD 和 WINDING 的原理

EVEN_ODD

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

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
射線的方向無所謂,同一個點射向任何方向的射線,結果都是一樣的,不信你可以試試。

從上圖可以看出,射線每穿過圖形中的一條線,内外狀态就發生一次切換,這就是為什麼

EVEN_ODD

是一個「交叉填充」的模式。

WINDING

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

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

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

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

EVEN_ODD

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

是以,我前面的那個「簡單粗暴」的總結,對于

WINDING

來說并不完全正确:如果你所有的圖形都用相同的方向來繪制,那麼

WINDING

确實是一個「全填充」的規則;但如果使用不同的方向來繪制圖形,結果就不一樣了。

圖形的方向:對于添加子圖形類方法(如

Path.addCircle()

Path.addRect()

)的方向,由方法的

dir

參數來控制,這個在前面已經講過了;而對于畫線類的方法(如

Path.lineTo()

Path.arcTo()

)就更簡單了,線的方向就是圖形的方向。

是以,完整版的

EVEN_ODD

WINDING

的效果應該是這樣的:

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

INVERSE_EVEN_ODD

INVERSE_WINDING

,隻是把這兩種效果進行反轉而已,你懂了

EVEN_ODD

WINDING

,自然也就懂

INVERSE_EVEN_ODD

INVERSE_WINDING

了,我就不講了。

好,花了好長的篇幅來講

drawPath(path)

Path

,終于講完了。同時,

Canvas

對圖形的繪制就也講完了。圖形簡單時,使用

drawCircle()

drawRect()

等方法來直接繪制;圖形複雜時,使用

drawPath()

來繪制自定義圖形。

除此之外,

Canvas

還可以繪制

Bitmap

和文字。

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

繪制

Bitmap

對象,也就是把這個

Bitmap

中的像素内容貼過來。其中

left

top

是要把

bitmap

繪制到的位置坐标。它的使用非常簡單。

drawBitmap(bitmap, 200, 100, paint);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

它的重載方法:

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

是用來繪制的字元串,

x

y

是繪制的起點坐标。

canvas.drawText(text, 200, 100, paint);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

插播五: 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);
                

Java

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
設定文字的位置和尺寸,這些隻是繪制文字最基本的操作。文字的繪制具有極高的定制性,不過由于它的定制性實在太高了,是以我會在後面專門用一期來講文字的繪制。這一期就不多講了。

嗯……就這樣吧。繪制部分第一節,

Canvas

drawXXX()

系列方法和

Paint

的基本使用,就到這裡。

練習項目

為了避免轉頭就忘,強烈建議你趁熱打鐵,做一下這個練習項目:HenCoderPracticeDraw1

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

下期預告

下期是

Paint

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

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎
Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

感謝

感謝參與這期預釋出内測的讀者:

jiahuaAndroid、huajinyuan、quhuainan、Van Gogh、停停走走、JK森、街 景、Tiger、意琦行_、MadisonRong、AaronYi、czwathou、别對生活說抱歉、hml、四葉花

訂閱

由于網站的訂閱功能依然沒做好,是以可以先去我的知乎專欄、掘金專欄或微信公衆号(HenCoder)訂閱。微信公衆号二維碼:

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

贊賞

你給不給我錢,我都會認真做、全心做。是以給錢之前請慎重考慮,确定你是要贊賞,而不是購買服務。

Android-安卓Canvas進階畫圖自定義 View 1-1 繪制基礎

繼續閱讀