Android關於Canvas你所知道的和不知道的一切

在一年的Android自學中,Canvas一直是我能避且避的類,甚至不惜封裝本身的繪圖庫來替代它。
現在回首,虐我千萬次的Canvas也不過如此,靜下心看看,其實也沒有想象中的那麼糟糕。
就像曾經等級30的我去打點等級40的副本(Canvas)很是吃力,如今等級50的我回來吊打它同樣。
因此朋友,遇到承受不了的困擾,不要太沮喪,去別的地方刷怪升級,一旦境界提高了,遲早能夠"報仇雪恨"
Android技術棧C模塊,第一篇正式開講:git

若是將View、Canvas、Paint、Coder(編寫代碼的人)作個類比:

那麼Canvas是一個黑匣子裏的白紙,它的特性是能夠添加圖層和平移,旋轉、縮放、斜切等,最重要的就是它的n種drawXXX...
Paint是繪製用的畫筆,它的特性是提供繪製工具與制定畫筆的特殊效果(如筆頭Cap,線接方式Join,六種Effect)
View則是讓黑匣子變成透明的視口,也是咱們最熟悉。那Coder就是在操縱畫筆的在白紙上繪製的人,是最核心的github


1、前期準備:

1.自定義View中的canvas:

提及Canvas對象,貌似不多去new它,更多的是在自定義控件時的Ondraw方法裏回調有canvas對象編程

public class CanvasView extends View {

    public CanvasView(Context context) {
        this(context, null);

    }

    public CanvasView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        //TODO init 初始化
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //TODO drawGrid 繪製網格
        //TODO drawCoo 繪製座標系
    }
}
複製代碼

這裏值得提一下:onDarw中儘可能不要建立對象,由於視圖更新都會走onDarw(),而不斷開闢空間canvas

onDraw.png


2.準備網格與座標系

若是要演示繪製,這二者必不可少,放在analyze包裏
實現效果:給出座標原點後會自動繪製座標系以及網格和數字數組

網格和座標系效果2.png

1).使用方式:
//成員變量
    private Paint mGridPaint;//網格畫筆
    private Point mWinSize;//屏幕尺寸
    private Point mCoo;//座標系原點
    
//TODO init 初始化:release:
    //準備屏幕尺寸
    mWinSize = new Point();
    mCoo = new Point(500, 500);
    Utils.loadWinSize(getContext(), mWinSize);
    mGridPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    
//TODO drawGrid 繪製網格:release:
    HelpDraw.drawGrid(canvas, mWinSize, mGridPaint);
//TODO drawCoo 繪製座標系:release:
    HelpDraw.drawCoo(canvas, mCoo, mWinSize, mGridPaint);
複製代碼
2).網格--路徑輔助:com.toly1994.c.view.analyze.HelpPath#gridPath
/**
 * 繪製網格:注意只有用path才能繪製虛線
 *
 * @param step 小正方形邊長
 * @param winSize 屏幕尺寸
 */
public static Path gridPath(int step, Point winSize) {
    Path path = new Path();
    for (int i = 0; i < winSize.y / step + 1; i++) {
        path.moveTo(0, step * i);
        path.lineTo(winSize.x, step * i);
    }
    for (int i = 0; i < winSize.x / step + 1; i++) {
        path.moveTo(step * i, 0);
        path.lineTo(step * i, winSize.y);
    }
    return path;
}
複製代碼
2).網格--繪製:com.toly1994.c.view.analyze.HelpDraw#drawGrid
/**
 * 繪製網格
 * @param canvas 畫布
 * @param winSize 屏幕尺寸
 * @param paint 畫筆
 */
public static void drawGrid(Canvas canvas, Point winSize, Paint paint) {
    //初始化網格畫筆
    paint.setStrokeWidth(2);
    paint.setColor(Color.GRAY);
    paint.setStyle(Paint.Style.STROKE);
    //設置虛線效果new float[]{可見長度, 不可見長度},偏移值
    paint.setPathEffect(new DashPathEffect(new float[]{10, 5}, 0));
    canvas.drawPath(HelpPath.gridPath(50, winSize), paint);
}
複製代碼
3).輔助--獲取屏幕尺寸:com.toly1994.c.view.analyze.Utils#loadWinSize
/**
 * 得到屏幕高度
 *
 * @param ctx 上下文
 * @param winSize 屏幕尺寸
 */
public static void loadWinSize(Context ctx, Point winSize) {
    WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics outMetrics = new DisplayMetrics();
    if (wm != null) {
        wm.getDefaultDisplay().getMetrics(outMetrics);
    }
    winSize.x = outMetrics.widthPixels;
    winSize.y = outMetrics.heightPixels;
}
複製代碼
4).座標系--路徑:com.toly1994.c.view.analyze.HelpPath#cooPath
/**
 * 座標系路徑
 *
 * @param coo     座標點
 * @param winSize 屏幕尺寸
 * @return 座標系路徑
 */
public static Path cooPath(Point coo, Point winSize) {
    Path path = new Path();
    //x正半軸線
    path.moveTo(coo.x, coo.y);
    path.lineTo(winSize.x, coo.y);
    //x負半軸線
    path.moveTo(coo.x, coo.y);
    path.lineTo(coo.x - winSize.x, coo.y);
    //y負半軸線
    path.moveTo(coo.x, coo.y);
    path.lineTo(coo.x, coo.y - winSize.y);
    //y負半軸線
    path.moveTo(coo.x, coo.y);
    path.lineTo(coo.x, winSize.y);
    return path;
}
複製代碼
5).座標系--繪製:com.toly1994.c.view.analyze.HelpDraw#drawCoo
/**
 * 繪製座標系
 * @param canvas  畫布
 * @param coo     座標系原點
 * @param winSize 屏幕尺寸
 * @param paint   畫筆
 */
public static void drawCoo(Canvas canvas, Point coo, Point winSize, Paint paint) {
    //初始化網格畫筆
    paint.setStrokeWidth(4);
    paint.setColor(Color.BLACK);
    paint.setStyle(Paint.Style.STROKE);
    //設置虛線效果new float[]{可見長度, 不可見長度},偏移值
    paint.setPathEffect(null);
    //繪製直線
    canvas.drawPath(HelpPath.cooPath(coo, winSize), paint);
    //左箭頭
    canvas.drawLine(winSize.x, coo.y, winSize.x - 40, coo.y - 20, paint);
    canvas.drawLine(winSize.x, coo.y, winSize.x - 40, coo.y + 20, paint);
    //下箭頭
    canvas.drawLine(coo.x, winSize.y, coo.x - 20, winSize.y - 40, paint);
    canvas.drawLine(coo.x, winSize.y, coo.x + 20, winSize.y - 40, paint);
    //爲座標系繪製文字
    drawText4Coo(canvas, coo, winSize, paint);
}
/**
 * 爲座標系繪製文字
 *
 * @param canvas  畫布
 * @param coo     座標系原點
 * @param winSize 屏幕尺寸
 * @param paint   畫筆
 */
private static void drawText4Coo(Canvas canvas, Point coo, Point winSize, Paint paint) {
    //繪製文字
    paint.setTextSize(50);
    canvas.drawText("x", winSize.x - 60, coo.y - 40, paint);
    canvas.drawText("y", coo.x - 40, winSize.y - 60, paint);
    paint.setTextSize(25);
    //X正軸文字
    for (int i = 1; i < (winSize.x - coo.x) / 50; i++) {
        paint.setStrokeWidth(2);
        canvas.drawText(100 * i + "", coo.x - 20 + 100 * i, coo.y + 40, paint);
        paint.setStrokeWidth(5);
        canvas.drawLine(coo.x + 100 * i, coo.y, coo.x + 100 * i, coo.y - 10, paint);
    }
    //X負軸文字
    for (int i = 1; i < coo.x / 50; i++) {
        paint.setStrokeWidth(2);
        canvas.drawText(-100 * i + "", coo.x - 20 - 100 * i, coo.y + 40, paint);
        paint.setStrokeWidth(5);
        canvas.drawLine(coo.x - 100 * i, coo.y, coo.x - 100 * i, coo.y - 10, paint);
    }
    //y正軸文字
    for (int i = 1; i < (winSize.y - coo.y) / 50; i++) {
        paint.setStrokeWidth(2);
        canvas.drawText(100 * i + "", coo.x + 20, coo.y + 10 + 100 * i, paint);
        paint.setStrokeWidth(5);
        canvas.drawLine(coo.x, coo.y + 100 * i, coo.x + 10, coo.y + 100 * i, paint);
    }
    //y負軸文字
    for (int i = 1; i < coo.y / 50; i++) {
        paint.setStrokeWidth(2);
        canvas.drawText(-100 * i + "", coo.x + 20, coo.y + 10 - 100 * i, paint);
        paint.setStrokeWidth(5);
        canvas.drawLine(coo.x, coo.y - 100 * i, coo.x + 10, coo.y - 100 * i, paint);
    }
}
複製代碼

2、Canvas繪製基礎圖形(若是以爲簡單可跳過)

之前看到一個類有不少方法都有些不耐煩,這麼多,怎麼記得住。
如今看到一個類有不少方法,--哇,太好了,哈哈,居然連這方法都有,做者真給力省的我實現了。
Canvas圖形繪製的API,全部的我分了一下類:以下(顏色、點、線、矩形、類圓、文字、圖片、其餘)
下面一一介紹:bash

Canvas繪製API


1.繪製顏色

繪製顏色

/**
     * 繪製顏色(注意在畫座標系前繪製,不然後者覆蓋)
     * @param canvas
     */
    private void drawColor(Canvas canvas) {
//        canvas.drawColor(Color.parseColor("#E0F7F5"));
//        canvas.drawARGB(255, 224, 247, 245);
//        三者等價
        canvas.drawRGB(224, 247, 245);
    }
複製代碼

繪製顏色.png


2.繪製點

繪製點.png

/**
     * 繪製點
     * @param canvas
     */
    private void drawPoint(Canvas canvas) {
        //繪製點
        canvas.drawPoint(100, 100, mRedPaint);
        ////繪製一組點,座標位置由float數組指定(必須是2的倍數個)
        canvas.drawPoints(new float[]{
                400, 400, 500, 500,
                600, 400, 700, 350,
                800, 300, 900, 300
        }, mRedPaint);
    }
複製代碼

繪製點.png


3.繪製直線

繪製線

/**
 * 繪製線
 * @param canvas
 */
private void drawLine(Canvas canvas) {
    canvas.drawLine(500, 200, 900, 400, mRedPaint);
    //繪製一組點,座標位置由float數組指定(必須是4的倍數個)
    canvas.drawLines(new float[]{
            200, 200, 400, 200,
            400, 200, 200, 400,
            200, 400, 400, 400
    }, mRedPaint);
}
複製代碼

繪製線.png


4.繪製矩形

繪製矩形.png

/**
     * 繪製矩形
     *
     * @param canvas
     */
    private void drawRect(Canvas canvas) {
        canvas.drawRect(100, 100, 500, 300, mRedPaint);
        //等價上行
//        Rect rect = new Rect(100, 100, 500, 300);
//        canvas.drawRect(rect,mRedPaint);
        //(左上右下X圓角,Y圓角)
        canvas.drawRoundRect(100 + 500, 100, 500 + 500, 300, 50, 50, mRedPaint);
    }
複製代碼

繪製矩形.png


5.繪製類圓

繪製類圓.png

/**
     * 繪製類圓
     *
     * @param canvas
     */
    private void drawLikeCircle(Canvas canvas) {
        //繪製圓(矩形邊界,畫筆)
        canvas.drawCircle(650, 200, 100, mRedPaint);
//        canvas.drawOval(100, 100, 500, 300, mRedPaint);
        //等價上行
        //繪製橢圓(矩形邊界,畫筆)
        RectF rect = new RectF(100, 100, 500, 300);
        canvas.drawOval(rect, mRedPaint);

        RectF rectArc = new RectF(100 + 500, 100, 500 + 500, 300);
        //繪製圓弧(矩形邊界,開始角度,掃過角度,使用中心?邊緣兩點與中心連線區域:邊緣兩點連線區域)
        canvas.drawArc(rectArc, 0, 90, true, mRedPaint);

        RectF rectArc2 = new RectF(100 + 500 + 300, 100, 500 + 500 + 300, 300);
        //繪製圓弧(矩形邊界,開始角度,掃過角度,使用中心?邊緣兩點與中心連線區域:邊緣兩點連線區域)
        canvas.drawArc(rectArc2, 0, 90, false, mRedPaint);
    }
複製代碼

繪製類圓.png


6.繪製圖片

繪製圖片.png

/**
     * 繪製圖片
     * @param canvas
     */
    private void drawBitmap(Canvas canvas) {
        //1.定點繪製圖片
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.menu_bg);
        canvas.drawBitmap(bitmap, 100, 100, mRedPaint);
        //2.適用變換矩陣繪製圖片
        Matrix matrix = new Matrix();
        //設置變換矩陣:縮小3倍,斜切0.5,右移150,下移100
        matrix.setValues(new float[]{
                1, 0.5f, 1500 * 3,
                0, 1, 100 * 3,
                0, 0, 3
        });
        canvas.drawBitmap(bitmap, matrix, mRedPaint);

        //3.圖片適用矩形區域不剪裁
        RectF rectf1 = new RectF(100 + 900, 100, 600 + 900, 400);
        canvas.drawBitmap(bitmap, null, rectf1, mRedPaint);

        //4.圖片裁剪出的矩形區域
        Rect rect = new Rect(300, 300, 400, 400);
        //圖片適用矩形區域
        RectF rectf2 = new RectF(100 + 900, 100 + 400, 600 + 900, 400 + 400);
        canvas.drawBitmap(bitmap, rect, rectf2, mRedPaint);
    }

複製代碼

繪製圖片.png


7.繪製Picture

1).一開始挺納悶Picture不就是圖片嗎?而後翻了一下API:微信

/**
 * A Picture records drawing calls (via the canvas returned by beginRecording)
 * and can then play them back into Canvas (via {@link Picture#draw(Canvas)} or
 * {@link Canvas#drawPicture(Picture)}).For most content (e.g. text, lines, rectangles),
 * drawing a sequence from a picture can be faster than the equivalent API
 * calls, since the picture performs its playback without incurring any
 * method-call overhead.
 *
 * <p class="note"><strong>Note:</strong> Prior to API level 23 a picture cannot
 * be replayed on a hardware accelerated canvas.</p>
 
 一個Picture類記錄繪製(經過beginRecording方法返回的Canvas),能夠展現這個Canvas
 到其餘Canvas上(經過Picture#draw(Canvas)或者Canvas#drawPicture(Picture)),
 對於大多數的內容,從picture繪製都要比相應的API要快速,由於picture的展示不會招致方法調用開銷
 在API級別23以前,沒法在硬件加速畫布上展現Picture(自譯,僅供參考)
複製代碼

2).經過一段代碼你就能很清楚它是幹嗎用的ide

若是繪製一個品字,須要這樣:工具

testPicture.png

private void drawPicture(Canvas canvas) {
        canvas.drawRect(100, 0, 200, 100, mRedPaint);
        canvas.drawRect(0, 100, 100, 200, mRedPaint);
        canvas.drawRect(200, 100, 300, 200, mRedPaint);
    }
複製代碼

若是想要複用這個品字形,大多數人知道,平移畫布,複製粘貼,
對於少許的代碼,這還能夠接收,若是是很是複雜的圖形,每次繪製重複的內容,會浪費性能性能

private void drawPicture(Canvas canvas) {
     //建立Picture對象
     Picture picture = new Picture();
     //肯定picture產生的Canvas元件的大小,並生成Canvas元件
     Canvas recodingCanvas = picture.beginRecording(canvas.getWidth(), canvas.getHeight());
     //Canvas元件的操做
     recodingCanvas.drawRect(100, 0, 200, 100, mRedPaint);
     recodingCanvas.drawRect(0, 100, 100, 200, mRedPaint);
     recodingCanvas.drawRect(200, 100, 300, 200, mRedPaint);
     //Canvas元件繪製結束
     picture.endRecording();
     
     canvas.save();
     canvas.drawPicture(picture);//使用picture的Canvas元件
     canvas.translate(0, 300);
     picture.draw(canvas);//同上:使用picture的Canvas元件
     canvas.drawPicture(picture);
     canvas.translate(350, 0);
     canvas.drawPicture(picture);
     canvas.restore();
複製代碼

testPicture2.png

Picture至關於先拍一張照片,而且是在別的Canvas上,在別的Canvas上,在別的Canvas上!
重要的話說三遍:當須要的時候在貼在當前的canvas上,picture繪製的優點就是節能減排
當有大量複雜內容須要複用,Picture這個的canvas元件是不二的選擇:


8.繪製文字(文字的效果有Paint決定,細節將在Paint篇介紹)

繪製文字

/**
     * 繪製文字
     *
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        mRedPaint.setTextSize(100);
        canvas.drawText("張風捷特烈--Toly", 200, 300, mRedPaint);
    }
複製代碼

繪製文字.png

無聊的代碼終於敲完了,進入正題。


3、Canvas的畫布變換

之前對Canvas的變換很厭倦,如今看了鍵值是神技
做爲一代PS大神的我,理解Canvas狀態保存與恢復本應易如反掌,爲什麼最近才豁然開朗

1.先看下面的圖形:將座標系原點設爲(500,500)
private void stateTest(Canvas canvas) {
        canvas.drawLine(mCoo.x + 500, mCoo.y + 200, mCoo.x + 900, mCoo.y + 400, mRedPaint);
//        canvas.rotate(45);
        canvas.drawRect(mCoo.x + 100, mCoo.x + 100, mCoo.y + 300, mCoo.y + 200, mRedPaint);
    }
複製代碼

狀態測試1.png

問題來了,想畫一個斜45度的矩形怎麼辦?
貌似沒有斜矩形的API,一個一個點找,貌似太麻煩了,我把紙轉一下不就好了嗎!
紙就是Canvas,看一下API,果真有rotate()方法,懷着忐忑的心情:

private void stateTest(Canvas canvas) {
        canvas.drawLine(mCoo.x + 500, mCoo.y + 200, mCoo.x + 900, mCoo.y + 400, mRedPaint);
        canvas.rotate(45);
        canvas.drawRect(mCoo.x + 100, mCoo.x + 100, mCoo.y + 300, mCoo.y + 200, mRedPaint);
    }
複製代碼

果真轉得天翻地覆

狀態測試旋轉.png


2.圖層的概念

PS中的圖層可謂PS的精華,它保證了在一個圖層中繪製而不會影響到其餘的圖層
在Canvas中每次的save()都存將先前的狀態保存下來,產生一個新的繪圖層,
咱們能夠爲所欲爲地地畫而不會影響其餘已畫好的圖,最後用restore()將這個圖層合併到原圖層
這像是棧的概念,每次save(),新圖層入棧(注意能夠save屢次),只有棧頂的層能夠進行操做,restore()彈棧

圖層.png


3.旋轉畫布:rotate()
private void stateTest(Canvas canvas) {
        canvas.drawLine(mCoo.x + 500, mCoo.y + 200, mCoo.x + 900, mCoo.y + 400, mRedPaint);
        canvas.drawRect(mCoo.x + 100, mCoo.x + 100, mCoo.y + 300, mCoo.y + 200, mRedPaint);
        canvas.save();//保存canvas狀態
        //(角度,中心點x,中心點y)
        canvas.rotate(45, mCoo.x + 100, mCoo.y + 100);
        mRedPaint.setColor(Color.parseColor("#880FB5FD"));
        canvas.drawRect(mCoo.x + 100, mCoo.x + 100, mCoo.y + 300, mCoo.y + 200, mRedPaint);
        canvas.restore();//圖層向下合併
    }
複製代碼

定點旋轉.png

4.平移畫布:translate():寫一堆mCoo,也就是讓畫布移動一下而已

效果必變,是否是清爽許多

private void stateTest(Canvas canvas) {
        canvas.save();
        canvas.translate(mCoo.x, mCoo.y);//將原點平移到座標系原點
        canvas.drawLine(500, 200, 900, 400, mRedPaint);
        canvas.drawRect(100, 100, 300, 200, mRedPaint);
        canvas.save();//保存canvas狀態
        //(角度,中心點x,中心點y)
        canvas.rotate(45, 100, 100);
        mRedPaint.setColor(Color.parseColor("#880FB5FD"));
        canvas.drawRect(100, 100, 300, 200, mRedPaint);
        canvas.restore();//圖層向下合併
        canvas.restore();
    }
複製代碼

平移.png

5.縮放畫布:scale()
private void stateTest(Canvas canvas) {
        canvas.save();
        canvas.translate(mCoo.x, mCoo.y);//將原點平移到座標系原點
        canvas.drawLine(500, 200, 900, 400, mRedPaint);
        canvas.drawRect(100, 100, 300, 200, mRedPaint);
        canvas.save();//保存canvas狀態
        //(角度,中心點x,中心點y)
        canvas.scale(2, 2, 100, 100);
        mRedPaint.setColor(Color.parseColor("#880FB5FD"));
        canvas.drawRect(100, 100, 300, 200, mRedPaint);
        canvas.restore();//圖層向下合併
        canvas.restore();
    }
複製代碼

定點縮放.png

6.斜切畫布:scale()
private void stateTest(Canvas canvas) {

        canvas.save();
        canvas.translate(mCoo.x, mCoo.y);//將原點平移到座標系原點
        canvas.drawLine(500, 200, 900, 400, mRedPaint);
        canvas.drawRect(100, 100, 300, 200, mRedPaint);
        canvas.save();//保存canvas狀態
        canvas.skew(1f,0f);
        mRedPaint.setColor(Color.parseColor("#880FB5FD"));
        canvas.drawRect(100, 100, 300, 200, mRedPaint);
        canvas.restore();//圖層向下合併
        canvas.restore();
    }
複製代碼

斜切.png

7.畫布選擇保存狀態:

canvas保存狀態.png

public int save (int saveFlags)
默認:MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG

ALL_SAVE_FLAG	            保存所有狀態
CLIP_SAVE_FLAG	            過時---僅保存剪輯區(不做爲圖層)
CLIP_TO_LAYER_SAVE_FLAG	    過時---僅剪裁區做爲圖層保存
FULL_COLOR_LAYER_SAVE_FLAG	過時---僅保存圖層的所有色彩通道
HAS_ALPHA_LAYER_SAVE_FLAG	過時---僅保存圖層的alpha(不透明度)通道
MATRIX_SAVE_FLAG	        過時---僅保存Matrix信息( translate, rotate, scale, skew)
複製代碼
int count = canvas.getSaveCount();//獲取圖層的個數3
    canvas.restoreToCount(1);//直接恢復到第幾個圖層
複製代碼
4、Canvas的裁剪
1.可見主要就兩種類型,內裁剪和外裁剪,Op的操做被廢棄了

canvas剪裁.png

2.內剪裁:(區域內的以後繪製的內容保存)
private void clip(Canvas canvas) {
        //剪裁區域
        Rect rect = new Rect(20, 100, 250, 300);
        canvas.clipRect(rect);
        canvas.drawRect(0, 0, 200, 300, mRedPaint);
    }
複製代碼

內剪裁.png

3.外剪裁:(區域外的以後繪製的內容保存)--注意API26及以上可用
private void clip(Canvas canvas) {
        //剪裁區域
        Rect rect = new Rect(20, 100, 250, 300);
        canvas.clipOutRect(rect);
        canvas.drawRect(0, 0, 200, 300, mRedPaint);
    }
複製代碼

外剪裁.png

Canvas的相關內容就到這裏(注:路徑的繪製會在Path篇精講),下一節將帶來畫筆Paint的知識


後記:捷文規範

1.本文成長記錄及勘誤表
項目源碼 日期 備註
V0.1--無 2018-11-5 Android關於Canvas你所知道的和不知道的一切
V0.2--無 2018-11-6 增長繪製Picture的內容
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
個人github 個人簡書 個人CSDN 我的網站
3.聲明

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大編程愛好者共同交流
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持


icon_wx_200.png
相關文章
相關標籤/搜索