Android自定义控件入门到精通--Canvas(画布)

《Android自定义控件入门到精通》文章索引 ☞ https://blog.csdn.net/Jhone_csdn/article/details/118146683

《Android自定义控件入门到精通》所有源码 ☞ https://gitee.com/zengjiangwen/Code

Canvas(画布)

在这里插入图片描述

画布:在Ps中,画布就是你的操作空间,画布之上有图层

在这里插入图片描述

图层:“我是谁”分别写在三个透明图层上,由于所有图层都是透明的,所以上层图层不会遮挡底下两层的内容,我们看见的还是全部的文字

在这里插入图片描述

我们还可以对选中的图层进行平移,缩放,旋转,斜切,还可以对整个画布进行裁剪

在这里插入图片描述

在Andrond的Canvas中,Canvas表示画布,并且画布之上也有图层

Android的Canvas和Ps中的画布及图层有什么区别呢?

我总结了一下,大家可以根据后面的例子去验证和加深理解

Ps中的画布及图层:

  • 平移、缩放、旋转、斜切,作用的对象是当前选中的图层(可选中多个图层)
  • 裁剪作用的对象是整个画布,也就是对所有图层都起作用

Android中的画布及图层:

  • 每次调用Canvas.drawxxx()方法,都是新建一层透明图层,并在上面绘图
  • Canvas中的平移、缩放、旋转、斜切,裁剪针对的是画布,而非图层,且对已经绘制好的图层没影响,只会影响后面新建的图层

translate (平移)

Canvas.translate(dx,dy)

  • dx:在X轴上平移的距离
  • dy:在Y轴上平移的距离
    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(6);
        //新建图层,画矩形
        canvas.drawRect(0,0,100,60,mPaint);
        //平移画布,只影响后面新建的图层
        canvas.translate(150,150);
        mPaint.setColor(Color.GREEN);
         //新建图层,画矩形
        canvas.drawRect(0,0,100,60,mPaint);
    }

在这里插入图片描述

可以看到,红色矩形并没有平移,而只有绿色矩形所在图层平移了,也就是说新建的绿色矩形所在图层受到了Canvas平移后的影响,而Canvas平移前的红色矩形所在图层不会被影响

那如果我们接着画一个蓝色矩形,那么蓝色矩形所在图层会不会受到Canvas平移后的影响呢?

    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(6);
        //新建图层,画矩形
        canvas.drawRect(0,0,100,60,mPaint);
       //平移画布,只影响后面新建的图层
        canvas.translate(150,150);
        mPaint.setColor(Color.GREEN);
      	 //新建图层,画矩形
        canvas.drawRect(0,0,100,60,mPaint);
        mPaint.setColor(Color.BLUE);
         //新建图层,画矩形
        canvas.drawRect(0,0,150,80,mPaint);
    }

在这里插入图片描述

可以看到,蓝色矩形也被影响了,注意了,我们所有矩形都是以(0,0)为起点的

结论:平移操作是针对画布的,而非单个图层,平移前的图层不受影响,平移后所有新建的图层都会被影响,且平移后的坐标会作为新图层的原点(0,0)

旋转、缩放、斜切都是一个道理

rotate(旋转)

@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(Color.RED);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(6);
    //为了好观察效果,平移画布,使后面所有新建的图层都将以(200,100)的位置为原点(0,0)
    canvas.translate(200,100);
    //新建图层,画矩形
    canvas.drawRect(0,0,100,60,mPaint);
    //将画布旋转45度
    canvas.rotate(45);
    mPaint.setColor(Color.GREEN);
    //新建图层,画矩形
    canvas.drawRect(0,0,100,60,mPaint);
    mPaint.setColor(Color.BLUE);
     //新建图层,画矩形
    canvas.drawRect(0,0,150,80,mPaint);
}

在这里插入图片描述

scale(缩放)

@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(Color.RED);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(6);
    //为了好观察效果,新建一个图层并将原点平移(200,100),后面所有新建的图层都将以(200,100)的位置为原点(0,0)
    canvas.translate(200,100);
    //新建图层,画矩形
    canvas.drawRect(0,0,100,60,mPaint);
    //画布放大两倍
    canvas.scale(2,2);
    mPaint.setColor(Color.GREEN);
    //新建图层,画矩形
    canvas.drawRect(0,0,100,60,mPaint);
    mPaint.setColor(Color.BLUE);
    //新建图层,画矩形
    canvas.drawRect(0,0,150,80,mPaint);
}

在这里插入图片描述

skew(斜切)

@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(Color.RED);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(6);
    //为了好观察效果,新建一个图层并将原点平移(200,100),后面所有新建的图层都将以(200,100)的位置为原点(0,0)
    canvas.translate(200,100);
    //新建图层,画矩形
    canvas.drawRect(0,0,100,60,mPaint);
    //画布斜切,x轴斜切0.2f
    canvas.skew(0.2f,0);
    mPaint.setColor(Color.GREEN);
    //新建图层,画矩形
    canvas.drawRect(0,0,100,60,mPaint);
    mPaint.setColor(Color.BLUE);
    //新建图层,画矩形
    canvas.drawRect(0,0,150,80,mPaint);
}

在这里插入图片描述

斜切是对坐标系的操作,我们不防把斜切前和斜切后的x、y轴画出来看看

@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(Color.RED);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(6);
    //为了好观察效果,新建一个图层并将原点平移(200,100),后面所有新建的图层都将以(200,100)的位置为原点(0,0)
    canvas.translate(200,100);
    //新建图层,画矩形
    canvas.drawRect(0,0,100,60,mPaint);

    //斜切前的坐标系
    canvas.drawLine(0,0,350,0,mPaint);
    canvas.drawText("X",400,20,mPaint);
    canvas.drawLine(0,0,0,350,mPaint);
    canvas.drawText("Y",20,400,mPaint);

    //画布斜切,x轴斜切0.2f
    canvas.skew(0.2f,0);
    mPaint.setColor(Color.GREEN);
    //新建图层,画矩形
    canvas.drawRect(0,0,100,60,mPaint);
    mPaint.setColor(Color.BLUE);
    //新建图层,画矩形
    canvas.drawRect(0,0,150,80,mPaint);

    //斜切后的坐标系
    canvas.drawLine(0,0,350,0,mPaint);
    canvas.drawText("X",400,20,mPaint);
    canvas.drawLine(0,0,0,350,mPaint);
    canvas.drawText("Y",20,400,mPaint);
}

在这里插入图片描述

考验大家数学几何图形功底的时候又到了

公式:

假设有点A(x1,x2),斜切参数(sx,sy),点A斜切后的坐标为(x2,y2),则:

  • x2=sx*y1+x1
  • y2=sy*x1+y1

代码验证:

验证过程

  • 画一个矩形A
  • 通过上面的公式,求得矩形A四个顶点斜切后的坐标并画出
  • 画布进行斜切
  • 斜切后以矩形A的参数画矩形B,若矩形B的四个顶点和斜切后的四个顶点重合,则公式成立
@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(Color.RED);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(4);
    mPaint.setTextSize(20);
    //绘制斜切前坐标系
    canvas.drawLine(0, 0, 500, 0, mPaint);
    canvas.drawText("X", 550, 20, mPaint);
    canvas.drawLine(0, 0, 0, 500, mPaint);
    canvas.drawText("Y", 20, 500, mPaint);
    //画红色矩形
    canvas.drawRect(300, 100, 450, 250, mPaint);
    //矩形的四个顶点为:float[]{300,100,450,100,450,250,300,250}
    float[] points = new float[]{300, 100, 450, 100, 450, 250, 300, 250};
    //根据红色矩形的四个顶点坐标计算斜切后的四个点坐标
    float[] newPoints = calcuSkew(0.5f, 0.5f, points);
    mPaint.setColor(Color.BLUE);
    mPaint.setStrokeWidth(30);
    //画斜切后的四个顶点
    canvas.drawPoints(newPoints, mPaint);
    //画布斜切
    canvas.skew(0.5f, 0.5f);
    mPaint.setColor(Color.GREEN);
    mPaint.setStrokeWidth(4);
    //画绿色矩形,观察绿色矩形四个顶点是否跟四个蓝色点重合,若重合,公式成立
    canvas.drawRect(300, 100, 450, 250, mPaint);
    //绘制斜切后的坐标系
    canvas.drawLine(0, 0, 500, 0, mPaint);
    canvas.drawText("X", 550, 20, mPaint);
    canvas.drawLine(0, 0, 0, 500, mPaint);
    canvas.drawText("Y", 20, 500, mPaint);
}
public float[] calcuSkew(float sx, float sy, float[] points) {
    //     X2=sx*Y1+X1
    //     Y2=sy*X1+Y1
    float[] newPoints = new float[points.length];
    for (int i = 0; i < points.length; i++) {
        if (i % 2 == 0) {
            newPoints[i] = sx * points[i + 1] + points[i];
        } else {
            newPoints[i] = sy * points[i - 1] + points[i];
        }
    }
    return newPoints;
}

在这里插入图片描述

可以看到,我们计算出来的斜切点跟矩形斜切后的顶点重合了,证明公式成立,那么通过这个公式,我们就可以根据效果,推倒出(sx,sy)了。

clip(裁剪)

@Override
protected void onDraw(Canvas canvas) {
    //整个画布填充紫色,观察此时画布大小
    canvas.drawColor(0xff562358);
    mPaint.setColor(Color.GREEN);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(4);
    //新建图层,画一个绿色矩形
    canvas.drawRect(100,50,450,250,mPaint);
    //裁剪画布,画布边界小于上面的矩形,观察绿色矩形是否会被裁剪
    canvas.clipRect(0,0,300,200);
    //裁剪后的画布填充蓝色,带透明度,观察此时画布大小
    canvas.drawColor(0x500000ff);
    mPaint.setColor(Color.RED);
    //超出裁剪后的画布大小画矩形,观察红色矩形是否会被裁剪
    canvas.drawRect(100,100,450,250,mPaint);
}

在这里插入图片描述

可以看到,裁剪前的图层不受影响,而会影响裁剪后的图层

Canvas 状态的保存与恢复

通过上面的学习,我们知道Canvas的平移、旋转、缩放、斜切、裁剪操作,会对后面的所有图层内容有影响。

现有这么一个需求:

  • 先画一个矩形A
  • 然后旋转画布45度后,在画一个矩形B
  • 接着在矩形A的旁边画一个与A平行的矩形C

在这里插入图片描述

这个需求有三种办法:

方法一:(这个方法打乱了需求的步骤A-B-C)

  • 先画矩形A
  • 在画矩形C
  • 然后旋转画布45度后,在画矩形B

方法二:

  • 先画矩形A
  • 然后旋转画布45度后,在画矩形B
  • 然后旋转画布-45度后,在画矩形C

这两种方法现在看还行,但是如果面对非常复杂的绘制流程,这样操作无疑是一场灾难

方法三:

  • 先画矩形A
  • 保存当前画布的状态
  • 然后旋转画布45度后,在画矩形B
  • 恢复画布到之前保存的状态
  • 画矩形C

这时候,不管你画A和C之间的操作多么复杂,是否有其它状态的改变,我都可以直接回到保存的状态,继续在这个状态下绘制

@Override
protected void onDraw(Canvas canvas) {
    //平移画布到中间点位置,好观察效果
    canvas.translate(100,100);
    //画红色矩形
    mPaint.setColor(Color.RED);
    canvas.drawRect(0,0,60,30,mPaint);
    //保存当前画布状态
    int saveId = canvas.save();
    //旋转画布
    canvas.rotate(45);
    //画蓝色矩形
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(0,0,60,30,mPaint);
    //恢复画布状态
    canvas.restore();
    //画绿色矩形
    mPaint.setColor(Color.GREEN);
    canvas.drawRect(70,0,140,30,mPaint);
}

Canvas 多画布

先看个例子

案例一:

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.WHITE);
    //平移画布到中间点位置,好观察效果
    canvas.translate(100,100);
    //画红色矩形
    mPaint.setColor(Color.RED);
    canvas.drawRect(0,0,60,30,mPaint);
    //保存当前画布状态,并裁剪一块区域
    int saveId = canvas.saveLayer(200,100,400,300,mPaint);
    canvas.drawColor(0x50666666);
    canvas.drawRect(240,100,360,160,mPaint);
    canvas.rotate(15);
    //画蓝色矩形
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(240,100,360,160,mPaint);
    //恢复画布状态
    canvas.restoreToCount(saveId);
    //画绿色矩形
    mPaint.setColor(Color.GREEN);
    canvas.drawRect(100,100,160,130,mPaint);
}

案例二:

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.WHITE);
    //平移画布到中间点位置,好观察效果
    canvas.translate(100,100);
    //画红色矩形
    mPaint.setColor(Color.RED);
    canvas.drawRect(0,0,60,30,mPaint);
    //保存当前画布状态,并裁剪一块区域
    //int saveId = canvas.saveLayer(200,100,400,300,mPaint);
    int saveId = canvas.save();
    canvas.clipRect(200,100,400,300);
    
    canvas.drawColor(0x50666666);
    canvas.drawRect(240,100,360,160,mPaint);
    canvas.rotate(15);
    //画蓝色矩形
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(240,100,360,160,mPaint);
    //恢复画布状态
    canvas.restoreToCount(saveId);
    //画绿色矩形
    mPaint.setColor(Color.GREEN);
    canvas.drawRect(100,100,160,130,mPaint);
}

首先这两个案例的结果都是一样的:

在这里插入图片描述

那下面这两种方式是等价的吗?

方式一:

canvas.saveLayer()

方式二:

canvas.save();
canvas.clipRect(RectF rectF);

非也,我们再回到xfermode的刮刮卡例子中,我们用的是方式一实现的,那我们用方式二实现看看行不行:

    @Override
    protected void onDraw(Canvas canvas) {
        //绘制一个白底背景
        canvas.drawColor(Color.WHITE);
        //绘制文字图层
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawText("你在找什么?",mDstB.getWidth()/2,mDstB.getHeight()/2,mPaint);
        //保存当前画布状态,并裁剪一块区域
	   //int saveLayerId = canvas.saveLayer(0, 0, mDstB.getWidth(), mDstB.getHeight(), mPaint);
        int saveLayerId=canvas.save();
        canvas.clipRect(0,0,mDstB.getWidth(), mDstB.getHeight());
        //绘制美女图片图层
        canvas.drawBitmap(mDstB,0,0,mPaint);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
        //绘制手势轨迹图层
        mPaint.setColor(Color.BLUE);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(mPath,mPaint);
        mPaint.setXfermode(null);
        //回到底层画布上继续其它内容绘制
        canvas.restoreToCount(saveLayerId);
    }

在这里插入图片描述

发现底层的文字被擦除了,所以这两种方式并不是等价的。

那么这两种方式我们就得重新理解下了:

方式一:

//保存当前画布状态,并新建一个画布,画布大小和位置用rect表示
int saveId=canvas.saveLayer(rect,null)

方式二:

//保存当前画布状态
int saveId=canvas.save();
//裁剪当前画布,画布大小和位置用rect表示
canvas.clipRect(RectF rect);

区别:

  • 方式一是新建了一块画布
  • 方式二还是在原画布上操作
  • 方式一和方式二都可以通过canvas.restore()或者canvas.restoreToCount()来回到保存的画布状态,并且保存和恢复可以多次调用,栈堆原理。

既然saveLayer是新建的一块画布,那刮刮卡的效果我们还可以换种方式实现,手动new Canvas:

//layerBitmap= Bitmap.createBitmap(mDstB.getWidth(),mDstB.getHeight(), Bitmap.Config.ARGB_8888);
@Override
protected void onDraw(Canvas canvas) {
    //绘制一个白底背景
    canvas.drawColor(Color.WHITE);
    //绘制文字图层
    mPaint.setColor(Color.BLACK);
    mPaint.setStyle(Paint.Style.FILL);
    canvas.drawText("你在找什么?",mDstB.getWidth()/2,mDstB.getHeight()/2,mPaint);
	//创建画布
    Canvas layerCanvas=new Canvas(layerBitmap);
    //保存当前画布状态,并裁剪一块区域
	//int saveLayerId = canvas.saveLayer(0, 0, mDstB.getWidth(), mDstB.getHeight(), mPaint);
    //绘制美女图片图层
    layerCanvas.drawBitmap(mDstB,0,0,mPaint);
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
    //绘制手势轨迹图层
    mPaint.setColor(Color.BLUE);
    mPaint.setStyle(Paint.Style.STROKE);
    layerCanvas.drawPath(mPath,mPaint);
    mPaint.setXfermode(null);
    //回到底层画布上继续其它内容绘制
	//canvas.restoreToCount(saveLayerId);
	//将layerCanvas画布上的内容绘制到canvas上
    canvas.drawBitmap(layerBitmap,0,0,mPaint);
}

总结:多层画布常用于隔离图层,多层画布的实现方式有两种:

方式一:

//保存当前画布状态,并新建一个画布,画布大小和位置用rect表示
int saveId=canvas.saveLayer(rect,null)

方式二:

//定义画布的大小、透明度等
Bitmap layerBitmap= Bitmap.createBitmap(mDstB.getWidth(),mDstB.getHeight(), Bitmap.Config.ARGB_8888);
//创建画布
Canvas layerCanvas=new Canvas(layerBitmap);
//将layerCanvas画布上的内容绘制到屏幕上
canvas.drawBitmap(layerBitmap,0,0,mPaint);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一鱼浅游

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值