Canvas詳解

一、概念

畫布,通過畫筆繪製幾何圖形、文本、路徑和位圖等。

二、常用api類型

常用api分爲繪製、變換、狀態保存和恢復

2.1 繪製集合圖形,文本,位圖等

//在指定座標繪製位圖
public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint)

//根據指定的起始點、結束點之間繪製連線
public void drawLine(float startX, float startY, float stopX, float stopY,
        @NonNull Paint paint)
        
//根據指定的path,繪製連線
public void drawPath(@NonNull Path path, @NonNull Paint paint)

//根據指定的座標,繪製點
public void drawPoint(float x, float y, @NonNull Paint paint)

//根據指定的座標,繪製文字
public void drawText(@NonNull String text, int start, int end, float x, float y,
        @NonNull Paint paint)

2.2 位置、形狀變換

//平移操作
void translate(float dx, float dy)

//縮放操作
void scale(float sx, float sy)

//旋轉操作
void rotate(float degrees)

//傾斜操作
void skew(float sx, float sy)

//切割操作,參數指定區域內不可以繪製
void clipxxx(......)

//反向切割操作,參數指定區域內不可以繪製
void clipOutxxx(......)

//可通過matrix實現平移、縮放、旋轉等操作
void setMatrix(@Nullable Matrix matrix)

2.2.1 平移操作

首先要初始化畫筆Paint:

private void init(){
    mPaint = new Paint();
    mPaint.setColor(Color.RED);
    mPaint.setStrokeWidth(4);
    mPaint.setStyle(Paint.Style.STROKE);
}

然後進行平移操作:

//1.平移操作:先畫出矩形,再平移、改色畫新矩形
canvas.drawRect(0,0,100,100,mPaint);
canvas.translate(50,50);
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,100,100,mPaint);

效果:
在這裏插入圖片描述
矩形經過了平移,在draw的時候傳入的起始點座標爲(0,0),但是translate函數將其平移了(50,50),於是就出現了這樣的效果。我們還可以在下面再繪製一條直線來驗證:

//1.平移操作:先畫出矩形,再平移、改色畫新矩形
canvas.drawRect(0,0,100,100,mPaint);
canvas.translate(50,50);
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,100,100,mPaint);
canvas.drawLine(0,0,200,200,mPaint);//畫直線

在這裏插入圖片描述
以後繪製任何內容,都是經過translate平移後開始的。

2.2.2 縮放操作

scale()方法,參數需要傳入x、y方向的縮放比例。這裏我們全部都傳入0.5f:

//2.縮放操作
canvas.drawRect(100,100,300,300,mPaint);
canvas.scale(0.5f,0.5f);
mPaint.setColor(Color.GRAY);
canvas.drawRect(100,100,300,300,mPaint);

在這裏插入圖片描述

這個scale()方法會將畫布進行縮小,因此纔會出現這種效果。
也有一個重載方法:

//2.縮放操作
        canvas.drawRect(100,100,300,300,mPaint);
        canvas.scale(0.5f,0.5f,200,200);
        
        //以下三行和上面一行效果一樣
//        canvas.translate(200,200);
//        canvas.scale(0.5f,0.5f);
//        canvas.translate(-200,-200);

        mPaint.setColor(Color.GRAY);
        canvas.drawRect(100,100,300,300,mPaint);

在這裏插入圖片描述
這裏的效果有點區別,矩形的長寬縮小了,但是繪製起點發生了改變。這個方法的意義就是先translate操作,後scale操作,最後反向translate。

2.2.3 旋轉操作

rotate方法,參數傳遞是旋轉角度。默認順時針旋轉,旋轉後再去繪製矩形。

//3.旋轉操作
canvas.drawRect(0,0,300,300,mPaint);
canvas.rotate(45);
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,300,300,mPaint);

當然我們也可以先將畫布進行平移操作後再看一下效果:

//3.旋轉操作
canvas.translate(50,50);//平移畫布
canvas.drawRect(0,0,300,300,mPaint);
canvas.rotate(45);
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,300,300,mPaint);

在這裏插入圖片描述
旋轉中心,是平移之後的原點。

當然,rotate方法也有重載方法,參數分別表示:旋轉角度、旋轉中心的x座標、旋轉中心的y座標:

//先畫矩形,然後將旋轉圓心定位矩形中心,改變畫筆顏色,重新繪製矩形
canvas.drawRect(200,200,600,600,mPaint);
canvas.rotate(45,400,400);
mPaint.setColor(Color.GRAY);
canvas.drawRect(200,200,600,600,mPaint);

2.2.4 傾斜操作

skew方法,參數表示x、y方向的tan值

//4.傾斜操作
canvas.drawRect(0,0,600,600,mPaint);
canvas.skew(1,0); //在x方向傾斜45度
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,600,600,mPaint);

效果就是:在x方向傾斜45度,就是y軸逆時針旋轉45度
在這裏插入圖片描述
如果是y方向傾斜45度的話:

//4.傾斜操作
canvas.drawRect(0,0,600,600,mPaint);
canvas.skew(0,1); //在y方向傾斜45度
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,600,600,mPaint);

這個效果就是x軸順時針旋轉45度。

2.2.5 切割

可以切割矩形、路徑。切割矩形,需要傳入一個需要切割的矩形區域。首先確定切割區域,接下來的操作只會在這個切割區域內有效

//5.切割操作
canvas.drawRect(20,20,70,70,mPaint);
mPaint.setColor(Color.GRAY);
canvas.drawRect(20,80,70,100,mPaint);
canvas.clipRect(20,20,70,70); //畫布被裁減
canvas.drawCircle(10,10,10,mPaint); //座標超出裁減區域,無法繪製

原本應該繪製出來的圓並沒有出現,因爲我們已經通過clipRect切割了畫布,圓的繪製並不在切割區域,所以無法繪製顯示。
在這裏插入圖片描述
現在如果在裁減區域內畫圓:

//5.切割操作
        canvas.drawRect(200,200,700,700,mPaint);
        mPaint.setColor(Color.GRAY);
        canvas.drawRect(200,800,700,1000,mPaint);
        canvas.clipRect(200,200,700,700);
//        canvas.drawCircle(100,100,100,mPaint);
        canvas.drawCircle(300,300,100,mPaint);

圓的座標位於裁減區域內,成功繪製出來了。

反向裁減

//反向裁減
canvas.drawRect(200,200,700,700,mPaint);
mPaint.setColor(Color.GRAY);
canvas.drawRect(200,800,700,1000,mPaint);
canvas.clipOutRect(200,200,700,700);
canvas.drawCircle(100,100,100,mPaint);
canvas.drawCircle(300,300,100,mPaint);

這樣的操作和上面的操作效果是相反的。裁減區域以外的才能正常繪製,裁減區域內的繪製無效。

2.2.6 矩陣

//6. matrix
canvas.drawRect(200,200,700,700,mPaint);
Matrix matrix = new Matrix();
matrix.setTranslate(50,50); //平移
//        matrix.setRotate(45); //旋轉45度
//        matrix.setScale(0.5f,0.5f); //縮放
canvas.setMatrix(matrix);
mPaint.setColor(Color.GRAY);
canvas.drawRect(200,200,700,700,mPaint);

2.3 狀態保存和恢復

Canvas調用translate、scale、rotate、skew、clipRect等變換後,後續的操作都是基於變換後的Canvas,都收到了影響,對以後的操作不利。Canvas提供了sava、saveLayer、saveLayerAlpha、restore、restoreToCount來保存和恢復狀態

//繪製矩形
canvas.drawRect(200,200,700,700,mPaint);
//平移
canvas.translate(50,50);
//更改畫筆顏色,繪製另一個矩形
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,500,500,mPaint);

運行以上代碼,看一下初步效果。
在這裏插入圖片描述
如果我將畫布反向平移後,再畫一條線:

//繪製矩形
canvas.drawRect(200,200,700,700,mPaint);
//平移
canvas.translate(50,50);
//更改畫筆顏色,繪製另一個矩形
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,500,500,mPaint);

//我想把畫布再平移回去
canvas.translate(-50,-50);
canvas.drawLine(0,0,400,500,mPaint);

在這裏插入圖片描述
如果在初次平移畫布後,我想在原點畫一條線,需要先進行反向平移。針對這種情況,需要用到canvas提供的save方法進行狀態的保存、restore方法進行狀態恢復:

//繪製矩形
canvas.drawRect(200,200,700,700,mPaint);
//保存狀態
canvas.save();
//平移
canvas.translate(50,50);
//更改畫筆顏色,繪製另一個矩形
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,500,500,mPaint);

//無需反向平移,直接恢復狀態
canvas.restore();
canvas.drawLine(0,0,400,500,mPaint);

效果和上面的效果是一樣的。調用sava後,無需關心後續會進行什麼操作,直接一個restore方法就可以進行狀態恢復。

restore方法可以多次調用:

//繪製矩形
canvas.drawRect(200,200,700,700,mPaint);
//保存狀態
canvas.save();
//平移
canvas.translate(50,50);
//更改畫筆顏色,繪製另一個矩形
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,500,500,mPaint);

canvas.save();
canvas.translate(50,50);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,500,500,mPaint);

//無需反向平移,直接恢復狀態
canvas.restore();
canvas.drawLine(0,0,400,500,mPaint);

①保存畫布的狀態,將畫布平移到(50,50),繪製一個藍色矩形;②保存畫布狀態,將畫布平移到(50,50),畫一個綠色矩形;③恢復畫布狀態,畫一條直線
經過運行發現,restore將畫布的狀態恢復到了上一次sava時,直線從(50,50)處繪製。如果想讓直線從原點開始繪製,需要再額外多調用一次restore方法將畫布狀態恢復到最初。
效果:

實際上,Canvas內部維護了一個狀態棧,每調用一次sava方法都會進行一次壓棧操作。當調用restore方法後會將狀態恢復到上一次,也就是把最頂層的狀態進行出棧操作。

當然,我們也可以通過getSaveCount()方法來查看保存的狀態的個數(默認保存的狀態初始值爲1,restore最小隻能將狀態數恢復至1,繼續調用會報錯)。有興趣的可以通過打印日誌查看保存狀態的個數。

restoreToCount()方法可以將狀態恢復到指定的狀態下,這個狀態是由sava方法所返回的:

//繪製矩形
canvas.drawRect(200,200,700,700,mPaint);
//保存狀態
int state = canvas.save();
//平移
canvas.translate(50,50);
//更改畫筆顏色,繪製另一個矩形
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,500,500,mPaint);

canvas.save();
canvas.translate(50,50);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,500,500,mPaint);

//將狀態恢復到指定的程度
canvas.restoreToCount(state);

除了使用save保存狀態之外,還可以使用saveLayer方法保存狀態,同樣返回int類型的值。保存之後同樣可以通過restoreToCount,將畫布狀態恢復至指定程度。在圖層混合模式的離屏繪製時用到了它,它會新創建一個圖層,將saveLayer和restoreToCount之間的代碼先繪製到圖層上,然後再將最後的這個圖層繪製到Canvas上。

值得注意的是,saveLayer方法可以指定圖層的大小:

//先繪製一個矩形
canvas.drawRect(200,200,700,700,mPaint);

int layerId = canvas.saveLayer(0, 0, 700, 700, mPaint);
//將畫筆調整顏色
mPaint.setColor(Color.BLUE);
//使用Matrix實現變換
Matrix matrix = new Matrix();
//移動到指定座標
matrix.setTranslate(100,100);
canvas.setMatrix(matrix);
//繪製矩形
canvas.drawRect(0,0,700,700,mPaint);
//調用
canvas.restoreToCount(layerId);

//繪製一個矩形
mPaint.setColor(Color.RED);
canvas.drawRect(0,0,100,100,mPaint);

運行效果如下:
在這裏插入圖片描述
首先繪製了一個(200,200,700,700)的矩形,然後創建了(0, 0, 700, 700)的圖層。 移動畫布,繪製與圖層大小等同的矩形,但是沒有繪製完全,右下兩個邊已經超出了圖層範圍。恢復狀態,重新再原點繪製一個矩形。

三、案例

粒子散落效果,得到bitmap的水平、豎直方向上各有多少個像素。通過bitmap的getPixel方法得到指定座標位置的像素的顏色值,如此就能將每一個點都封裝到粒子對象中。

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);
bitmap.getWidth(); //寬表示水平方向有多少像素
bitmap.getHeight(); //高表示豎直方向有多少像素
int pixel = bitmap.getPixel(0, 0);//返回當前位置像素的顏色值

首先我們來定義這麼一個粒子對象:

/**
 * 粒子封裝對象
 */
public class Ball {

    /**
     * 這些已經能完全描述出一個圓
     */
    //像素點的顏色值
    public int color;
    //粒子圓心座標
    public float x,y;
    //粒子半徑
    public float r;


    /**
     * 讓圓動起來,進行位置變換。變換過程,模仿自由落體運動。
     * 所以要定義加速度、速度
     */
    //運動的x、y方向的速度
    public float vX,vY;
    //運動的x、y方向的加速度
    public float aX,aY;
}

接下來創建自定義view:

//遍歷整張bitmap的每個像素點,將其相關屬性封裝到Ball對象中。
//在onDraw方法中繪製這個List<Ball>集合中的每個粒子
//現在的效果,繪製的並非是一個圖片,而是很多個圓
private void init(){
    mPaint = new Paint();
    mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);
    //循環遍歷這個bitmap的每個像素
    for (int i = 0; i < mBitmap.getWidth(); i++) {
        for (int j = 0; j < mBitmap.getHeight(); j++) {
            Ball ball = new Ball();
            //將每個像素點的顏色交給Ball對象
            ball.color=mBitmap.getPixel(i,j);
            //粒子的圓心座標x、y
            ball.x=i*d+d/2;
            ball.y=j*d+d/2;
            //粒子半徑
            ball.r=d/2;

            list.add(ball);
        }
    }
}
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    //先平移畫布
    canvas.translate(500,500);
    
    //遍歷集合,實現繪製
    for (Ball ball:list){
        mPaint.setColor(ball.color);
        canvas.drawCircle(ball.x,ball.y,ball.r,mPaint);
    }
}

繼續,當點擊圖片時需要產生爆炸碎裂效果,此時需要重寫onTouchEvent方法:

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction()==MotionEvent.ACTION_DOWN){
        //執行屬性動畫
    }
    return super.onTouchEvent(event);
}

接下來需要定義屬性動畫:

mValueAnimator = ValueAnimator.ofFloat(0, 1); //從0到1開始變換
mValueAnimator.setRepeatCount(-1); //重複運行
mValueAnimator.setDuration(2000); //執行時間
mValueAnimator.setInterpolator(new LinearInterpolator()); //設置線性插值器
//設置監聽
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        updateBall(); //更新粒子
        invalidate(); //觸發onDraw方法
    }
});

關於更新粒子的updateBall()方法,需要遍歷List集合,不斷的修改粒子的速度、位置

/**
 * 更新粒子
 */
private void updateBall() {

    for (Ball ball:list){
        //更新粒子的位置
        ball.x+=ball.vX;
        ball.y+=ball.vY;

        //更新粒子速度
        ball.vX+=ball.aX;
        ball.vY+=ball.aY;
    }
}

別忘了,在init()的for循環的時候,需要對速度、加速度都要進行初始化,給他們一個初始值:

//初始化粒子的速度、加速度。可以看出來,速度爲某一個範圍的隨機值,
//加速度初始值水平方向爲0,豎直方向爲0.98f
//速度(-20,20)
ball.vX = (float) (Math.pow(-1, Math.ceil(Math.random() * 1000)) * 20 * Math.random());
ball.vY = rangInt(-15, 35);
//加速度
ball.aX = 0;
ball.aY = 0.98f;

速度、加速度搞定之後,接下來就該在DOWN事件中調用方法:

//執行屬性動畫
mValueAnimator.start();

點擊DOWN事件後,會觸發屬性動畫。在回調中會更新粒子的位置,之後繼續觸發onDraw方法。

完整代碼如下:

Ball.java

/**
 * 粒子封裝對象
 */
public class Ball {

    /**
     * 這些已經能完全描述出一個圓
     */
    //像素點的顏色值
    public int color;
    //粒子圓心座標
    public float x,y;
    //粒子半徑
    public float r;


    /**
     * 讓圓動起來,進行位置變換。變換過程,模仿自由落體運動。
     * 所以要定義加速度、速度
     */
    //運動的x、y方向的速度
    public float vX,vY;
    //運動的x、y方向的加速度
    public float aX,aY;
}

SplitView.java

public class SplitView extends View {

    private Paint mPaint;
    private Bitmap mBitmap;
    //負責接收Bitmap對象轉換的Ball對象
    private List<Ball> list = new ArrayList<>();
    private float d=3;//粒子直徑
    private ValueAnimator mValueAnimator;

    public SplitView(Context context) {
        super(context);
        init();
    }

    public SplitView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public SplitView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        mPaint = new Paint();
        mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);
        //循環遍歷這個bitmap的每個像素
        for (int i = 0; i < mBitmap.getWidth(); i++) {
            for (int j = 0; j < mBitmap.getHeight(); j++) {
                Ball ball = new Ball();
                //將每個像素點的顏色交給Ball對象
                ball.color=mBitmap.getPixel(i,j);
                //粒子的圓心座標x、y
                ball.x=i*d+d/2;
                ball.y=j*d+d/2;
                //粒子半徑
                ball.r=d/2;

                //初始化粒子的速度、加速度
                //速度(-20,20)
                ball.vX = (float) (Math.pow(-1, Math.ceil(Math.random() * 1000)) * 20 * Math.random());
                ball.vY = rangInt(-15, 35);
                //加速度
                ball.aX = 0;
                ball.aY = 0.98f;

                list.add(ball);
            }
        }
        mValueAnimator = ValueAnimator.ofFloat(0, 1); //從0到1開始變換
        mValueAnimator.setRepeatCount(-1); //重複運行
        mValueAnimator.setDuration(2000); //執行時間
        mValueAnimator.setInterpolator(new LinearInterpolator()); //設置線性插值器
        //設置監聽
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                updateBall(); //更新粒子
                invalidate(); //觸發onDraw方法
            }
        });
    }

    /**
     * 更新粒子
     */
    private void updateBall() {

        for (Ball ball:list){
            //更新粒子的位置
            ball.x+=ball.vX;
            ball.y+=ball.vY;

            //更新粒子速度
            ball.vX+=ball.aX;
            ball.vY+=ball.aY;
        }
    }

    private int rangInt(int i, int j) {
        int max = Math.max(i, j);
        int min = Math.min(i, j) - 1;
        //在0到(max - min)範圍內變化,取大於x的最小整數 再隨機
        return (int) (min + Math.ceil(Math.random() * (max - min)));
    }

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

        //先平移畫布
        canvas.translate(500,500);

        //遍歷集合,實現繪製
        for (Ball ball:list){
            mPaint.setColor(ball.color);
            canvas.drawCircle(ball.x,ball.y,ball.r,mPaint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction()==MotionEvent.ACTION_DOWN){
            //執行屬性動畫
            mValueAnimator.start();
        }
        return super.onTouchEvent(event);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章