Canvas類的最全面詳解 - 自定義View應用系列

weixin_34120274發表於2017-03-06
944365-207a738cb165a2da.png

前言

  • 自定義View是Android開發者必須瞭解的基礎;而Canvas類的使用在自定義View繪製中發揮著非常重要的作用
  • 網上有大量關於自定義View中Canvas類的文章,但存在一些問題:內容不全、思路不清晰、簡單問題複雜化等等
  • 今天,我將全面總結自定義View中的Canvas類的使用,我能保證這是市面上的最全面、最清晰、最易懂的
  1. 文章較長,建議收藏等充足時間再進行閱讀
  2. 閱讀本文前請先閱讀 自定義View基礎 - 最易懂的自定義View原理系列

目錄

944365-f91b9bb80a2239a1.png
目錄

1. 簡介

  • 定義:畫布,是一種繪製時的規則

是安卓平臺2D圖形繪製的基礎

  • 作用:規定繪製內容時的規則 & 內容
  1. 記住:繪製內容是根據畫布的規定繪製在螢幕上的
  2. 理解為:畫布只是繪製時的規則,但內容實際上是繪製在螢幕上的

2. Canvas的本質

請務必記住:

  • 繪製內容是根據畫布(Canvas)的規定繪製在螢幕上的
  • 畫布(Canvas)只是繪製時的規則,但內容實際上是繪製在螢幕上的

為了更好地說明繪製內容的本質和Canvas,請看下面例子:

2.1 例項

  • 例項情況:先畫一個矩形(藍色);然後移動畫布;再畫一個矩形(紅色)
  • 程式碼分析:
         // 畫一個矩形(藍色)
        canvas.drawRect(100, 100, 150, 150, mPaint1);

        // 將畫布的原點移動到(400,500)
        canvas.translate(400,500);

        // 再畫一個矩形(紅色)
        canvas.drawRect(100, 100, 150, 150, mPaint2);
  • 效果圖
944365-5e372c7c4752beda.png
效果圖
  • 具體流程分析
944365-4939288060046151.png
流程分析

看完上述分析,你應該非常明白Canvas的本質了。

  • 總結
    繪製內容是根據畫布的規定繪製在螢幕上的
  1. 內容實際上是繪製在螢幕上;
  2. 畫布,即Canvas,只是規定了繪製內容時的規則;
  3. 內容的位置由座標決定,而座標是相對於畫布而言的

注:關於對畫布的操作(縮放、旋轉和錯切)原理都是相同的,下面會詳細說明。


3. 基礎

3.1 Paint類

  • 定義:畫筆
  • 作用:確定繪製內容的具體效果(如顏色、大小等等)

在繪製內容時需要畫筆Paint

  • 具體使用:

步驟1:建立一個畫筆物件
步驟2:畫筆設定,即設定繪製內容的具體效果(如顏色、大小等等)
步驟3:初始化畫筆(儘量選擇在View的建構函式)

具體使用如下:

// 步驟1:建立一個畫筆
private Paint mPaint = new Paint();

// 步驟2:初始化畫筆
// 根據需求設定畫筆的各種屬性,具體如下:

    private void initPaint() {

        // 設定最基本的屬性
        // 設定畫筆顏色
        // 可直接引入Color類,如Color.red等
        mPaint.setColor(int color); 
        // 設定畫筆模式
         mPaint.setStyle(Style style); 
        // Style有3種型別:
        // 型別1:Paint.Style.FILLANDSTROKE(描邊+填充)
        // 型別2:Paint.Style.FILL(只填充不描邊)
        // 型別3:Paint.Style.STROKE(只描邊不填充)
        // 具體差別請看下圖:
        // 特別注意:前兩種就相差一條邊
        // 若邊細是看不出分別的;邊粗就相當於加粗       
        
        //設定畫筆的粗細
        mPaint.setStrokeWidth(float width)       
        // 如設定畫筆寬度為10px
        mPaint.setStrokeWidth(10f);    

        // 不常設定的屬性
        // 得到畫筆的顏色     
        mPaint.getColor()      
        // 設定Shader
        // 即著色器,定義了圖形的著色、外觀
        // 可以繪製出多彩的圖形
        // 具體請參考文章:http://blog.csdn.net/iispring/article/details/50500106
        Paint.setShader(Shader shader)  

        //設定畫筆的a,r,p,g值
       mPaint.setARGB(int a, int r, int g, int b)      
         //設定透明度
        mPaint.setAlpha(int a)   
       //得到畫筆的Alpha值
        mPaint.getAlpha()        


        // 對字型進行設定(大小、顏色)
        //設定字型大小
          mPaint.setTextSize(float textSize)       

        // 文字Style三種模式:
          mPaint.setStyle(Style style); 
        // 型別1:Paint.Style.FILLANDSTROKE(描邊+填充)
        // 型別2:Paint.Style.FILL(只填充不描邊)
        // 型別3:Paint.Style.STROKE(只描邊不填充) 
        
      // 設定對齊方式   
      setTextAlign()
      // LEFT:左對齊
      // CENTER:居中對齊
      // RIGHT:右對齊

        //設定文字的下劃線
          setUnderlineText(boolean underlineText)      
        
        //設定文字的刪除線
        setStrikeThruText(boolean strikeThruText)    

         //設定文字粗體
        setFakeBoldText(boolean fakeBoldText)  
        
           // 設定斜體
        Paint.setTextSkewX(-0.5f);


        // 設定文字陰影
        Paint.setShadowLayer(5,5,5,Color.YELLOW);
     }

// 步驟3:在建構函式中初始化
    public CarsonView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

Style模式效果如下:

944365-2c250206d0a3829b.png
Style模式效果

3.2 Path類

具體請看我寫的另外一篇文章:Path類的最全面詳解 - 自定義View應用系列

3.3 關閉硬體加速

  • 在Android4.0的裝置上,在開啟硬體加速的情況下,使用自定義View可能會出現問題

具體問題可以看這裡

  • 所以測試前,請先關閉硬體加速。具體關閉方式如下:
    *在AndroidMenifest.xml的application節點新增: *
android:hardwareAccelerated="false"

4. Canvas的使用

4.1 物件建立 & 獲取

Canvas物件 & 獲取的方法有4個:

// 方法1
// 利用空構造方法直接建立物件
Canvas canvas = new Canvas();

// 方法2
// 通過傳入裝載畫布Bitmap物件建立Canvas物件
// CBitmap上儲存所有繪製在Canvas的資訊
Canvas canvas = new Canvas(bitmap)

// 方法3
// 通過重寫View.onDraw()建立Canvas物件
// 在該方法裡可以獲得這個View對應的Canvas物件

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //在這裡獲取Canvas物件
    }

// 方法4
// 在SurfaceView裡畫圖時建立Canvas物件

        SurfaceView surfaceView = new SurfaceView(this);
        // 從SurfaceView的surfaceHolder裡鎖定獲取Canvas
        SurfaceHolder surfaceHolder = surfaceView.getHolder();
        //獲取Canvas
        Canvas c = surfaceHolder.lockCanvas();
        
        // ...(進行Canvas操作)
        // Canvas操作結束之後解鎖並執行Canvas
        surfaceHolder.unlockCanvasAndPost(c);


官方推薦方法4來建立並獲取Canvas,原因:

  • SurfaceView裡有一條執行緒是專門用於畫圖,所以方法4的畫圖效能最好,並適用於高質量的、重新整理頻率高的圖形
  • 而方法3重新整理頻率低於方法3,但系統花銷小,節省資源

4.2 繪製方法使用

  • 利用Canvas類可繪畫出很多內容,如圖形、文字、線條等等;
  • 對應使用的方法如下:

僅列出常用方法,更加詳細的方法可參考官方文件 Canvas

944365-ff8cc50eb32128a8.png
Canvas繪製方法

下面我將逐個方法進行詳細講解

特別注意

Canvas具體使用時是在複寫的onDraw()裡:

  @Override
    protected void onDraw(Canvas canvas){
      
        super.onDraw(canvas);
   
    // 對Canvas進行一系列設定
    //  如畫圓、畫直線等等
   canvas.drawColor(Color.BLUE); 
    // ...
    }

}

具體為什麼,請看我寫的自定義View原理系列文章:
(1)自定義View基礎 - 最易懂的自定義View原理系列
(2)自定義View Measure過程 - 最易懂的自定義View原理系列
(3)自定義View Layout過程 - 最易懂的自定義View原理系列
(4)自定義View Draw過程- 最易懂的自定義View原理系列

4.2.1 繪製顏色

  • 作用:將顏色填充整個畫布,常用於繪製底色
  • 具體使用
    // 傳入一個Color類的常量引數來設定畫布顏色
    // 繪製藍色
   canvas.drawColor(Color.BLUE); 
944365-6664b385824de539.png
效果圖

4.2.2 繪製基本圖形

a. 繪製點(drawPoint)

  • 原理:在某個座標處繪製點

可畫一個點或一組點(多個點)

  • 具體使用

// 特別注意:需要用到畫筆Paint
// 所以之前記得建立畫筆
// 為了區分,這裡使用了兩個不同顏色的畫筆

// 描繪一個點
// 在座標(200,200)處
canvas.drawPoint(300, 300, mPaint1);    

// 繪製一組點,座標位置由float陣列指定
// 此處畫了3個點,位置分別是:(600,500)、(600,600)、(600,700)
canvas.drawPoints(new float[]{         
                600,500,
                600,600,
                600,700
        },mPaint2);
944365-efd689c40b73cf9b.png
效果圖

b. 繪製直線(drawLine)

  • 原理:兩點(初始點 & 結束點)確定一條直線
  • 具體使用:
// 畫一條直線
// 在座標(100,200),(700,200)之間繪製一條直線
   canvas.drawLine(100,200,700,200,mPaint1);

// 繪製一組線
// 在座標(400,500),(500,500)之間繪製直線1
// 在座標(400,600),(500,600)之間繪製直線2
        canvas.drawLines(new float[]{
                400,500,500,500,
                400,600,500,600
        },mPaint2);
    }
944365-31d3be148e1d0069.png
效果圖

c. 繪製矩形(drawRect)

  • 原理:矩形的對角線頂點確定一個矩形

一般是採用左上角和右下角的兩個點的座標。

  • 具體使用
// 關於繪製矩形,Canvas提供了三種過載方法

       // 方法1:直接傳入兩個頂點的座標
       // 兩個頂點座標分別是:(100,100),(800,400)
        canvas.drawRect(100,100,800,400,mPaint);

        // 方法2:將兩個頂點座標封裝為RectRectF
        Rect rect = new Rect(100,100,800,400);
        canvas.drawRect(rect,mPaint);

        // 方法3:將兩個頂點座標封裝為RectF
        RectF rectF = new RectF(100,100,800,400);
        canvas.drawRect(rectF,mPaint);

        // 特別注意:Rect類和RectF類的區別
        // 精度不同:Rect = int & RectF = float

        // 三種方法畫出來的效果是一樣的。
944365-6423d2e5278530df.png
效果圖

d. 繪製圓角矩形

  • 原理:矩形的對角線頂點確定一個矩形

類似於繪製矩形

  • 具體使用
       // 方法1:直接傳入兩個頂點的座標
       // API21時才可使用
       // 第5、6個引數:rx、ry是圓角的引數,下面會詳細描述
       canvas.drawRoundRect(100,100,800,400,30,30,mPaint);
      
        // 方法2:使用RectF類
        RectF rectF = new RectF(100,100,800,400);
        canvas.drawRoundRect(rectF,30,30,mPaint);
      
944365-08bc785093daef5a.png
效果圖
  • 與矩形相比,圓角矩形多了兩個引數rx 和 ry
  • 圓角矩形的角是橢圓的圓弧,rx 和 ry實際上是橢圓的兩個半徑,如下圖:
944365-a2d54cc88fac1fcb.png
橢圓示意圖
  • 特別注意:當 rx大於寬度的一半, ry大於高度一半 時,畫出來的為橢圓

實際上,在rx為寬度的一半,ry為高度的一半時,剛好是一個橢圓;但由於當rx大於寬度一半,ry大於高度一半時,無法計算出圓弧,所以drawRoundRect對大於該數值的引數進行了修正,凡是大於一半的引數均按照一半來處理

944365-788151672bac01ce.png
效果圖

e. 繪製橢圓

  • 原理:矩形的對角線頂點確定矩形,根據傳入矩形的長寬作為長軸和短軸畫橢圓
  1. 橢圓傳入的引數和矩形是一樣的;
  2. 繪製橢圓實際上是繪製一個矩形的內切圖形。
  • 具體使用
        // 方法1:使用RectF類
        RectF rectF = new RectF(100,100,800,400);
        canvas.drawOval(rectF,mPaint);

        // 方法2:直接傳入與矩形相關的引數
        canvas.drawOval(100,100,800,400,mPaint);

        // 為了方便表示,畫一個和橢圓一樣引數的矩形
         canvas.drawRect(100,100,800,400,mPaint);
944365-4c4404e53ab8e052.png
效果圖

f. 繪製圓

  • 原理:圓心座標+半徑決定圓
  • 具體使用
// 引數說明:
// 1、2:圓心座標
// 3:半徑
// 4:畫筆

// 繪製一個圓心座標在(500,500),半徑為400 的圓。
    canvas.drawCircle(500,500,400,mPaint);  

944365-0f27a18b3343b160.png
具體使用

g. 繪製圓弧

  • 原理:通過圓弧角度的起始位置和掃過的角度確定圓弧
  • 具體使用
// 繪製圓弧共有兩個方法
// 相比於繪製橢圓,繪製圓弧多了三個引數:
startAngle  // 確定角度的起始位置
sweepAngle // 確定掃過的角度
useCenter   // 是否使用中心(下面會詳細說明)

// 方法1
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint){}

// 方法2
public void drawArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean useCenter, @NonNull Paint paint) {}



為了理解第三個引數:useCenter,看以下示例:

// 以下示例:繪製兩個起始角度為0度、掃過90度的圓弧
// 兩者的唯一區別就是是否使用了中心點

    // 繪製圓弧1(無使用中心)
        RectF rectF = new RectF(100, 100, 800,400);
        // 繪製背景矩形
        canvas.drawRect(rectF, mPaint1);
        // 繪製圓弧
        canvas.drawArc(rectF, 0, 90, false, mPaint2);

   // 繪製圓弧2(使用中心)
        RectF rectF2 = new RectF(100,600,800,900);
        // 繪製背景矩形
        canvas.drawRect(rectF2, mPaint1);
        // 繪製圓弧
        canvas.drawArc(rectF2,0,90,true,mPaint2);
944365-bb94a463cd2f5c27.png
效果圖

從示例可以發現:

  • 不使用中心點:圓弧的形狀 = (起、止點連線+圓弧)構成的面積
  • 使用中心店:圓弧面積 = (起點、圓心連線 + 止點、圓心連線+圓弧)構成的面積

類似扇形

4.2.3 繪製文字

繪製文字分為三種應用場景:

  • 情況1:指定文字開始的位置
  1. 即指定文字基線位置
  2. 基線x預設在字串左側,基線y預設在字串下方
  • 情況2:指定每個文字的位置
  • 情況3:指定路徑,並根據路徑繪製文字

下面分別細說:

文字的樣式(大小,顏色,字型等)具體由畫筆Paint控制,詳細請會看上面基礎的介紹

情況1:指定文字開始的位置

// 引數text:要繪製的文字
// 引數x,y:指定文字開始的位置(座標)

// 引數paint:設定的畫筆屬性
    public void drawText (String text, float x, float y, Paint paint)

// 例項
canvas.drawText("abcdefg",300,400,mPaint1);



// 僅繪製文字的一部分
// 引數start,end:指定繪製文字的位置
// 位置以下標標識,由0開始
    public void drawText (String text, int start, int end, float x, float y, Paint paint)
    public void drawText (CharSequence text, int start, int end, float x, float y, Paint paint)

// 對於字元陣列char[]
// 擷取文字使用起始位置(index)和長度(count)
    public void drawText (char[] text, int index, int count, float x, float y, Paint paint)

// 例項:繪製從位置1-3的文字
canvas.drawText("abcdefg",1,4,300,400,mPaint1);

        // 字元陣列情況
        // 字元陣列(要繪製的內容)
        char[] chars = "abcdefg".toCharArray();

        // 引數為 (字元陣列 起始座標 擷取長度 基線x 基線y 畫筆)
        canvas.drawText(chars,1,3,200,500,textPaint);
        // 效果同上
944365-a4994282812d548e.png
效果圖

情況2:分別指定文字的位置

// 引數text:繪製的文字
// 引數pos:陣列型別,存放每個字元的位置(座標)
// 注意:必須指定所有字元位置
 public void drawPosText (String text, float[] pos, Paint paint)

// 對於字元陣列char[],可以擷取部分文字進行繪製
// 擷取文字使用起始位置(index)和長度(count)
    public void drawPosText (char[] text, int index, int count, float[] pos, Paint paint)

// 特別注意:
// 1. 在字元數量較多時,使用會導致卡頓
// 2. 不支援emoji等特殊字元,不支援字形組合與分解

  // 例項
  canvas.drawPosText("abcde", new float[]{
                100, 100,    // 第一個字元位置
                200, 200,    // 第二個字元位置
                300, 300,    // ...
                400, 400,
                500, 500
        }, mPaint1);




// 陣列情況(繪製部分文字)
       char[] chars = "abcdefg".toCharArray();

        canvas.drawPosText(chars, 1, 3, new float[]{
                300, 300,    // 指定的第一個字元位置
                400, 400,    // 指定的第二個字元位置
                500, 500,    // 指定的第三個字元位置

        }, mPaint1);
944365-919c352d2548397a.png
效果圖

情況3:指定路徑,並根據路徑繪製文字
關於Path類的使用請看我寫的文章具體請看我寫的另外一篇文章:Path類的最全面詳解 - 自定義View應用系列

       
 // 在路徑(540,750,640,450,840,600)寫上"在Path上寫的字:Carson_Ho"字樣
        // 1.建立路徑物件
        Path path = new Path();
        // 2. 設定路徑軌跡
        path.cubicTo(540, 750, 640, 450, 840, 600);
         // 3. 畫路徑
        canvas.drawPath(path,mPaint2);
        // 4. 畫出在路徑上的字
        canvas.drawTextOnPath("在Path上寫的字:Carson_Ho", path, 50, 0, mPaint2);
      
944365-298dfa377797653d.png
效果圖

4.2.4 繪製圖片

繪製圖片分為:繪製向量圖(drawPicture)和 繪製點陣圖(drawBitmap)

a. 繪製向量圖(drawPicture)

  • 作用:繪製向量圖的內容,即繪製儲存在向量圖裡某個時刻Canvas繪製內容的操作

向量圖(Picture)的作用:儲存(錄製)某個時刻Canvas繪製內容的操作

  • 應用場景:繪製之前繪製過的內容
  1. 相比於再次呼叫各種繪圖API,使用Picture能節省操作 & 時間
  2. 如果不手動呼叫,錄製的內容不會顯示在螢幕上,只是儲存起來

特別注意:使用繪製向量圖時前請關閉硬體加速,以免引起不必要的問題!

具體使用方法:

// 獲取寬度
Picture.getWidth ();

// 獲取高度
Picture.getHeight ()

// 開始錄製 
// 即將Canvas中所有的繪製內容儲存到Picture中
// 返回一個Canvas
Picture.beginRecording(int width, int height)

// 結束錄製
Picture.endRecording ()

// 將Picture裡的內容繪製到Canvas中
Picture.draw (Canvas canvas)

// 還有兩種方法可以將Picture裡的內容繪製到Canvas中
// 方法2:Canvas.drawPicture()
// 方法3:將Picture包裝成為PictureDrawable,使用PictureDrawable的draw方法繪製。

// 下面會詳細介紹

一般使用的具體步驟

// 步驟1:建立Picture物件
Picture mPicture = new Picture();

// 步驟2:開始錄製 
mPicture.beginRecording(int width, int height);

// 步驟3:繪製內容 or 操作Canvas
canvas.drawCircle(500,500,400,mPaint);
...(一系列操作)

// 步驟4:結束錄製
mPicture.endRecording ();

步驟5:某個時刻將儲存在Picture的繪製內容繪製出來
mPicture.draw (Canvas canvas);

下面我將用一個例項去表示如何去使用:

  • 例項介紹
    將座標系移動到(450,650);繪製一個圓,將上述Canvas操作錄製下來,並在某個時刻重新繪製出來。

步驟1:建立Picture物件

Picture mPicture = new Picture();

步驟2:開始錄製

Canvas recordingCanvas = mPicture.beginRecording(500, 500);

// 注:要建立Canvas物件來接收beginRecording()返回的Canvas物件

步驟3:繪製內容 or 操作Canvas

        // 位移
        // 將座標系的原點移動到(450,650)
        recordingCanvas.translate(450,650);

        // 記得先建立一個畫筆
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.FILL);

        // 繪製一個圓
        // 圓心為(0,0),半徑為100
       recordingCanvas.drawCircle(0,0,100,paint);

步驟4:結束錄製

mPicture.endRecording();

步驟5:將儲存在Picture的繪製內容繪製出來

有三種方法:

  • Picture.draw (Canvas canvas)
  • Canvas.drawPicture()
  • PictureDrawable.draw()

將Picture包裝成為PictureDrawable

主要區別如下:

944365-6dade27d271be15e.png
Paste_Image.png

方法1:Picture提供的draw()

// 在複寫的onDraw()裡
  @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);

        // 將錄製的內容顯示在當前畫布裡
        mPicture.draw(canvas);

// 注:此方法繪製後可能會影響Canvas狀態,不建議使用
 }


944365-e719adaac784946b.png
效果圖

方法2:Canvas提供的drawPicture()

不會影響Canvas狀態

// 提供了三種方法
// 方法1
public void drawPicture (Picture picture)
// 方法2
// Rect dst代表顯示的區域
// 若區域小於圖形,繪製的內容根據選區進行縮放
public void drawPicture (Picture picture, Rect dst)

// 方法3
public void drawPicture (Picture picture, RectF dst)

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

        // 例項1:將錄製的內容顯示(區域剛好佈滿圖形)
        canvas.drawPicture(mPicture, new RectF(0, 0, mPicture.getWidth(), mPicture.getHeight()));

        // 例項2:將錄製的內容顯示在當前畫布上(區域小於圖形)
        canvas.drawPicture(mPicture, new RectF(0, 0, mPicture.getWidth(), 200));


944365-37ff0186a7092f6e.png
效果圖

方法3:使用PictureDrawable的draw方法繪製

將Picture包裝成為PictureDrawable


 @Override
    protected void onDraw(Canvas canvas){

        super.onDraw(canvas);

        // 將錄製的內容顯示出來

        // 將Picture包裝成為Drawable
        PictureDrawable drawable = new PictureDrawable(mPicture);

        // 設定在畫布上的繪製區域(類似drawPicture (Picture picture, Rect dst)的Rect dst引數)
        // 每次都從Picture的左上角開始繪製
        // 並非根據該區域進行縮放,也不是剪裁Picture。

        // 例項1:將錄製的內容顯示(區域剛好佈滿圖形)
        drawable.setBounds(0, 0,mPicture.getWidth(), mPicture.getHeight());

        // 繪製
        drawable.draw(canvas);


        // 例項2:將錄製的內容顯示在當前畫布上(區域小於圖形)
        drawable.setBounds(0, 0,250, mPicture.getHeight());
944365-61964d6ba1c51612.png
效果圖

b. 繪製點陣圖(drawBitmap)

  • 作用:將已有的圖片轉換為點陣圖(Bitmap),最後再繪製到Canvas上

點陣圖,即平時我們使用的圖片資源

獲取Bitmap物件的方式

要繪製Bitmap,就要先獲取一個Bitmap物件,具體獲取方式如下:

944365-471a8ae2ba6ab7a9.png
獲取Bitmap物件方式

特別注意:繪製點陣圖(Bitmap)是讀取已有的圖片轉換為Bitmap,最後再繪製到Canvas。

所以:

  • 對於第1種方式:排除
  • 對於第2種方式:雖然滿足需求,但一般不推薦使用

具體請自行了解關於Drawble的內容

  • 對於第3種方式:滿足需求,下面會著重講解

通過BitmapFactory獲取Bitmap (從不同位置獲取):

// 共3個位置:資原始檔、記憶體卡、網路

// 位置1:資原始檔(drawable/mipmap/raw)
        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),R.raw.bitmap);

// 位置2:資原始檔(assets)
        Bitmap bitmap=null;
        try {
            InputStream is = mContext.getAssets().open("bitmap.png");
            bitmap = BitmapFactory.decodeStream(is);
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

// 位置3:記憶體卡檔案
    Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/bitmap.png");

// 位置4:網路檔案:
// 省略了獲取網路輸入流的程式碼
        Bitmap bitmap = BitmapFactory.decodeStream(is);
        is.close();

繪製Bitmap

繪製Bitmap共有四種方法:


// 方法1
    public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)

 // 方法2
    public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)

// 方法3
    public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)

// 方法4
    public void drawBitmap (Bitmap bitmap, Rect src, RectF dst, Paint paint)

// 下面詳細說

方法1

 public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)

// 後兩個引數matrix, paint是在繪製時對圖片進行一些改變
// 後面會專門說matrix
  
// 如果只是將圖片內容繪製出來只需將傳入新建的matrix, paint物件即可:
  canvas.drawBitmap(bitmap,new Matrix(),new Paint());
// 記得選取一種獲取Bitmap的方式
// 注:圖片左上角位置預設為座標原點。
944365-e0eb1fdd7f79e987.png
效果圖

方法2

// 引數 left、top指定了圖片左上角的座標(距離座標原點的距離):
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)

 canvas.drawBitmap(bitmap,300,400,new Paint());

944365-69bfe74957add3b5.png
Paste_Image.png

方法3

 public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
// 引數(src,dst) = 兩個矩形區域
// Rect src:指定需要繪製圖片的區域(即要繪製圖片的哪一部分)
// Rect dst 或RectF dst:指定圖片在螢幕上顯示(繪製)的區域
// 下面我將用例項來說明

// 例項
 // 指定圖片繪製區域
        // 僅繪製圖片的二分之一
        Rect src = new Rect(0,0,bitmap.getWidth()/2,bitmap.getHeight());

        // 指定圖片在螢幕上顯示的區域
        Rect dst = new Rect(100,100,250,250);

        // 繪製圖片
        canvas.drawBitmap(bitmap,src,dst,null);

// 下面我們一步步分析:
944365-f36ea85f6a140a65.png
分析

特別注意的是:如果src規定繪製圖片的區域大於dst指定顯示的區域的話,那麼圖片的大小會被縮放。

方法3的應用場景:

  • 便於素材管理
    當我需要畫很多個圖時,如果1張圖=1個素材的話,那麼管理起來很不方便;如果素材都放在一個圖,那麼按需繪製會便於管理


    944365-723a1255e9625937.png
    Paste_Image.png
  • 實現動態效果
    動態效果 = 逐漸繪製圖形部分,如下:

944365-a05fc6cf05056c87.gif
動態效果圖

在繪製時,只需要一個資原始檔,然後逐漸描繪就可以


944365-1fc8aa5ee631e58b.png
資原始檔

繪製過程如下:


944365-b0d59f897573c931.png
描繪過程

4.2.5 繪製路徑

// 通過傳入具體路徑Path物件 & 畫筆
canvas.drawPath(mPath, mPaint)

關於Path類的使用,具體請看我寫的另外一篇文章:Path類的最全面詳解 - 自定義View應用系列

4.2.6 畫布操作

  • 作用:改變畫布的性質

改變之後,任何的後續操作都會受到影響

A. 畫布變換

a. 平移(translate)

  • 作用:移動畫布(實際上是移動座標系,如下圖)
  • 具體使用

// 將畫布原點向右移200px,向下移100px
canvas.translate(200, 100)  
// 注:位移是基於當前位置移動,而不是每次都是基於螢幕左上角的(0,0)點移動
944365-0248517920c57f36.png
效果圖

b. 縮放(scale)

  • 作用:放大 / 縮小 畫布的倍數
  • 具體使用:
// 共有兩個方法
// 方法1
// 以(px,py)為中心,在x方向縮放sx倍,在y方向縮放sy倍
// 縮放中心預設為(0,0)
public final void scale(float sx, float sy)     

// 方法2
// 比方法1多了兩個引數(px,py),用於控制縮放中心位置
// 縮放中心為(px,py)
 public final void scale (float sx, float sy, float px, float py)

我將用下面的例子說明縮放的使用和縮放中心的意義。

// 例項:畫兩個對比圖
// 相同:都有兩個矩形,第1個= 正常大小,第2個 = 放大1.5倍 
// 不同點:第1個縮放中心在(0,0),第2個在(px,py)

// 第一個圖
  // 設定矩形大小
        RectF rect = new RectF(0,-200,200,0);

         // 繪製矩形(藍色)
        canvas.drawRect(rect, mPaint1);

        // 將畫布放大到1.5倍
        // 不移動縮放中心,即縮放中心預設為(0,0)
        canvas.scale(1.5f, 1.5f);
        // 繪製放大1.5倍後的藍色矩形(紅色)
        canvas.drawRect(rect,mPaint2);

// 第二個圖      
         // 設定矩形大小
        RectF rect = new RectF(0,-200,200,0);   

         // 繪製矩形(藍色)
        canvas.drawRect(rect, mPaint1);
       
        // 將畫布放大到1.5倍,並將縮放中心移動到(100,0)
        canvas.scale(1.5f, 1.5f, 100,0);              
        // 繪製放大1.5倍後的藍色矩形(紅色)
        canvas.drawRect(rect,mPaint2);
       
// 縮放的本質是:把形狀先畫到畫布,然後再縮小/放大。所以當放大倍數很大時,會有明顯鋸齒

944365-1802bb1d0a464bcb.png
效果圖

當縮放倍數為負數時,會先進行縮放,然後根據不同情況進行圖形翻轉

(設縮放倍數為(a,b),旋轉中心為(px,py)):

  1. a<0,b>0:以px為軸翻轉
  2. a>0,b<0:以py為軸翻轉
  3. a<0,b<0:以旋轉中心翻轉

具體如下圖:(縮放倍數為1.5,旋轉中心為(0,0)為例)

944365-024ca0347bfff54c.png
Paste_Image.png

c. 旋轉(rotate)

注意:角度增加方向為順時針(區別於數學座標系)

944365-50d76313f955b091.png
與數學座標系對比

// 方法1
// 以原點(0,0)為中心旋轉 degrees 度
public final void rotate(float degrees)  
  // 以原點(0,0)為中心旋轉 90 度
canvas.rotate(90);

// 方法2
// 以(px,py)點為中心旋轉degrees度
public final void rotate(float degrees, float px, float py)  
// 以(30,50)為中心旋轉 90 度
canvas.rotate(90,30,50);                

            
944365-93fcdc0780a04145.png
效果圖

d. 錯切(skew)

  • 作用:將畫布在x方向傾斜a角度、在y方向傾斜b角度
  • 具體使用:
// 引數 sx = tan a ,sx>0時表示向X正方向傾斜(即向左)
// 引數 sy = tan b ,sy>0時表示向Y正方向傾斜(即向下)
public void skew(float sx, float sy)   


// 例項
   // 為了方便觀察,我將座標系移到螢幕中央
        canvas.translate(300, 500);
        // 初始矩形
        canvas.drawRect(20, 20, 400, 200, mPaint2);

        // 向X正方向傾斜45度
        canvas.skew(1f, 0);
        canvas.drawRect(20, 20, 400, 200, mPaint1);
        
        //向X負方向傾斜45度
        canvas.skew(-1f, 0);
        canvas.drawRect(20, 20, 400, 200, mPaint1);
        
        // 向Y正方向傾斜45度
        canvas.skew(0, 1f);
        canvas.drawRect(20, 20, 400, 200, mPaint1);

       // 向Y負方向傾斜45度
        canvas.skew(0, -1f);
        canvas.drawRect(20, 20, 400, 200, mPaint1);

944365-edc7ed0e918c8aa4.png
效果圖

B. 畫布裁剪

即從畫布上裁剪一塊區域,之後僅能編輯該區域

特別注意:其餘的區域只是不能編輯,但是並沒有消失,如下圖

944365-958baa734c8197ad.png
Paste_Image.png
裁剪共分為:裁剪路徑、裁剪矩形、裁剪區域

// 裁剪路徑
// 方法1
public boolean clipPath(@NonNull Path path)
// 方法2
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op)


// 裁剪矩形
// 方法1
public boolean clipRect(int left, int top, int right, int bottom)
// 方法2
public boolean clipRect(float left, float top, float right, float bottom)
// 方法3
public boolean clipRect(float left, float top, float right, float bottom,
            @NonNull Region.Op op) 

// 裁剪區域
// 方法1
public boolean clipRegion(@NonNull Region region)
// 方法2
public boolean clipRegion(@NonNull Region region, @NonNull Region.Op op)

這裡特別說明一下引數Region.Op op
作用:在剪下多個區域下來的情況,當這些區域有重疊的時候,這個引數決定重疊部分該如何處理,多次裁剪之後究竟獲得了哪個區域,有以下幾種引數:

944365-33ea3e07ef4659a0.png
Paste_Image.png

以三個引數為例講解:
Region.Op.DIFFERENCE:顯示第一次裁剪與第二次裁剪不重疊的區域

944365-b9c272b158d7fb1e.png
Paste_Image.png
   // 為了方便觀察,我將座標系移到螢幕中央
        canvas.translate(300, 500);

        //原來畫布設定為灰色
        canvas.drawColor(Color.GRAY);

        //第一次裁剪
        canvas.clipRect(0, 0, 600, 600);

        //將第一次裁剪後的區域設定為紅色
        canvas.drawColor(Color.RED);

        //第二次裁剪,並顯示第一次裁剪與第二次裁剪不重疊的區域
        canvas.clipRect(0, 200, 600, 400, Region.Op.DIFFERENCE);

        //將第一次裁剪與第二次裁剪不重疊的區域設定為黑色
        canvas.drawColor(Color.BLACK);

Region.Op.REPLACE:顯示第二次裁剪的區域

944365-b2741accd3b47952.png
     //原來畫布設定為灰色)
        canvas.drawColor(Color.GRAY);

        //第一次裁剪
        canvas.clipRect(0, 0, 600, 600);

        //將第一次裁剪後的區域設定為紅色
        canvas.drawColor(Color.RED);

        //第二次裁剪,並顯示第二次裁剪的區域
        canvas.clipRect(0, 200, 600, 400, Region.Op.REPLACE);

        //將第二次裁剪的區域設定為藍色
        canvas.drawColor(Color.BLUE);

Region.Op.INTERSECT:顯示第二次與第一次的重疊區域

944365-dc72186158cfe5d2.png
Paste_Image.png
//原來畫布設定為灰色)
        canvas.drawColor(Color.GRAY);

        //第一次裁剪
        canvas.clipRect(0, 0, 600, 600);

        //將第一次裁剪後的區域設定為紅色
        canvas.drawColor(Color.RED);

        //第二次裁剪,並顯示第一次裁剪與第二次裁剪重疊的區域
        canvas.clipRect(-100, 200, 600, 400, Region.Op.INTERSECT);

        //將第一次裁剪與第二次裁剪重疊的區域設定為黑色
        canvas.drawColor(Color.BLACK);

關於其他引數,較為簡單,此處不作過多展示。

C. 畫布快照

這裡先理清幾個概念

  • 畫布狀態:當前畫布經過的一系列操作
  • 狀態棧:存放畫布狀態和圖層的棧(後進先出)


    944365-94c10f0731911bea.png
    狀態棧
  • 畫布的構成:由多個圖層構成,如下圖
  1. 在畫布上操作 = 在圖層上操作
  2. 如無設定,繪製操作和畫布操作是預設在預設圖層上進行
  3. 在通常情況下,使用預設圖層就可滿足需求;若需要繪製複雜的內容(如地圖),則需使用更多的圖層
  4. 最終顯示的結果 = 所有圖層疊在一起的效果
944365-9aac96190bc0a533.png
畫布構成 - 圖層

a. 儲存當前畫布狀態(save)

  • 作用:儲存畫布狀態(即儲存畫布的一系列操作)
  • 應用場景:畫布的操作是不可逆的,而且會影響後續的步驟,假如需要回到之前畫布的狀態去進行下一次操作,就需要對畫布的狀態進行儲存和回滾

// 方法1:
  // 儲存全部狀態
  public int save ()

// 方法2:
  // 根據saveFlags引數儲存一部分狀態
  // 使用該引數可以只儲存一部分狀態,更加靈活
  public int save (int saveFlags)

// saveFlags引數說明:
// 1.ALL_SAVE_FLAG(預設):儲存全部狀態
// 2. CLIP_SAVE_FLAG:儲存剪輯區
// 3. CLIP_TO_LAYER_SAVE_FLAG:剪裁區作為圖層儲存
// 4. FULL_COLOR_LAYER_SAVE_FLAG:儲存圖層的全部色彩通道
// 5. HAS_ALPHA_LAYER_SAVE_FLAG:儲存圖層的alpha(不透明度)通道
// 6. MATRIX_SAVE_FLAG:儲存Matrix資訊(translate, rotate, scale, skew)

// 每呼叫一次save(),都會在棧頂新增一條狀態資訊(入棧)
944365-94c10f0731911bea.png
入棧

b. 儲存某個圖層狀態(saveLayer)

  • 作用:新建一個圖層,並放入特定的棧中
  • 具體使用

使用起來非常複雜,因為圖層之間疊加會導致計算量成倍增長,營儘量避免使用。

// 無圖層alpha(不透明度)通道
public int saveLayer (RectF bounds, Paint paint)
public int saveLayer (RectF bounds, Paint paint, int saveFlags)
public int saveLayer (float left, float top, float right, float bottom, Paint paint)
public int saveLayer (float left, float top, float right, float bottom, Paint paint, int saveFlags)

// 有圖層alpha(不透明度)通道
public int saveLayerAlpha (RectF bounds, int alpha)
public int saveLayerAlpha (RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha, int saveFlags)

c. 回滾上一次儲存的狀態(restore)

  • 作用:恢復上一次儲存的畫布狀態
  • 具體使用

// 採取狀態棧的形式。即從棧頂取出一個狀態進行恢復。
canvas.restore();
944365-94c10f0731911bea.png
效果圖

d. 回滾指定儲存的狀態(restoreToCount)

  • 作用:恢復指定狀態;將指定位置以及以上所有狀態出棧
  • 具體使用:
 canvas.restoreToCount(3) ;
// 彈出 3、4、5的狀態,並恢復第3次儲存的畫布狀態
944365-f9cd2f1d679520f6.png
效果圖

e. 獲取儲存的次數(getSaveCount)

  • 作用:獲取儲存過圖層的次數

即獲取狀態棧中儲存狀態的數量

canvas.getSaveCount();
// 以上面棧為例,則返回5
// 注:即使彈出所有的狀態,返回值依舊為1,代表預設狀態。(返回值最小為1)

總結

對於畫布狀態的儲存和回滾的套路,一般如下:

 // 步驟1:儲存當前狀態
//  把Canvas的當前狀態資訊入棧
 save();     

 // 步驟2:對畫布進行各種操作(旋轉、平移Blabla)
   ...      

 // 步驟3:回滾到之前的畫布狀態
  // 把棧裡面的資訊出棧,取代當前的Canvas資訊
   restore();  

5. 總結


請點贊!因為你們的贊同/鼓勵是我寫作的最大動力!

相關文章閱讀
Android開發:最全面、最易懂的Android螢幕適配解決方案
Android開發:史上最全的Android訊息推送解決方案
Android開發:最全面、最易懂的Webview詳解
Android開發:JSON簡介及最全面解析方法!
Android四大元件:Service服務史上最全面解析
Android四大元件:BroadcastReceiver史上最全面解析


歡迎關注Carson_Ho的簡書!

不定期分享關於安卓開發的乾貨,追求短、平、快,但卻不缺深度

944365-9b76fa3c52d478a7.png

相關文章