天天看點

Canvas中的繪圖師講解與實戰——Android進階UI

目錄

    • 一、前言
    • 二、如何畫好一幅圖
      • 1、rotate 旋轉
      • 2、scale 縮放
      • 3、skew 斜切
      • 4、translate 偏移
      • 5、setMatrix 矩陣
    • 三、Canvas的圖形API
      • 1、drawCircle 畫圓
      • 2、drawOval 畫橢圓
      • 3、drawLine 畫線
      • 4、drawArc 畫弧
      • 5、drawPoint 畫點
      • 6、drawRect 畫矩形
      • 7、drawRoundRect 畫圓角矩形
      • 8、drawColor 給畫布點顔色
      • 9、drawRGB 給畫布點顔色
      • 10、drawPath 繪制路徑
    • 四、畫布儲存狀态API
      • 1、狀态值
      • 2、save
      • 3、saveLayer
      • 4、saveLayerAlpha
      • 5、恢複
      • 6、小結
    • 五、實戰——時鐘與指針
      • 1、效果圖
      • 2、程式設計思路
        • (1)一個圓圈
        • (2)刻度
        • (3)指針
      • (4)開啟旋轉
    • 六、寫在最後

一、前言

在上一篇文章中,我們隻是分享了裁剪類型的API,今天接着分享繪圖部分API。話不多說,老規矩,先上實戰圖。

時鐘與指針

Canvas中的繪圖師講解與實戰——Android進階UI

二、如何畫好一幅圖

我們在上一篇文章中講到了,繪制一幅圖的工具和坐标系。我們繼續思考,在現實中使用一張紙繪制時,我們會對這張紙進行旋轉一定角度來友善自己繪制,有時為了繪制一些細節,會進行放大,有時也會進行移動這張紙。而這些操作,在canvas中也有各自對應的操作。

1、rotate 旋轉

(1)第一個rotate函數

描述: 以原點為旋轉中心,旋轉畫布 degrees 角度。正數為順時針旋轉,負數為逆時針旋轉。

舉個例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.rotate(30);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);
           

效果圖

紅色為原圖,藍色為旋轉後繪制的圖。

Canvas中的繪圖師講解與實戰——Android進階UI

(2)第二個rotate函數

描述: 以 (px, py) 為旋轉中心,将畫布旋轉 degrees 角度。正數為順時針旋轉,負數為逆時針旋轉。

舉個例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.rotate(30, 200, 300);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);
           

效果圖

紅色為原圖,藍色為旋轉後繪制的圖。

Canvas中的繪圖師講解與實戰——Android進階UI

2、scale 縮放

(1)第一個scale函數

描述 : 以原點進行縮放畫布,x軸縮放 sx 倍,y軸縮放 sy 倍。

舉個例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.scale(0.5f,0.33f);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);
           

效果圖

紅色為原圖,藍色為縮放後繪制的圖。

Canvas中的繪圖師講解與實戰——Android進階UI

(2)第二個scale函數

描述: 以 (px, py) 進行縮放畫布,x軸縮放 sx 倍,y軸縮放 sy 倍。

舉個例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.scale(0.5f, 0.33f, 200, 300);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);
           

效果圖

紅色為原圖,藍色為縮放後繪制的圖。

Canvas中的繪圖師講解與實戰——Android進階UI

3、skew 斜切

描述: 進行 x 軸和 y軸 的拉伸。

拉伸規則 當一個點為(x, y)時,進行斜切變換(sx, sy),得到的結果 (rx, ry)

  1. rx = x + sx * y;
  2. ry = y + sy * x;

舉個例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.skew(1, 0.5f);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);
           

效果圖

紅色為原圖,藍色為斜切後繪制的圖。

可以使用上面的 “拉伸規則” ,将紅色框的點帶入便可得到藍色框對應的點。
Canvas中的繪圖師講解與實戰——Android進階UI

4、translate 偏移

描述: 将畫布水準移動 dx 個像素, 垂直移動 dy 個像素。

舉個例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.translate(100, 200);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);
           

效果圖

紅色為原圖,藍色為移動後繪制的圖。

Canvas中的繪圖師講解與實戰——Android進階UI

5、setMatrix 矩陣

描述: 将矩陣作用于畫布。

舉個例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

mMatrix.preTranslate(getWidth() / 2, getHeight() / 2);
mMatrix.preScale(2, 1);

canvas.setMatrix(mMatrix);
mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);
           

效果圖

紅色為原圖,藍色為使用矩陣後繪制的圖。

Canvas中的繪圖師講解與實戰——Android進階UI

值得一提

矩陣的内容比較多,這裡隻是略帶一提,如果想見識見識他的真正威力,可以看看在小盆友另一篇博文放蕩不羁SVG講解與實戰實戰中的使用,具體代碼請進傳送門。

三、Canvas的圖形API

1、drawCircle 畫圓

描述: 在坐标為 (cx,cy) 的地方繪制半徑為 radius 的圓。

舉個例子

// 在 原點處 畫半徑為100的圓
canvas.drawCircle(0, 0, 100, mPaint);
           

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

2、drawOval 畫橢圓

(1)第一個drawOval函數

描述: 在 oval 的矩形範圍内,繪制橢圓。

舉個例子

RectF mRectF = new RectF();
mRectF.left = -150;
mRectF.top = -150;
mRectF.right = 400;
mRectF.bottom = 150;

canvas.drawOval(mRectF, mPaint);
           

效果圖

橘色部分則為我們繪制的橢圓,而紫色框(為了友善觀看而繪制出來)則是我們的 oval 的範圍。

Canvas中的繪圖師講解與實戰——Android進階UI

(2)第二個drawOval函數

描述: 在 左上(left,top) 和 右下(right,bottom) 形成的矩形範圍内,繪制橢圓。

值得注意的是,這個方法隻能在 API21 以上的版本 才能使用,是以建議使用第一個函數。

舉個例子

效果圖

橘色部分則為我們繪制的橢圓,而紫色框(為了友善觀看而繪制出來)則是我們的 oval 的範圍。

兩個函數效果完全一樣,隻是前一個函數将兩個坐标點封裝在 Rect 中,而後一函數展示在函數參數中。
Canvas中的繪圖師講解與實戰——Android進階UI

3、drawLine 畫線

(1)drawLine函數

public void drawLine(float startX, float startY, float stopX, float stopY,
            @NonNull Paint paint)         
           

描述: 在坐标 (startX, startY) 和 (stopX, stopY) 中繪制一條直線。

舉個例子

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

(2)第一個drawLines函數

描述: pts數組中每四個數構成一條直線,每四個數中前兩個為起始坐标,後兩個為終止坐标。如果不夠四個數,則這一組不進行繪制。

舉個例子

private float[] pts = new float[]{
            0, -400, 200, -400, // 構成上面的線
            -300, 0, -300, 300, // 構成左邊的線
            0, 400, 300, 400    // 構成右邊的線
    };

canvas.drawLines(pts, mPaint);
           

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

(3)第二個drawLines函數(帶偏移)

public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count,
            @NonNull Paint paint) 
           

描述: 該方法比上一個方法多加兩個參數,即偏移量和數量。偏移量offset為一時,則從pts的下标為1的地方開始進行讀數,count則決定了多少個數。

舉個例子

private float[] pts = new float[]{
            0, -400, 200, -400, 
            -300, 0, -300, 300, 
            0, 400, 300, 400    
    };

canvas.drawLines(pts, 2, 8, mPaint);
           

效果圖

pts數組中,從下标為2的數字開始,每四個數構成一條線,直到下标為 10 (由8+2得來) 的數為止。第一條線為上面的線,第二條線為下面的線。

Canvas中的繪圖師講解與實戰——Android進階UI

4、drawArc 畫弧

(1)第一個drawArc函數

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

描述: 在 oval矩形範圍内,繪制 從startAngle角度 到 sweepAngle角度的圓弧。

參數說明:

1)oval:圓弧所繪的矩形範圍區域。

2)startAngle:起始角度。0度時,指向為坐标系中的x軸正半軸。

3)sweepAngle:基于 startAngle 角度,掃過的角度範圍,正數則按順時針方向,負數則按逆時針方向。

4)useCenter:弧的兩端是否要連接配接中心點。true連接配接中心點,false不連接配接中心點。

5)paint:畫筆。

舉個例子

RectF mRectF = new RectF();
mRectF.left = -150;
mRectF.top = -150;
mRectF.right = 400;
mRectF.bottom = 150;

canvas.drawArc(mRectF, 0, 120, true, mPaint);
           

效果圖

橘色部分則為弧線部分,紫色則為矩形範圍(為了友善檢視才繪出)。

Canvas中的繪圖師講解與實戰——Android進階UI

(2)第二個drawArc函數

public void drawArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean useCenter, @NonNull Paint paint)
           

描述: 該方法和上一方法功能完全一樣,隻是用四個 float 表示 矩形的端點。

舉個例子

效果圖

橘色為圓弧,紫色為矩形範圍

Canvas中的繪圖師講解與實戰——Android進階UI

5、drawPoint 畫點

(1)drawPoint函數

描述: 在坐标為 (x,y) 處繪制點

舉個例子

mPaint.setColor(mColor1);
mPaint.setStrokeWidth(dpToPx(5));
canvas.drawPoint(100, 100, mPaint);
           

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

(2)第一個drawPoints函數

描述: pts數組中每兩個數構成一個坐标(前者為x,後者為y),并在該坐标處點。

舉個例子

private float[] pts = new float[]{
        0, -400,
        200, -400,
        -300, 0
};

mPaint.setColor(mColor2);
mPaint.setStrokeWidth(dpToPx(5));
canvas.drawPoints(pts, mPaint);
           

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

(3)第二個drawPoints函數(帶偏移)

public void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count,
            @NonNull Paint paint)
           

描述: 這個方法和上面的方法大緻相同,唯一差別在于從下标為offset開始讀取坐标,讀取長度個數為count。

舉個例子

private float[] pts = new float[]{
        0, -400,
        200, -400,
        -300, 0
};

mPaint.setColor(mColor2);
mPaint.setStrokeWidth(dpToPx(5));
canvas.drawPoints(pts, 1, pts.length - 1, mPaint);
           

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

6、drawRect 畫矩形

(1)drawRect函數

public void drawRect(@NonNull RectF rect, @NonNull Paint paint)
public void drawRect(@NonNull Rect r, @NonNull Paint paint)
           

描述: 在 rect 的範圍内繪制矩形,兩個方法的唯一差別在于第一個參數類型分别為 RectF 和 Rect。

RectF 和 Rect 的差別:

  1. 精度不同:RectF 四個點為浮點數,Rect 四個點為整型
  2. 所包含的方法不完全相同。

舉個例子

RectF mRectF = new RectF();
mRectF.left = -150;
mRectF.top = -150;
mRectF.right = 400;
mRectF.bottom = 150;

canvas.drawRect(mRectF, mPaint);
           

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

(2)drawRect函數

描述: 在 (left,top) 和 (right,bottom) 形成的矩形範圍内繪制矩形。

舉個例子

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

7、drawRoundRect 畫圓角矩形

(1)第一個drawRoundRect函數

描述: 在 rect 範圍内,繪制圓角矩形。

參數說明:

1)rx:水準方向的半徑,下圖中的橘色部分

2)ry:豎直方向的半徑,下圖中的紅色部分

Canvas中的繪圖師講解與實戰——Android進階UI

舉個例子

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

(2)第二個drawRoundRect函數

public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,
            @NonNull Paint paint)
           

描述: 與上述的方法功能完全相同,隻是繪制範圍由四個浮點數進行确定。

舉個例子

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

8、drawColor 給畫布點顔色

(1)第一個drawColor函數

描述: 給畫布繪制color顔色值。

舉個例子

比較簡單就不上效果圖了。

(2)第二個drawColor函數

描述: 給畫布繪制顔色,會與之前的圖形有 mode 的作用。

舉個例子

Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo);

Matrix mMatrix = new Matrix();
mMatrix.setScale(0.25f, 0.25f);

canvas.drawBitmap(mBitmap, mMatrix, mPaint);
canvas.drawColor(Color.parseColor("#88880000"),
                PorterDuff.Mode.DST_OVER);
           

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

值得一提

我們所介紹的第一個

drawColor(@ColorInt int color)

函數,其實最後使用了

PorterDuff.Mode.SRC_OVER

模式。

至于 PorterDuff.Mode 的具體使用,請看小盆友的另一篇博文:圖像操縱大師Xfermode講解與實戰

9、drawRGB 給畫布點顔色

(1)drawRGB函數

描述: 給畫布繪制顔色,按照 紅(r),綠(g),藍(b) 三原色進行組合

舉個例子

(2)drawARGB函數

描述: 給畫布繪制顔色,按照 透明度(a),紅(r),綠(g),藍(b) 三原色進行組合

舉個例子

10、drawPath 繪制路徑

描述: 将 路徑path 繪制在畫布上。

舉個例子

這個方法使用的地方非常之多,例如我們繪制一個 “心” 形

mPaint.setColor(mColor1);
mPaint.setStyle(Paint.Style.FILL);
// 路徑的建構,移步github
canvas.drawPath(mPath, mPaint);
           

效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

值得一提

心形路徑的建構使用了 貝塞爾曲線,對 貝塞爾曲線 有興趣的童鞋,可以移步小盆友的另一篇博文:自帶美感的貝塞爾曲線原理與實戰

四、畫布儲存狀态API

1、狀态值

在進行 API 講解前,我們需要先說明狀态值,他控制着我們要儲存什麼資訊。

  1. MATRIX_SAVE_FLAG:儲存圖層的 Matrix矩陣資訊
  2. CLIP_SAVE_FLAG:儲存裁剪資訊
  3. HAS_ALPHA_LAYER_SAVE_FLAG:儲存該圖層的透明度
  4. FULL_COLOR_LAYER_SAVE_FLAG:完全保留該圖層顔色
  5. CLIP_TO_LAYER_SAVE_FLAG:建立圖層時,會把canvas(所有圖層)裁剪到參數指定的範圍,如果省略這個flag将導緻圖層開銷巨大,性能不好。
  6. ALL_SAVE_FLAG:儲存所有資訊

敲黑闆了!!! 雖然羅列了這麼多,但1-5的FLAG已經全部被遺棄,隻剩

ALL_SAVE_FLAG

這個FLAG。

2、save

描述: 這個函數用于儲存圖層狀态,儲存此刻的 canvas 畫布的所有狀态(例如:原點位置,旋轉角度,一切我們對canvas的操作都被儲存)。

3、saveLayer

// saveFlags 隻能是 Canvas.ALL_SAVE_FLAG
public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
            @Saveflags int saveFlags)
      
// saveFlags 隻能是 Canvas.ALL_SAVE_FLAG      
public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags)

// API21及以上才可使用
public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint)
// API21及以上才可使用
public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) 
           

描述: 該方法與

save

一樣會儲存進狀态棧,然後通過

restore

restoreToCount

進行恢複。不同的是該方法會建立一個新的圖層。

這裡建立的圖層,我們可以類比為PS中的圖層概念,存在意義是不會影響到其他圖層的資料。例如我們在XFermode的博文中的刮刮卡的實戰中,就有用到這一概念,否則我們需要看到的圖檔也會被一同清除。

4、saveLayerAlpha

// saveFlags 隻能是 Canvas.ALL_SAVE_FLAG
public int saveLayerAlpha(@Nullable RectF bounds, int alpha, @Saveflags int saveFlags)
      
// saveFlags 隻能是 Canvas.ALL_SAVE_FLAG  
public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha,
            @Saveflags int saveFlags)

// API21及以上才可使用
public int saveLayerAlpha(@Nullable RectF bounds, int alpha) 
// API21及以上才可使用
public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha)
           

描述: 與

saveLayer

相同的是會存進狀态棧和建立一個圖層,然後通過

restore

restoreToCount

進行恢複。不同的是建立的圖層是具有透明度的,而透明度由 alpha 決定,範圍為 0-255。

5、恢複

// 恢複
public void restore()

// 恢複至指定的 狀态棧層數
public void restoreToCount(int saveCount)
           

描述: 這兩個方法,是将上面三種方法儲存的函數進行恢複。而差別在于

restore

每次從狀态棧中恢複拿出一個狀态恢複,而

restoreToCount

是 恢複到指定的狀态棧層數(該層也會被出棧),這個 saveCount 參數在上面三種類型的方法調用後都會進行傳回各自對應的層數。

6、小結

先舉個例子彙總一下這幾個方法:

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

    log(canvas);

    int layer = canvas.saveLayer(0, 0, getWidth(), getHeight(),
            mPaint, Canvas.ALL_SAVE_FLAG);
    log(canvas);

    canvas.save();
    log(canvas);

    canvas.saveLayer(0, 0, getWidth(), getHeight(),
            mPaint, Canvas.ALL_SAVE_FLAG);
    log(canvas);

    canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(),
            50, Canvas.ALL_SAVE_FLAG);
    log(canvas);

    canvas.translate(getWidth() / 2, getHeight() / 2);
    canvas.drawRect(mRect, mPaint);

    canvas.restore();
    log(canvas);

    canvas.restoreToCount(layer);
    log(canvas);

}

private void log(Canvas canvas) {
    Log.i("canvas", "canvas count:" + canvas.getSaveCount());
}
           

輸出結果

Canvas中的繪圖師講解與實戰——Android進階UI

我們從代碼和輸出結果可以得出以下幾個結論:

  1. 初始狀态下,狀态棧中便有一個預設的狀态;
  2. 在不建立圖層的情況下,所有操作都是作用于預設圖層;
  3. 使用

    restoreToCount(x)

    進行恢複,會連同x層出棧;

一圖勝千言:

将上面的代碼轉換成圖,就如下效果

Canvas中的繪圖師講解與實戰——Android進階UI

五、實戰——時鐘與指針

1、效果圖

Canvas中的繪圖師講解與實戰——Android進階UI

github位址:傳送門

2、程式設計思路

我們先拆解下這幅圖,其實構成的為三部分:

  1. 一個圓圈
  2. 刻度
  3. 指針

我們逐一解決:

(1)一個圓圈

這個我們信手拈來,canvas就有繪制圓的 API,我們在第三節的一小點就講到了

(2)刻度

對于刻度,其實有兩種畫法:

  • 第一種:是聽起來比較 “高大上” ,使用三角函數算出每個刻度的起始坐标和終止坐标,然後進行繪制。
  • 第二種:較為機靈,使用我們在 第二小節的第一點 介紹的

    rotate

    進行一點點的旋轉畫布,然後繪制線。

(3)指針

我們需要先建構下圖中藍色的路徑作為指針,由一段圓弧和兩條線構成。

Canvas中的繪圖師講解與實戰——Android進階UI

建構思路:

第一步:在紅色的矩形内,繪制圓弧(使用了第三小節第四點)

第二步:從圓弧的左點繪制線到圖中紅色頂點

第三部:從紅色頂點繪制線到圓弧右點,最後關閉路徑path

具體代碼如下:

mPointerPath.moveTo(mPointerRadius, 0);
// 第一步
mPointerPath.addArc(mPointerRectF, 0, 180);
// 第二步
mPointerPath.lineTo(0, -width / 4);
// 第三步
mPointerPath.lineTo(mPointerRadius, 0);
mPointerPath.close();
           

(4)開啟旋轉

我們隻需要通過屬性動畫,讓指針動起來即可。而指針的旋轉隻需要通過讓畫布旋轉即可,也就是用到第二小節第一點的

rotate

canvas.save();
canvas.rotate(mCurAngle);

... 省略建立指針

mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mPointerColor);
canvas.drawPath(mPointerPath, mPaint);
canvas.restore();
           

時鐘與指針 完整代碼:傳送門

六、寫在最後

這次介紹的是canvas最為基礎的API操作,但其實越為基礎的東西,越容易被忽略也越是進階中最需要的部分。這次寫的時間耗時較久,主要是API較多,寫demo和截圖比較頻繁。

如果你覺得文章對你有所幫助,請給我一個贊并關注我吧。如果發現有那些欠妥的地方,請留言區與我讨論,我們共同進步。

進階UI系列的Github位址:請進入傳送門,如果喜歡的話給我一個star吧?