《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);