Android 自定義View學習(七)——Canvas知識學習

上篇的學習,Paint中的基礎知識基本結束,筆學完了,學習開始學習畫布。本篇記錄學習Canvas中的一些知識。在drawBitmap()方法中會引出計劃下篇要學習的內容Matrix,drawPath()方法會引出計劃在下下篇學習的貝塞爾曲線,學習的重點也是這兩個方法

學習資料:

本人很菜,有錯誤,請指出


雖然稱Canvas為畫布,但并不是直接在Canvas畫,Canvas內部默認會創建一個Biatmap,也可以通過構造方法或者setBitmap()方法傳入一個,像素所有的信息是畫在了這個Bitmap上,然后Bitmap被保存在了Canvas之內

這是我看愛哥的博客后,自己做的一個總結,若有錯誤,請指出。針對Canvas的兩種構造方法,愛哥針對源碼有做分析,可以看看了解一下

Canvas的方法中,clipdraw方法占據了一大半,在Android 自定義View學習(二)——開始了解Canvas和Paint了解過了幾個draw方法,本篇進行補充學習


1.draw方法補充學習

draw一系列方法中,有一個特殊的牛B的存在,drawBitmapMesh(),這個方法牛B在可以幾乎對Bitmap做任何操作。雖然這個方法很強大,但使用的頻率并不算高,也有點雞肋。一些比較簡單的Bitmap處理可能優先考慮Matrix,而過于復雜的處理,會耗時比較久,效率可能并不高。一般優先不考慮這個方法,遇到Matrix實現不了的需求,記得有這么一個方法可以學習,然后使用


1.1 drawBitmap方法

感覺這個方法使用頻率很高。一共有6個重載方法,其中兩個參數最多的方法已經廢棄,也就是需要學習4個

1.1.1 第1個方法

drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint)
  • bitmap 要畫的目標Bitmap
  • left 左上角的X軸坐標
  • top 左上角的Y軸坐標
  • paint 畫筆

簡單使用

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
    final float x = getWidth() / 2 - bitmap.getWidth() / 2;//水平居中
    final float y = 0;
    canvas.drawBitmap(bitmap, x, y, mPaint);
}
drawBitmap第1個方法

在實際開發中,還會考慮圖片寬高的壓縮,顯示位置,以及padding的等等


1.1.2 第2個和第3個方法

drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,@Nullable Paint paint)

drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst,@Nullable Paint paint)

兩個方法的差別在于第三個參數RectRectF

  • src 用來截取Bitmap局部所想要顯示的像素塊區域,通過構造方法中的四個坐標系點確定范圍。這個參數可以為null,為null就是整個Bitmap都作為目標資源顯示
  • dst 用來顯示的區域,在控件中繪制Bitmap的區域,可以實現拉伸或者縮放

簡單使用

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.test);
    rect = new Rect(0,0,1080,600);
    rectF = new RectF(0f,0f,1080f,600f);
}

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (bitmap != null)
        canvas.drawBitmap(bitmap,null,rectF,mPaint);
}
drawBitmap第2個方法

1080是測試手機的屏幕寬度

第2個參數rect = new Rect(x,y,x1,y1) ,構造方法中四個參數分別為:

  • x, X軸開始的坐標點,默認為0
  • y, Y軸開始的坐標點
  • x1, X軸結束的坐標點,若x1-x大于了Bitmap的寬度,截取的有效區域就是Bitmap的寬度
  • y1, Y軸結束的坐標點

第3個參數rectF = new RectF(x,y,x1,y1),構造方法中四個參數分別為:

  • x,開始繪制的X軸的坐標點,默認為0
  • y,開始繪制的Y軸的坐標點
  • x1,結束繪制的X軸的坐標點,若x1-x大于srcx1-x,就是拉伸;小于就是縮放
  • y1,結束繪制的Y軸的坐標點

簡單修改代碼:

rect = new Rect(0,0,400,600);
rectF = new RectF(100f,0f,700f,600f);
拉伸顯示局部

此時在控件X100f位置開始繪制資源Bitmap(0,0)到(400,600)局部區域,最終的顯示效果就成了拉伸局部的效果。Y軸同理

這兩個參數的作用,用幾個數簡單測試一下,比較直觀。


1.1.3 第4個方法

drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, @Nullable Paint paint)

簡單使用

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
    matrix = new Matrix();
    matrix.setTranslate(100f,100f);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmap(bitmap,matrix,mPaint);
}
drawBitmap第4個方法

Matrix是一個3 * 3的矩陣,下篇進行記錄學習


1.2 畫Line方法

畫一條線的方法

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

畫多條線的方法

drawLines(@Size(multiple=4) @NonNull float[] pts, @NonNull Paint paint)

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

1.2.1 drawLine 畫一條線

drawLine()方法就一個,沒有重載方法。畫規則曲線可以考慮使用Path

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.parseColor("#FF4081"));
    mPaint.setStrokeWidth(10f);
}

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawLine(100f,100f,600f,500f,mPaint);
}
畫直線

方法參數也很簡單,四個參數確定兩個點的坐標,然后兩點一線


1.2.2 drawLines畫多條直線

兩個參數的drawsLines(float[] pts, Paint paint)方法簡單使用

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.parseColor("#FF4081"));
    mPaint.setStrokeWidth(10f);
    floats = new float[]{100f,100f,300f,300f,400f,200f,600f,200f};//每4個數一組,確定兩個點的坐標
}

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawLines(floats,mPaint);
}
畫多條直線

方法源碼中有這樣一個@Size(multiple=4)注解,要求floats中每4個值看做一組,每組來確定一條直線的兩個端點,不足4個的部分是無效的

兩個參數的方法內部還是調用了四個參數的方法,在源碼中

public void drawLines(@Size(multiple=4) @NonNull float[] pts, @NonNull Paint paint) {
    drawLines(pts, 0, pts.length, paint);
}

drawLines(float[] pts, int offset, int count,Paint paint)中,

  • offset 表示跳過floats中幾個值
  • count 表示跳過offset后,數組的長度

簡單修改代碼

canvas.drawLines(floats,4,floats.length-4,mPaint);

就只會畫出(400f,200f),(600f,200f)確定的那條水平的短線


1.3 畫Potion方法

同1.2畫Line方法一樣,畫點的方法也是有兩種,畫一個點和畫多個點

畫一個點很簡單,直接看畫多個點

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

根據方法內的注解得知,pts的大小要大于2,并且每兩個一組,多余的無效

簡單使用:

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.parseColor("#FF4081"));
    mPaint.setStrokeWidth(10f);
    floats = new float[]{100f,100f,300f,200f,200f};
}

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPoints(floats,mPaint);
}
繪制兩個點

floats中,有5個值,但實際有效的也就是前4個


1.3 drawPath 方法

Android 自定義View學習(三)——Paint繪制文字屬性中學習了解了setPathEffect(PathEffect effect)方法在繪制路徑時的效果,drawPath()方法往往都會配合PahtEffect來使用

 drawPath(@NonNull Path path, @NonNull Paint paint)

方法只有一個,簡單使用

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.parseColor("#FF4081"));
    mPaint.setStyle(Paint.Style.STROKE);//設置風格為空心
    mPaint.setStrokeWidth(10f);

    path = new Path();
    path.moveTo(540f,50f);
    path.lineTo(740f,300f);
    path.lineTo(340f,300f);
    path.close();//形成閉合 將(340,300)和(540,50)連接起來
}
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(path, mPaint);
}
繪制等腰三角形

繪制出一個等腰三角形

主要就是用到Path這個類,這個類有很多方法除了可以繪制直線,還可以繪制曲線


1.3.1 Path類

The Path class encapsulates compound (multiple contour) geometric paths consisting of straight line segments, quadratic curves, and cubic curves.
It can be drawn with canvas.drawPath(path, paint), either filled or stroked (based on the paint's Style), or it can be used for clipping or to draw text on a path.

這個類封裝了一些可以借助一元直線方程,二遠方程曲線,立方曲線的方法來繪制一些較為復雜的組合形式的集合圖形,繪制出來的風格則是根據畫筆設置的style來決定,也可以剪切或者繪制一段文字在路徑上

除了已經用到的moveTo()lineTo()方法,Path中還有很多add開頭的方法。有兩個重點方法是quadTo()cubicTo()方法


1.3.1 quadTo和cubicTo方法

quadTo()可以用來繪制二階貝塞爾曲線,也就是3個點確定一個曲線

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.parseColor("#FF4081"));
    mPaint.setStyle(Paint.Style.STROKE);//設置風格為空心
    mPaint.setStrokeWidth(10f);

    path = new Path();
    path.moveTo(100f,100f);

    floats = new float[]{200f,200f,900f,100f};
    path.quadTo(200f,200f,900f,100f);
}

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(path, mPaint);
    canvas.drawPoints(floats,mPaint);
}
二階貝塞爾曲線

圖中的曲線,是由(100,100),(900,100),(200,200)三個點來確定。


cubicTo方法使用和quadTo用法類似,只是多了一組參數,多了一個點而已

 path.cubicTo(200f,200f,400f,100f,900f,200f);

測試效果和quadTo很容易就分區

依稀記得高三的數學考試最后一道大題往往就是要求繪制會一個點的運動軌跡,大部分都是一段曲線或者一個橢圓之類的,和這里有些類似。

這兩方法重要的是理解其中的原理,貝爾塞爾曲線等到Matrix學習結束后再進行學習


1.3.2 addTo方法

先來看簡單用法

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.parseColor("#FF4081"));
    mPaint.setStyle(Paint.Style.STROKE);//設置風格為空心
    mPaint.setStrokeWidth(10f);

    path = new Path();
    path.moveTo(100f,100f);
    rectF = new RectF(100, 100, 400, 400);
    path.arcTo(rectF,0,90);
}
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(path, mPaint);
}
addTo方法

這個方法就是畫出一段弧線后,將弧線的起始點和moveTo確定的path的起始點進行連接起來?;【€截取圓的一部分。圓的直徑為300,經過很簡單分析,圓心在(250,250)的點


1.3.3 rLineTo方法

r就是relative,相對的縮寫

簡單使用

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.parseColor("#FF4081"));
    mPaint.setStyle(Paint.Style.STROKE);//設置風格為空心
    mPaint.setStrokeWidth(10f);

    path = new Path();
    path.moveTo(100f,100f);
    path.rLineTo(300,200f);
 }
 
 protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(path, mPaint);
    canvas.drawPoint(300,200,mPaint);
}
rLineTo方法

rLineTo方法確定的坐標是相對于moveTo來說的,實際最終的畫出的直線的結束點的坐標為(400,300)


1.3.4 addArc方法

簡單使用

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.parseColor("#FF4081"));
    mPaint.setStyle(Paint.Style.STROKE);//設置風格為空心
    mPaint.setStrokeWidth(10f);

    path = new Path();
    path.moveTo(100f,100f);
    path.lineTo(300,200f);
    //添加弧形
    rectF = new RectF(100, 100, 400, 400);
    path.addArc(rectF,0,90);
}

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, mPaint);
        canvas.drawPoint(300,200,mPaint);
}
addArc方法

addArc方法和addTo方法卻別是,addArc是添加一段弧形,并不將繪制的圖形連接起來


1.4 canvas.drawTextOnPath 方法

在繪制的路徑上,繪制文字

private void init() {
    //路徑畫筆
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.parseColor("#FF4081"));
    mPaint.setStyle(Paint.Style.STROKE);//設置風格為空心
    mPaint.setStrokeWidth(10f);
    //文字畫筆
    textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    textPaint.setTextSize(60f);

    //路徑
    path = new Path();
    rectF = new RectF(100, 100, 300, 400);
    path.addOval(rectF, Path.Direction.CW);

    chars = new char[]{'a','b','c','d','e'};
}

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //繪制路徑
    canvas.drawPath(path, mPaint);
    //繪制路徑上的文字
    canvas.drawTextOnPath(chars,0,chars.length,path,0f,chars.length,textPaint);
}
在路徑上繪制文字

addOval方法中,可以改變路徑閉合的方向,簡單修改代碼

path.addOval(rectF, Path.Direction.CCW);
Path.Direction.CCW

文字在路徑內側繪制,并且逆時針


draw系列方法中,有一個看起來很有意思的方法,drawTextRun方法,但這個方法最低要求的23

canvas.drawTextRun(chars,0,chars.length,0,chars.length,100f,100f,false,textPaint);

沒有23的真機,就用了虛擬機,看方法名字,以為會按照一定的方法,文字進行滾動,可并沒有,設置為falseabcde,設置為trueedcba,不清楚在真機上啥效果

加上在開始了解Canvas中的方法,draw大致就學習到這里


2.Clip方法學習

Clip開頭的方法主要有兩個:

  • clipPath() 利用Path的方法,可以裁切出一塊不規則區域畫布
  • clipRect() 可以裁切出一塊矩形畫布

還有一個已經廢棄的clipRegion(),廢棄就不學了,直接學替代的方法


2.1 clipRect裁切規則區域畫布

簡單使用:

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.parseColor("#FF4081"));
    mPaint.setStyle(Paint.Style.FILL);
    //矩形
    rectF = new RectF(0, 0, 400, 400);
}

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //繪制底色 黃
    canvas.drawColor(Color.YELLOW);
    //截取畫布
    canvas.clipRect(rectF);
    //截取后的畫布底色
    canvas.drawColor(Color.CYAN);
    //驗證有效區域
    canvas.drawRect(300,300,600,600,mPaint);
}
drawRect方法

clipRect(rectF),就是在在畫布裁出以(0,0),(0,400),(400,0),(400,400)四個點確定的矩形。之后Canvas有效的區域便就是裁出的矩形區域,再次進行繪制時,超出這個區域便無法繪制,但裁切并不會影響c裁切前已經繪制好的區域,clip裁切針對的是Canvas

cliprRect有這樣一個cliprRect((@NonNull RectF rect, @NonNull Region.Op op)重載方法


2.1.1 Region.Op

OpRegion類中的一個枚舉,有6個值

直接用代碼演示效果

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.WHITE);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(5f);
    //裁切區域1
    rectF1 = new RectF(100, 100, 300, 300);
    //裁切區域2
    rectF2 = new RectF(200, 200, 400, 400);
}

 protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //繪制底色 藍
    canvas.drawColor(Color.BLUE);
    canvas.save();
    //截取畫布1
    canvas.clipRect(rectF1);
    //截取畫布2
    canvas.clipRect(rectF2,Region.Op.DIFFERENCE);
    //截取后的有效區域畫布底色
    canvas.drawColor(Color.RED);
    canvas.restore();
    //繪制輔助區域
    canvas.drawRect(rectF1, mPaint);
    canvas.drawRect(rectF2, mPaint);
}

紅色區域就代表兩次裁切后的有效區域

  • Region.Op.DIFFERENCE
Region.Op.DIFFERENCE第一次的非交集

取第一次裁切的非交集部分


  • Region.Op.INTERSECT
Region.Op.INTERSECT交集

取兩次的交集


  • Region.Op.REPLACE
Region.Op.REPLACE第二次替代第一次裁切

第二次替代第一次裁切


  • Region.Op.UNION
Region.Op.UNION兩次裁切的和

兩次裁切的和


  • Region.Op.REVERSE_DIFFERENCE
Region.Op.REVERSE_DIFFERENCE取第2次的非交集

DIFFERENCE相反,取的第2次裁切的非交集區域


  • Region.Op.XOR
Region.Op.XOR兩次的非交集

異或,取兩次交集外的區域

有點類似PorterDuffXfermode圖像處理的效果


2.2 CilpPath 裁切不規則畫布

裁切出一個圓形區域的畫布

private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.WHITE);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(5f);

    path = new Path();
    path.addCircle(300,300,100, Path.Direction.CCW);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //繪制底色 藍
    canvas.drawColor(Color.BLUE);
    //裁切畫布
    canvas.clipPath(path);
    //繪制裁切后的區域底色
    canvas.drawColor(Color.parseColor("#FF4081"));
    //繪制輔助圓形
    canvas.drawCircle(300,300,100,mPaint);
}
裁切圓形的畫布

clip的方法基本就學到這里


3.其他方法

畫布除了裁切外,還有可以進行旋轉

3.1 rotate 旋轉方法

private void init() {
        rectP1 = new Paint(Paint.ANTI_ALIAS_FLAG);
        rectP1.setColor(Color.BLUE);

        rectP2 = new Paint(Paint.ANTI_ALIAS_FLAG);
        rectP2.setColor(Color.parseColor("#FF4081"));

        rectF1 = new RectF(100,100,400,400);
        rectF2 = new RectF(200,200,300,300);
}

/**
 *  旋轉畫布
 */
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //旋轉30°
    canvas.rotate(30);
    canvas.drawRect(rectF1,rectP1);
    canvas.drawRect(rectF2,rectP2);
}
旋轉30度

整個畫布進行了順時針旋轉30°,參數為正時,是順時針旋轉,負數為逆時針旋轉

如果只想讓小的紅色的矩形進行旋轉,而藍色的大矩形不旋轉,需要了解畫布中的


3.2 save和restore方法

簡單修改代碼,加入saverestore兩個方法

private void init() {
    rectP1 = new Paint(Paint.ANTI_ALIAS_FLAG);
    rectP1.setColor(Color.BLUE);

    rectP2 = new Paint(Paint.ANTI_ALIAS_FLAG);
    rectP2.setColor(Color.parseColor("#FF4081"));

    rectF1 = new RectF(100,100,400,400);
    rectF2 = new RectF(200,200,300,300);

}

/**
 *  旋轉畫布
 */
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawRect(rectF1,rectP1);
    canvas.save();
    //旋轉30°
    canvas.rotate(30);
    canvas.drawRect(rectF2,rectP2);
    canvas.restore();
}
旋轉小矩形

save()就是保存當前圖層
restore() 就是把圖層恢復到最近一次save()方法前的狀態

關于保存圖層,還有一個更加強大的saveLayer(),這個方法就等用到時,再進行學習


3.3 translate 平移畫布

這個方法使用很簡單

@Override
protected void onDraw(Canvas canvas) {    
    super.onDraw(canvas);   
    //平移 
    canvas.translate(800,200);  
    //繪制矩形  
    canvas.drawRect(rectF1,rectP1);   
    canvas.drawRect(rectF2,rectP2); 
    //在(100,100)處繪制一個小圓,用來輔助觀察坐標系的改變
    canvas.drawCircle(100,100,30,rectP2);
}
平移

這個方法需要注意的是,Canvas的坐標系就進行了改變,觀察小圓的位置


3.4 scale 縮放方法

縮放的使用也非常簡單

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.scale(0.5F, 1.0F);
    canvas.drawBitmap(bitmap,0,0,null);
}
縮放

縮放有效值為0~1f,1表示不進行縮放,原始大小

縮放方法有個重載方法scale(float sx, float sy, float px, float py)

簡單修改代碼

canvas.scale(0.5F, 1.0F,540,0);
指定縮放中心

px,py確定縮放中心,canvas.scale(0.5F, 1.0F)默認為(0,0)為縮放中心,指定(540,0)為縮放中心時,屏幕寬度為1080Canvas就在水平居中縮放


3.5 skew 錯切

簡單使用

 protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.skew(0.5f,0);
    canvas.drawBitmap(bitmap,0,0,null);
}
錯切

這幾個方法都只是簡單的調用,看了看效果,以后用到就再深入了解學習


4. 最后

Canvas的基本知識也就學習這些,遺漏的遇到再學習

篇幅有點長,但并不難理解,基本都是調用一下就可以比較直觀看出效果的方法

下篇學習Matrix

月餅節到了,中秋快樂 : )

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容