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);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章