Carson帶你學(xué)Android :自定義View Canvas類全面解析


前言

  • 自定義View是Android開發(fā)者必須了解的基礎(chǔ);而Canvas類的使用在自定義View繪制中發(fā)揮著非常重要的作用
  • 網(wǎng)上有大量關(guān)于自定義View中Canvas類的文章,但存在一些問題:內(nèi)容不全、思路不清晰、簡單問題復(fù)雜化等等
  • 今天,我將全面總結(jié)自定義View中的Canvas類的使用,我能保證這是市面上的最全面、最清晰、最易懂的

Carson帶你學(xué)Android自定義View文章系列:
Carson帶你學(xué)Android:自定義View基礎(chǔ)
Carson帶你學(xué)Android:一文梳理自定義View工作流程
Carson帶你學(xué)Android:自定義View繪制準(zhǔn)備-DecorView創(chuàng)建
Carson帶你學(xué)Android:自定義View Measure過程
Carson帶你學(xué)Android:自定義View Layout過程
Carson帶你學(xué)Android:自定義View Draw過程
Carson帶你學(xué)Android:手把手教你寫一個(gè)完整的自定義View
Carson帶你學(xué)Android:Canvas類全面解析
Carson帶你學(xué)Android:Path類全面解析


目錄

示意圖

1. 簡介

  • 定義:畫布,是一種繪制時(shí)的規(guī)則

是安卓平臺(tái)2D圖形繪制的基礎(chǔ)

  • 作用:規(guī)定繪制內(nèi)容時(shí)的規(guī)則 & 內(nèi)容
  1. 記?。豪L制內(nèi)容是根據(jù)畫布的規(guī)定繪制在屏幕上的
  2. 理解為:畫布只是繪制時(shí)的規(guī)則,但內(nèi)容實(shí)際上是繪制在屏幕上的

2. 本質(zhì)

請(qǐng)務(wù)必記住:

  • 繪制內(nèi)容是根據(jù)畫布(Canvas)的規(guī)定繪制在屏幕上的
  • 畫布(Canvas)只是繪制時(shí)的規(guī)則,但內(nèi)容實(shí)際上是繪制在屏幕上的

為了更好地說明繪制內(nèi)容的本質(zhì)和Canvas,請(qǐng)看下面例子:

2.1 實(shí)例

  • 實(shí)例情況:先畫一個(gè)矩形(藍(lán)色);然后移動(dòng)畫布;再畫一個(gè)矩形(紅色)
  • 代碼分析:
// 畫一個(gè)矩形(藍(lán)色)
canvas.drawRect(100, 100, 150, 150, mPaint1);

// 將畫布的原點(diǎn)移動(dòng)到(400,500)
canvas.translate(400,500);

// 再畫一個(gè)矩形(紅色)
canvas.drawRect(100, 100, 150, 150, mPaint2);
  • 效果圖
效果圖
  • 具體流程分析
流程分析

看完上述分析,你應(yīng)該非常明白Canvas的本質(zhì)了。

  • 總結(jié)
    繪制內(nèi)容是根據(jù)畫布的規(guī)定繪制在屏幕上的
  1. 內(nèi)容實(shí)際上是繪制在屏幕上;
  2. 畫布,即Canvas,只是規(guī)定了繪制內(nèi)容時(shí)的規(guī)則;
  3. 內(nèi)容的位置由坐標(biāo)決定,而坐標(biāo)是相對(duì)于畫布而言的

注:關(guān)于對(duì)畫布的操作(縮放、旋轉(zhuǎn)和錯(cuò)切)原理都是相同的,下面會(huì)詳細(xì)說明。


3. 基礎(chǔ)

3.1 Paint類

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

在繪制內(nèi)容時(shí)需要畫筆Paint

  • 具體使用:

步驟1:創(chuàng)建一個(gè)畫筆對(duì)象
步驟2:畫筆設(shè)置,即設(shè)置繪制內(nèi)容的具體效果(如顏色、大小等等)
步驟3:初始化畫筆(盡量選擇在View的構(gòu)造函數(shù))

具體使用如下:

// 步驟1:創(chuàng)建一個(gè)畫筆
private Paint mPaint = new Paint();

// 步驟2:初始化畫筆
// 根據(jù)需求設(shè)置畫筆的各種屬性,具體如下:

    private void initPaint() {

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

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

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


        // 對(duì)字體進(jìn)行設(shè)置(大小、顏色)
        //設(shè)置字體大小
          mPaint.setTextSize(float textSize)       

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

        //設(shè)置文本的下劃線
          setUnderlineText(boolean underlineText)      
        
        //設(shè)置文本的刪除線
        setStrikeThruText(boolean strikeThruText)    

         //設(shè)置文本粗體
        setFakeBoldText(boolean fakeBoldText)  
        
           // 設(shè)置斜體
        Paint.setTextSkewX(-0.5f);


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

// 步驟3:在構(gòu)造函數(shù)中初始化
    public CarsonView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

Style模式效果如下:

Style模式效果

3.2 Path類

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

3.3 關(guān)閉硬件加速

  • 在Android4.0的設(shè)備上,在打開硬件加速的情況下,使用自定義View可能會(huì)出現(xiàn)問題

具體問題可以看這里。

  • 所以測試前,請(qǐng)先關(guān)閉硬件加速
  • 具體關(guān)閉方式:在AndroidMenifest.xml的application節(jié)點(diǎn)添加
android:hardwareAccelerated="false"

4. Canvas的使用

4.1 對(duì)象創(chuàng)建 & 獲取

Canvas對(duì)象 & 獲取的方法有4個(gè):

// 方法1
// 利用空構(gòu)造方法直接創(chuàng)建對(duì)象
Canvas canvas = new Canvas();

// 方法2
// 通過傳入裝載畫布Bitmap對(duì)象創(chuàng)建Canvas對(duì)象
// CBitmap上存儲(chǔ)所有繪制在Canvas的信息
Canvas canvas = new Canvas(bitmap)

// 方法3
// 通過重寫View.onDraw()創(chuàng)建Canvas對(duì)象
// 在該方法里可以獲得這個(gè)View對(duì)應(yīng)的Canvas對(duì)象

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //在這里獲取Canvas對(duì)象
    }

// 方法4
// 在SurfaceView里畫圖時(shí)創(chuàng)建Canvas對(duì)象

        SurfaceView surfaceView = new SurfaceView(this);
        // 從SurfaceView的surfaceHolder里鎖定獲取Canvas
        SurfaceHolder surfaceHolder = surfaceView.getHolder();
        //獲取Canvas
        Canvas c = surfaceHolder.lockCanvas();
        
        // ...(進(jìn)行Canvas操作)
        // Canvas操作結(jié)束之后解鎖并執(zhí)行Canvas
        surfaceHolder.unlockCanvasAndPost(c);

官方推薦方法4來創(chuàng)建并獲取Canvas,原因:

  • SurfaceView里有一條線程是專門用于畫圖,所以方法4的畫圖性能最好,并適用于高質(zhì)量的、刷新頻率高的圖形
  • 而方法3刷新頻率低于方法3,但系統(tǒng)花銷小,節(jié)省資源

4.2 繪制方法使用

  • 利用Canvas類可繪畫出很多內(nèi)容,如圖形、文字、線條等等;
  • 對(duì)應(yīng)使用的方法如下:

僅列出常用方法,更加詳細(xì)的方法可參考官方文檔 Canvas

Canvas繪制方法

下面我將逐個(gè)方法進(jìn)行詳細(xì)講解

特別注意

Canvas具體使用時(shí)是在復(fù)寫的onDraw()里:

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

}

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

4.2.1 繪制顏色

  • 作用:將顏色填充整個(gè)畫布,常用于繪制底色
  • 具體使用
    // 傳入一個(gè)Color類的常量參數(shù)來設(shè)置畫布顏色
    // 繪制藍(lán)色
   canvas.drawColor(Color.BLUE); 
效果圖

4.2.2 繪制基本圖形

a. 繪制點(diǎn)(drawPoint)

  • 原理:在某個(gè)坐標(biāo)處繪制點(diǎn)

可畫一個(gè)點(diǎn)或一組點(diǎn)(多個(gè)點(diǎn))

  • 具體使用

// 特別注意:需要用到畫筆Paint
// 所以之前記得創(chuàng)建畫筆
// 為了區(qū)分,這里使用了兩個(gè)不同顏色的畫筆

// 描繪一個(gè)點(diǎn)
// 在坐標(biāo)(200,200)處
canvas.drawPoint(300, 300, mPaint1);    

// 繪制一組點(diǎn),坐標(biāo)位置由float數(shù)組指定
// 此處畫了3個(gè)點(diǎn),位置分別是:(600,500)、(600,600)、(600,700)
canvas.drawPoints(new float[]{         
                600,500,
                600,600,
                600,700
        },mPaint2);
效果圖

b. 繪制直線(drawLine)

  • 原理:兩點(diǎn)(初始點(diǎn) & 結(jié)束點(diǎn))確定一條直線
  • 具體使用:
// 畫一條直線
// 在坐標(biāo)(100,200),(700,200)之間繪制一條直線
   canvas.drawLine(100,200,700,200,mPaint1);

// 繪制一組線
// 在坐標(biāo)(400,500),(500,500)之間繪制直線1
// 在坐標(biāo)(400,600),(500,600)之間繪制直線2
        canvas.drawLines(new float[]{
                400,500,500,500,
                400,600,500,600
        },mPaint2);
    }
效果圖

c. 繪制矩形(drawRect)

  • 原理:矩形的對(duì)角線頂點(diǎn)確定一個(gè)矩形

一般是采用左上角和右下角的兩個(gè)點(diǎn)的坐標(biāo)。

  • 具體使用
// 關(guān)于繪制矩形,Canvas提供了三種重載方法

       // 方法1:直接傳入兩個(gè)頂點(diǎn)的坐標(biāo)
       // 兩個(gè)頂點(diǎn)坐標(biāo)分別是:(100,100),(800,400)
        canvas.drawRect(100,100,800,400,mPaint);

        // 方法2:將兩個(gè)頂點(diǎn)坐標(biāo)封裝為RectRectF
        Rect rect = new Rect(100,100,800,400);
        canvas.drawRect(rect,mPaint);

        // 方法3:將兩個(gè)頂點(diǎn)坐標(biāo)封裝為RectF
        RectF rectF = new RectF(100,100,800,400);
        canvas.drawRect(rectF,mPaint);

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

        // 三種方法畫出來的效果是一樣的。
效果圖

d. 繪制圓角矩形

  • 原理:矩形的對(duì)角線頂點(diǎn)確定一個(gè)矩形

類似于繪制矩形

  • 具體使用
       // 方法1:直接傳入兩個(gè)頂點(diǎn)的坐標(biāo)
       // API21時(shí)才可使用
       // 第5、6個(gè)參數(shù):rx、ry是圓角的參數(shù),下面會(huì)詳細(xì)描述
       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);
      
效果圖
  • 與矩形相比,圓角矩形多了兩個(gè)參數(shù)rx 和 ry
  • 圓角矩形的角是橢圓的圓弧,rx 和 ry實(shí)際上是橢圓的兩個(gè)半徑,如下圖:
橢圓示意圖
  • 特別注意:當(dāng) rx大于寬度的一半, ry大于高度一半 時(shí),畫出來的為橢圓

實(shí)際上,在rx為寬度的一半,ry為高度的一半時(shí),剛好是一個(gè)橢圓;但由于當(dāng)rx大于寬度一半,ry大于高度一半時(shí),無法計(jì)算出圓弧,所以drawRoundRect對(duì)大于該數(shù)值的參數(shù)進(jìn)行了修正,凡是大于一半的參數(shù)均按照一半來處理

效果圖

e. 繪制橢圓

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

        // 方法2:直接傳入與矩形相關(guān)的參數(shù)
        canvas.drawOval(100,100,800,400,mPaint);

        // 為了方便表示,畫一個(gè)和橢圓一樣參數(shù)的矩形
         canvas.drawRect(100,100,800,400,mPaint);
效果圖

f. 繪制圓

  • 原理:圓心坐標(biāo)+半徑?jīng)Q定圓
  • 具體使用
// 參數(shù)說明:
// 1、2:圓心坐標(biāo)
// 3:半徑
// 4:畫筆

// 繪制一個(gè)圓心坐標(biāo)在(500,500),半徑為400 的圓。
    canvas.drawCircle(500,500,400,mPaint);  

具體使用

g. 繪制圓弧

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

// 方法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) {}



為了理解第三個(gè)參數(shù):useCenter,看以下示例:

// 以下示例:繪制兩個(gè)起始角度為0度、掃過90度的圓弧
// 兩者的唯一區(qū)別就是是否使用了中心點(diǎn)

    // 繪制圓弧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);
效果圖

從示例可以發(fā)現(xiàn):

  • 不使用中心點(diǎn):圓弧的形狀 = (起、止點(diǎn)連線+圓弧)構(gòu)成的面積
  • 使用中心店:圓弧面積 = (起點(diǎn)、圓心連線 + 止點(diǎn)、圓心連線+圓?。?gòu)成的面積

類似扇形

4.2.3 繪制文字

繪制文字分為三種應(yīng)用場景:

  • 情況1:指定文本開始的位置
  1. 即指定文本基線位置
  2. 基線x默認(rèn)在字符串左側(cè),基線y默認(rèn)在字符串下方
  • 情況2:指定每個(gè)文字的位置
  • 情況3:指定路徑,并根據(jù)路徑繪制文字

下面分別細(xì)說:

文字的樣式(大小,顏色,字體等)具體由畫筆Paint控制,詳細(xì)請(qǐng)會(huì)看上面基礎(chǔ)的介紹

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

// 參數(shù)text:要繪制的文本
// 參數(shù)x,y:指定文本開始的位置(坐標(biāo))

// 參數(shù)paint:設(shè)置的畫筆屬性
    public void drawText (String text, float x, float y, Paint paint)

// 實(shí)例
canvas.drawText("abcdefg",300,400,mPaint1);



// 僅繪制文本的一部分
// 參數(shù)start,end:指定繪制文本的位置
// 位置以下標(biāo)標(biāo)識(shí),由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)

// 對(duì)于字符數(shù)組char[]
// 截取文本使用起始位置(index)和長度(count)
    public void drawText (char[] text, int index, int count, float x, float y, Paint paint)

// 實(shí)例:繪制從位置1-3的文本
canvas.drawText("abcdefg",1,4,300,400,mPaint1);

        // 字符數(shù)組情況
        // 字符數(shù)組(要繪制的內(nèi)容)
        char[] chars = "abcdefg".toCharArray();

        // 參數(shù)為 (字符數(shù)組 起始坐標(biāo) 截取長度 基線x 基線y 畫筆)
        canvas.drawText(chars,1,3,200,500,textPaint);
        // 效果同上
效果圖

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

// 參數(shù)text:繪制的文本
// 參數(shù)pos:數(shù)組類型,存放每個(gè)字符的位置(坐標(biāo))
// 注意:必須指定所有字符位置
 public void drawPosText (String text, float[] pos, Paint paint)

// 對(duì)于字符數(shù)組char[],可以截取部分文本進(jìn)行繪制
// 截取文本使用起始位置(index)和長度(count)
    public void drawPosText (char[] text, int index, int count, float[] pos, Paint paint)

// 特別注意:
// 1. 在字符數(shù)量較多時(shí),使用會(huì)導(dǎo)致卡頓
// 2. 不支持emoji等特殊字符,不支持字形組合與分解

  // 實(shí)例
  canvas.drawPosText("abcde", new float[]{
                100, 100,    // 第一個(gè)字符位置
                200, 200,    // 第二個(gè)字符位置
                300, 300,    // ...
                400, 400,
                500, 500
        }, mPaint1);




// 數(shù)組情況(繪制部分文本)
       char[] chars = "abcdefg".toCharArray();

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

        }, mPaint1);
效果圖

情況3:指定路徑,并根據(jù)路徑繪制文字
關(guān)于Path類的使用請(qǐng)看我寫的文章具體請(qǐng)看我寫的另外一篇文章:Path類的最全面詳解 - 自定義View應(yīng)用系列

       
 // 在路徑(540,750,640,450,840,600)寫上"在Path上寫的字:Carson_Ho"字樣
        // 1.創(chuàng)建路徑對(duì)象
        Path path = new Path();
        // 2. 設(shè)置路徑軌跡
        path.cubicTo(540, 750, 640, 450, 840, 600);
         // 3. 畫路徑
        canvas.drawPath(path,mPaint2);
        // 4. 畫出在路徑上的字
        canvas.drawTextOnPath("在Path上寫的字:Carson_Ho", path, 50, 0, mPaint2);
      
效果圖

4.2.4 繪制圖片

繪制圖片分為:繪制矢量圖(drawPicture)和 繪制位圖(drawBitmap)

a. 繪制矢量圖(drawPicture)

  • 作用:繪制矢量圖的內(nèi)容,即繪制存儲(chǔ)在矢量圖里某個(gè)時(shí)刻Canvas繪制內(nèi)容的操作

矢量圖(Picture)的作用:存儲(chǔ)(錄制)某個(gè)時(shí)刻Canvas繪制內(nèi)容的操作

  • 應(yīng)用場景:繪制之前繪制過的內(nèi)容
  1. 相比于再次調(diào)用各種繪圖API,使用Picture能節(jié)省操作 & 時(shí)間
  2. 如果不手動(dòng)調(diào)用,錄制的內(nèi)容不會(huì)顯示在屏幕上,只是存儲(chǔ)起來

特別注意:使用繪制矢量圖時(shí)前請(qǐng)關(guān)閉硬件加速,以免引起不必要的問題!

具體使用方法:

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

// 獲取高度
Picture.getHeight ()

// 開始錄制 
// 即將Canvas中所有的繪制內(nèi)容存儲(chǔ)到Picture中
// 返回一個(gè)Canvas
Picture.beginRecording(int width, int height)

// 結(jié)束錄制
Picture.endRecording ()

// 將Picture里的內(nèi)容繪制到Canvas中
Picture.draw (Canvas canvas)

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

// 下面會(huì)詳細(xì)介紹

一般使用的具體步驟

// 步驟1:創(chuàng)建Picture對(duì)象
Picture mPicture = new Picture();

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

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

// 步驟4:結(jié)束錄制
mPicture.endRecording ();

步驟5:某個(gè)時(shí)刻將存儲(chǔ)在Picture的繪制內(nèi)容繪制出來
mPicture.draw (Canvas canvas);

下面我將用一個(gè)實(shí)例去表示如何去使用:

  • 實(shí)例介紹
    將坐標(biāo)系移動(dòng)到(450,650);繪制一個(gè)圓,將上述Canvas操作錄制下來,并在某個(gè)時(shí)刻重新繪制出來。

步驟1:創(chuàng)建Picture對(duì)象

Picture mPicture = new Picture();

步驟2:開始錄制

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

// 注:要?jiǎng)?chuàng)建Canvas對(duì)象來接收beginRecording()返回的Canvas對(duì)象

步驟3:繪制內(nèi)容 or 操作Canvas

        // 位移
        // 將坐標(biāo)系的原點(diǎn)移動(dòng)到(450,650)
        recordingCanvas.translate(450,650);

        // 記得先創(chuàng)建一個(gè)畫筆
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.FILL);

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

步驟4:結(jié)束錄制

mPicture.endRecording();

步驟5:將存儲(chǔ)在Picture的繪制內(nèi)容繪制出來

有三種方法:

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

將Picture包裝成為PictureDrawable

主要區(qū)別如下:

Paste_Image.png

方法1:Picture提供的draw()

// 在復(fù)寫的onDraw()里
  @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);

        // 將錄制的內(nèi)容顯示在當(dāng)前畫布里
        mPicture.draw(canvas);

// 注:此方法繪制后可能會(huì)影響Canvas狀態(tài),不建議使用
 }


效果圖

方法2:Canvas提供的drawPicture()

不會(huì)影響Canvas狀態(tài)

// 提供了三種方法
// 方法1
public void drawPicture (Picture picture)
// 方法2
// Rect dst代表顯示的區(qū)域
// 若區(qū)域小于圖形,繪制的內(nèi)容根據(jù)選區(qū)進(jìn)行縮放
public void drawPicture (Picture picture, Rect dst)

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

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

        // 實(shí)例1:將錄制的內(nèi)容顯示(區(qū)域剛好布滿圖形)
        canvas.drawPicture(mPicture, new RectF(0, 0, mPicture.getWidth(), mPicture.getHeight()));

        // 實(shí)例2:將錄制的內(nèi)容顯示在當(dāng)前畫布上(區(qū)域小于圖形)
        canvas.drawPicture(mPicture, new RectF(0, 0, mPicture.getWidth(), 200));


效果圖

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

將Picture包裝成為PictureDrawable


 @Override
    protected void onDraw(Canvas canvas){

        super.onDraw(canvas);

        // 將錄制的內(nèi)容顯示出來

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

        // 設(shè)置在畫布上的繪制區(qū)域(類似drawPicture (Picture picture, Rect dst)的Rect dst參數(shù))
        // 每次都從Picture的左上角開始繪制
        // 并非根據(jù)該區(qū)域進(jìn)行縮放,也不是剪裁Picture。

        // 實(shí)例1:將錄制的內(nèi)容顯示(區(qū)域剛好布滿圖形)
        drawable.setBounds(0, 0,mPicture.getWidth(), mPicture.getHeight());

        // 繪制
        drawable.draw(canvas);


        // 實(shí)例2:將錄制的內(nèi)容顯示在當(dāng)前畫布上(區(qū)域小于圖形)
        drawable.setBounds(0, 0,250, mPicture.getHeight());
效果圖

b. 繪制位圖(drawBitmap)

  • 作用:將已有的圖片轉(zhuǎn)換為位圖(Bitmap),最后再繪制到Canvas上

位圖,即平時(shí)我們使用的圖片資源

獲取Bitmap對(duì)象的方式

要繪制Bitmap,就要先獲取一個(gè)Bitmap對(duì)象,具體獲取方式如下:

獲取Bitmap對(duì)象方式

特別注意:繪制位圖(Bitmap)是讀取已有的圖片轉(zhuǎn)換為Bitmap,最后再繪制到Canvas。

所以:

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

具體請(qǐng)自行了解關(guān)于Drawble的內(nèi)容

  • 對(duì)于第3種方式:滿足需求,下面會(huì)著重講解

通過BitmapFactory獲取Bitmap (從不同位置獲?。?/p>

// 共3個(gè)位置:資源文件、內(nèi)存卡、網(wǎng)絡(luò)

// 位置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:內(nèi)存卡文件
    Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/bitmap.png");

// 位置4:網(wǎng)絡(luò)文件:
// 省略了獲取網(wǎng)絡(luò)輸入流的代碼
        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)

// 下面詳細(xì)說

方法1

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

// 后兩個(gè)參數(shù)matrix, paint是在繪制時(shí)對(duì)圖片進(jìn)行一些改變
// 后面會(huì)專門說matrix
  
// 如果只是將圖片內(nèi)容繪制出來只需將傳入新建的matrix, paint對(duì)象即可:
  canvas.drawBitmap(bitmap,new Matrix(),new Paint());
// 記得選取一種獲取Bitmap的方式
// 注:圖片左上角位置默認(rèn)為坐標(biāo)原點(diǎn)。
效果圖

方法2

// 參數(shù) left、top指定了圖片左上角的坐標(biāo)(距離坐標(biāo)原點(diǎn)的距離):
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)

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

Paste_Image.png

方法3

 public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
// 參數(shù)(src,dst) = 兩個(gè)矩形區(qū)域
// Rect src:指定需要繪制圖片的區(qū)域(即要繪制圖片的哪一部分)
// Rect dst 或RectF dst:指定圖片在屏幕上顯示(繪制)的區(qū)域
// 下面我將用實(shí)例來說明

// 實(shí)例
 // 指定圖片繪制區(qū)域
        // 僅繪制圖片的二分之一
        Rect src = new Rect(0,0,bitmap.getWidth()/2,bitmap.getHeight());

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

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

// 下面我們一步步分析:
分析

特別注意的是:如果src規(guī)定繪制圖片的區(qū)域大于dst指定顯示的區(qū)域的話,那么圖片的大小會(huì)被縮放。

方法3的應(yīng)用場景:

  • 便于素材管理
    當(dāng)我需要畫很多個(gè)圖時(shí),如果1張圖=1個(gè)素材的話,那么管理起來很不方便;如果素材都放在一個(gè)圖,那么按需繪制會(huì)便于管理


    Paste_Image.png
  • 實(shí)現(xiàn)動(dòng)態(tài)效果
    動(dòng)態(tài)效果 = 逐漸繪制圖形部分,如下:

動(dòng)態(tài)效果圖

在繪制時(shí),只需要一個(gè)資源文件,然后逐漸描繪就可以


資源文件

繪制過程如下:


描繪過程

4.2.5 繪制路徑

// 通過傳入具體路徑Path對(duì)象 & 畫筆
canvas.drawPath(mPath, mPaint)

關(guān)于Path類的使用,具體請(qǐng)看我寫的另外一篇文章:Path類的最全面詳解 - 自定義View應(yīng)用系列

4.2.6 畫布操作

  • 作用:改變畫布的性質(zhì)

改變之后,任何的后續(xù)操作都會(huì)受到影響

A. 畫布變換

a. 平移(translate)

  • 作用:移動(dòng)畫布(實(shí)際上是移動(dòng)坐標(biāo)系,如下圖)
  • 具體使用

// 將畫布原點(diǎn)向右移200px,向下移100px
canvas.translate(200, 100)  
// 注:位移是基于當(dāng)前位置移動(dòng),而不是每次都是基于屏幕左上角的(0,0)點(diǎn)移動(dòng)
效果圖

b. 縮放(scale)

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

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

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

// 實(shí)例:畫兩個(gè)對(duì)比圖
// 相同:都有兩個(gè)矩形,第1個(gè)= 正常大小,第2個(gè) = 放大1.5倍 
// 不同點(diǎn):第1個(gè)縮放中心在(0,0),第2個(gè)在(px,py)

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

         // 繪制矩形(藍(lán)色)
        canvas.drawRect(rect, mPaint1);

        // 將畫布放大到1.5倍
        // 不移動(dòng)縮放中心,即縮放中心默認(rèn)為(0,0)
        canvas.scale(1.5f, 1.5f);
        // 繪制放大1.5倍后的藍(lán)色矩形(紅色)
        canvas.drawRect(rect,mPaint2);

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

         // 繪制矩形(藍(lán)色)
        canvas.drawRect(rect, mPaint1);
       
        // 將畫布放大到1.5倍,并將縮放中心移動(dòng)到(100,0)
        canvas.scale(1.5f, 1.5f, 100,0);              
        // 繪制放大1.5倍后的藍(lán)色矩形(紅色)
        canvas.drawRect(rect,mPaint2);
       
// 縮放的本質(zhì)是:把形狀先畫到畫布,然后再縮小/放大。所以當(dāng)放大倍數(shù)很大時(shí),會(huì)有明顯鋸齒

效果圖

當(dāng)縮放倍數(shù)為負(fù)數(shù)時(shí),會(huì)先進(jìn)行縮放,然后根據(jù)不同情況進(jìn)行圖形翻轉(zhuǎn)

(設(shè)縮放倍數(shù)為(a,b),旋轉(zhuǎn)中心為(px,py)):

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

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

Paste_Image.png

c. 旋轉(zhuǎn)(rotate)

注意:角度增加方向?yàn)轫槙r(shí)針(區(qū)別于數(shù)學(xué)坐標(biāo)系)

與數(shù)學(xué)坐標(biāo)系對(duì)比

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

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

            
效果圖

d. 錯(cuò)切(skew)

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


// 實(shí)例
   // 為了方便觀察,我將坐標(biāo)系移到屏幕中央
        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負(fù)方向傾斜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負(fù)方向傾斜45度
        canvas.skew(0, -1f);
        canvas.drawRect(20, 20, 400, 200, mPaint1);

效果圖

B. 畫布裁剪

即從畫布上裁剪一塊區(qū)域,之后僅能編輯該區(qū)域

特別注意:其余的區(qū)域只是不能編輯,但是并沒有消失,如下圖

Paste_Image.png
裁剪共分為:裁剪路徑、裁剪矩形、裁剪區(qū)域

// 裁剪路徑
// 方法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) 

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

這里特別說明一下參數(shù)Region.Op op
作用:在剪下多個(gè)區(qū)域下來的情況,當(dāng)這些區(qū)域有重疊的時(shí)候,這個(gè)參數(shù)決定重疊部分該如何處理,多次裁剪之后究竟獲得了哪個(gè)區(qū)域,有以下幾種參數(shù):

Paste_Image.png

以三個(gè)參數(shù)為例講解:
Region.Op.DIFFERENCE:顯示第一次裁剪與第二次裁剪不重疊的區(qū)域

Paste_Image.png
   // 為了方便觀察,我將坐標(biāo)系移到屏幕中央
        canvas.translate(300, 500);

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

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

        //將第一次裁剪后的區(qū)域設(shè)置為紅色
        canvas.drawColor(Color.RED);

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

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

Region.Op.REPLACE:顯示第二次裁剪的區(qū)域

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

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

        //將第一次裁剪后的區(qū)域設(shè)置為紅色
        canvas.drawColor(Color.RED);

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

        //將第二次裁剪的區(qū)域設(shè)置為藍(lán)色
        canvas.drawColor(Color.BLUE);

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

Paste_Image.png
//原來畫布設(shè)置為灰色)
        canvas.drawColor(Color.GRAY);

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

        //將第一次裁剪后的區(qū)域設(shè)置為紅色
        canvas.drawColor(Color.RED);

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

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

關(guān)于其他參數(shù),較為簡單,此處不作過多展示。

C. 畫布快照

這里先理清幾個(gè)概念

  • 畫布狀態(tài):當(dāng)前畫布經(jīng)過的一系列操作
  • 狀態(tài)棧:存放畫布狀態(tài)和圖層的棧(后進(jìn)先出)


    狀態(tài)棧
  • 畫布的構(gòu)成:由多個(gè)圖層構(gòu)成,如下圖
  1. 在畫布上操作 = 在圖層上操作
  2. 如無設(shè)置,繪制操作和畫布操作是默認(rèn)在默認(rèn)圖層上進(jìn)行
  3. 在通常情況下,使用默認(rèn)圖層就可滿足需求;若需要繪制復(fù)雜的內(nèi)容(如地圖),則需使用更多的圖層
  4. 最終顯示的結(jié)果 = 所有圖層疊在一起的效果
畫布構(gòu)成 - 圖層

a. 保存當(dāng)前畫布狀態(tài)(save)

  • 作用:保存畫布狀態(tài)(即保存畫布的一系列操作)
  • 應(yīng)用場景:畫布的操作是不可逆的,而且會(huì)影響后續(xù)的步驟,假如需要回到之前畫布的狀態(tài)去進(jìn)行下一次操作,就需要對(duì)畫布的狀態(tài)進(jìn)行保存和回滾

// 方法1:
  // 保存全部狀態(tài)
  public int save ()

// 方法2:
  // 根據(jù)saveFlags參數(shù)保存一部分狀態(tài)
  // 使用該參數(shù)可以只保存一部分狀態(tài),更加靈活
  public int save (int saveFlags)

// saveFlags參數(shù)說明:
// 1.ALL_SAVE_FLAG(默認(rèn)):保存全部狀態(tài)
// 2. CLIP_SAVE_FLAG:保存剪輯區(qū)
// 3. CLIP_TO_LAYER_SAVE_FLAG:剪裁區(qū)作為圖層保存
// 4. FULL_COLOR_LAYER_SAVE_FLAG:保存圖層的全部色彩通道
// 5. HAS_ALPHA_LAYER_SAVE_FLAG:保存圖層的alpha(不透明度)通道
// 6. MATRIX_SAVE_FLAG:保存Matrix信息(translate, rotate, scale, skew)

// 每調(diào)用一次save(),都會(huì)在棧頂添加一條狀態(tài)信息(入棧)
入棧

b. 保存某個(gè)圖層狀態(tài)(saveLayer)

  • 作用:新建一個(gè)圖層,并放入特定的棧中
  • 具體使用

使用起來非常復(fù)雜,因?yàn)閳D層之間疊加會(huì)導(dǎo)致計(jì)算量成倍增長,營盡量避免使用。

// 無圖層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. 回滾上一次保存的狀態(tài)(restore)

  • 作用:恢復(fù)上一次保存的畫布狀態(tài)
  • 具體使用

// 采取狀態(tài)棧的形式。即從棧頂取出一個(gè)狀態(tài)進(jìn)行恢復(fù)。
canvas.restore();
效果圖

d. 回滾指定保存的狀態(tài)(restoreToCount)

  • 作用:恢復(fù)指定狀態(tài);將指定位置以及以上所有狀態(tài)出棧
  • 具體使用:
 canvas.restoreToCount(3) ;
// 彈出 3、4、5的狀態(tài),并恢復(fù)第3次保存的畫布狀態(tài)
效果圖

e. 獲取保存的次數(shù)(getSaveCount)

  • 作用:獲取保存過圖層的次數(shù)

即獲取狀態(tài)棧中保存狀態(tài)的數(shù)量

canvas.getSaveCount();
// 以上面棧為例,則返回5
// 注:即使彈出所有的狀態(tài),返回值依舊為1,代表默認(rèn)狀態(tài)。(返回值最小為1)

總結(jié)

對(duì)于畫布狀態(tài)的保存和回滾的套路,一般如下:

 // 步驟1:保存當(dāng)前狀態(tài)
//  把Canvas的當(dāng)前狀態(tài)信息入棧
 save();     

 // 步驟2:對(duì)畫布進(jìn)行各種操作(旋轉(zhuǎn)、平移Blabla)
   ...      

 // 步驟3:回滾到之前的畫布狀態(tài)
  // 把棧里面的信息出棧,取代當(dāng)前的Canvas信息
   restore();  

5. 總結(jié)

通過閱讀本文,相信你已經(jīng)全面了解Canvas類的使用。Carson帶你學(xué)Android自定義View文章系列:
Carson帶你學(xué)Android:自定義View基礎(chǔ)
Carson帶你學(xué)Android:一文梳理自定義View工作流程
Carson帶你學(xué)Android:自定義View繪制準(zhǔn)備-DecorView創(chuàng)建
Carson帶你學(xué)Android:自定義View Measure過程
Carson帶你學(xué)Android:自定義View Layout過程
Carson帶你學(xué)Android:自定義View Draw過程
Carson帶你學(xué)Android:手把手教你寫一個(gè)完整的自定義View
Carson帶你學(xué)Android:Canvas類全面解析
Carson帶你學(xué)Android:Path類全面解析


歡迎關(guān)注Carson_Ho的簡書

不定期分享關(guān)于安卓開發(fā)的干貨,追求短、平、快,但卻不缺深度。


請(qǐng)點(diǎn)贊!因?yàn)槟愕墓膭?lì)是我寫作的最大動(dòng)力!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容