参考
- https://blog.csdn.net/harvic880925/article/details/39080931
- https://www.jianshu.com/p/f2b14c994f0f4
- https://blog.csdn.net/lijiuche/article/details/53467844
- https://blog.csdn.net/cquwentao/article/details/51423371
Canvas变换等操作,是非常重要的,经常忘,之前也有记录一下,但总是忘,用的时候又过来查一下;
平移操作(translate)
canvas中函数translate()是用来实现画布平移的,画布的原状是以手机屏幕左上角为原点,向左是X轴正方向,向下是Y轴正方向;
translate()
函数实现的相当于平移坐标系,即平移坐标系的原点的位置;
public void translate(float dx, float dy) {}
dx/dy正往右/下,否则,反之;值为偏移的量
屏幕显示与Canvas的关系
例子:画一个矩形,translate后,再画一个;
val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.RED
}
val rect = RectF(10f, 10f, 200f, 168f)
canvas.drawRect(rect, paint1)
// canvas平移
canvas.translate(100f, 100f)
val paint2 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.GREEN
}
canvas.drawRect(rect, paint2)
红色框,并没有平移;
由于屏幕显示与Canvas根本不是一个概念!Canvas是一个很虚幻的概念,相当于一个透明图层,每次Canvas画图时(即调用Draw系列函数),都会产生一个透明图层,然后在这个图层上画图,画完之后覆盖在屏幕上显示。所以上面的两个结果是由下面几个步骤形成的(权值转自原博客):
- 调用
canvas.drawRect(rect, paint1)
时,产生一个Canvas透明图层,由于当时还没有对坐标系平移,所以坐标原点是(0,0),再在系统在Canvas上画好之后,覆盖到屏幕上显示出来,过程如下图:
- 再调用
canvas.drawRect(rect, paint2)
时,又会重新产生一个全新的Canvas画布,但此时画布坐标已经改变了,即向右和向下分别移动了100像素,所以此时的绘图方式为:
超出屏幕之外的将不会显示;
总结
- 每次调用canvas.drawXXXX系列函数来绘图,都会产生一个全新的Canvas透明画布;
- 如果在DrawXXX前,调用平移、旋转等函数来对Canvas进行了操作,那么这个操作是不可逆的!每次产生的画布的最新位置都是这些操作后的位置。(
Save()、Restore()
后面会提) - 在Canvas与屏幕合成时,超出屏幕范围的图像是不会显示出来的;
旋转(Rotate)
画布的旋转是默认是围绕坐标原点
来旋转的,这里容易产生错觉,看起来觉得是图片旋转了,其实旋转的是画布,以后在此画布上画的东西显示出来的时候全部看起来都是旋转的。
canvas有2个rotate方法:
// 1. 正为顺时针旋转,负为指逆时针旋转,它的旋转中心点是原点(0,0)
public void rotate(float degrees)
// 2.构造函数除了度数以外,还可以指定旋转的中心点坐标(px,py)
public final void rotate(float degrees, float px, float py)
val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.RED
}
val rect = RectF(300f, 10f, 500f, 100f)
canvas.drawRect(rect, paint1)
// canvas rotate ,画布顺时针旋转45度后
canvas.rotate(30f)
val paint2 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.FILL
color = Color.GREEN
}
canvas.drawRect(rect, paint2)
过程:
-
第一次画的过程:
-
第二次画的过程:
- 那么第三次呢,第三次,是以旋转30度后的Canvas为参考进行新一次的旋转,来形成透明Canvas;
缩放(Scale)
canvas缩放也是2个方法:
// 1. 水平方向伸缩的比例与垂直缩放比例,大于1.0为放大,否则反之
public void scale(float sx, float sy)
// 2. 先平移(px,py),再缩放,再平移回去(-px,py),后续的操作不受影响
public final void scale(float sx, float sy, float px, float py)
示例代码:
val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.RED
}
canvas.drawCircle(300f, 300f, 148f, paint1)
canvas.scale(0.5f,1f)
//canvas.scale(0.5f,1f, 300f,300f)
val paint2 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.GREEN
}
canvas.drawCircle(300f, 300f, 148f, paint2)
上图绿圆为什么可显示完全,因为translate又平移回来了;
倾斜(skew)
canvas对应的倾斜方法:
// 将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,sy类似
public void skew(float sx, float sy)
注意: 这里全是倾斜角度的tan值,比如我们打算在X轴方向上倾斜60度,tan60=根号3,小数对应1.732; tan(45) = 1
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.BLACK
}
canvas.drawLine(0f, height / 2.toFloat(), width.toFloat(), height / 2.toFloat(), paint1)
canvas.drawLine(width / 2.toFloat(), 0f, width / 2.toFloat(), height.toFloat(), paint1)
canvas.translate(width / 2.toFloat(), height / 2.toFloat())
paint1.color = Color.RED
canvas.drawRect(0f, 0f, 200f, 100f, paint1)
// x倾斜45,y不变
//canvas.skew(1f, 0f) // x方向45度错切
// canvas.skew(-1f,0f) // x方向-45度错切
// canvas.skew(0f,1f) // y方向斜切45
canvas.skew(-1f, 1f)
paint1.color = Color.GREEN
canvas.drawRect(0f, 0f, 200f, 100f, paint1)
skew没有完全理解意思,理解后,回来再更新
clip裁剪画布(裁剪-非常重要)
裁剪画布是利用Clip系列函数,通过与Rect、Path、Region取交、并、差等集合运算来获得最新的画布形状。除了调用Save、Restore函数以外,这个操作是不可逆的,一但Canvas画布被裁剪,就不能再被恢复!
canvas相关的clip函数比较多,这里列几个:
根据Rect、Path来取得最新画布的函数
boolean clipPath(Path path)
boolean clipPath(Path path, Region.Op op)
boolean clipRect(Rect rect, Region.Op op)
val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.RED
}
// 填充为灰色
canvas.drawColor(Color.parseColor("#27000000"))
// 移动屏幕中间
canvas.translate(width / 4.toFloat(), height / 2.toFloat())
val rect1 = RectF(0f, 0f, 200f, 100f)
val rect2 = RectF(100f, 0f, 300f, 100f)
val path1 = Path().apply {
addRect(rect1, Path.Direction.CCW)
}
val path2 = Path().apply {
addRect(rect2, Path.Direction.CCW)
}
canvas.drawPath(path1, paint1)
paint1.color = Color.GREEN
canvas.drawPath(path2, paint1)
path1.op(path2, Path.Op.INTERSECT) // 交集
canvas.clipPath(path1) // 形成新画布
canvas.drawColor(Color.YELLOW)
效果如下(去交集后,填充):
画布的保存与恢复(save与restore,非常重要)
前面所有对画布的操作都是不可逆的,这会造成很多麻烦,比如,我们为了实现一些效果不得不对画布进行操作,但操作完了,画布状态也改变了,这会严重影响到后面的画图操作。能对画布的大小和状态(旋转角度、扭曲等)进行实时保存和恢复就最好了,这需要用到
画布的保存与恢复相关的函数——save()、restore()
方法说明:
- save(): 每次调用Save()函数,都会把当前的画布的状态进行保存,然后放入特定的栈中
- restore(): 每当调用Restore()函数,就会把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布,并在这个画布上做画。
例子:多次调用save
canvas.apply {
drawColor(Color.RED)
save() // 保存的画布大小为全屏幕大小
clipRect(Rect(100, 100, 800, 800))
drawColor(Color.GREEN)
save() // 保存画布大小为Rect(100, 100, 800, 800)
clipRect(Rect(200, 200, 700, 700))
drawColor(Color.BLUE)
save() // 保存画布大小为Rect(200, 200, 700, 700)
clipRect(Rect(300, 300, 600, 600))
drawColor(Color.BLACK)
save() // 保存画布大小为Rect(300, 300, 600, 600)
// 在上面clip,并作画
clipRect(Rect(400, 400, 500, 500))
drawColor(Color.WHITE)
}
上面总共调用了四次Save操作。每调用一次Save()操作就会将当前的画布状态保存到栈中,下次绘制,就在这个save上进行作画,所以这四次Save()所保存的状态的栈的状态如下:
例子2:多次调用restore
canvas.apply {
...
// ===== 多次restore
// 将栈顶的画布状态取出来,作为当前画布,并画成黄色背景 (黑色变黄色)
restore()
restore()
restore() // 到上图第2次状态
drawColor(Color.YELLOW)
}
Canvas Layer层
Canvas在一般情况下,可以看到是一张画布,所有的绘制都是在此画布上进行,如果需要叠加,如:地图上的标记等,就需要用到Canvas的图层了,默认情况下,可以当做只有一个图层 Layer,如果需要按图层来绘制图形,
可通过 Canvas的SaveLayerXXX,restore后 来创建一些中间层layer,并在layer进行绘制;这些Layer是按照‘栈结构’来管理的;
创建一个新的Layer到“栈”中,可使用saveLayer,saveLayerAlpha, 从栈中推出一个Layer,后续的操作都发生在此Layer上,使用 restore/restoreToCount,就会把本次的绘制的图像“绘制”到上层Layer上;类似于入栈
一样;
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
style = Paint.Style.FILL
}
canvas.apply {
drawColor(Color.WHITE)
drawCircle(100f,100f, 85f, paint)
paint.color = Color.BLUE
// 创建一个新的layer,后续的蓝色圆是在这个 layer 中绘制,与 红色圆 不是同一个layer
saveLayerAlpha(0f, 0f, 300f,300f,0x88) // 最后参数为透明度
drawCircle(150f, 150f, 85f, paint)
restore() // 把本次的绘制的图像“绘制”到上层Layer上,试着注释
drawLine(0f, 0f, 300f, 300f, paint)
}
注释掉 restore() 效果:
注意蓝线部分,因为没有restore,蓝色圆部分没有画上去;
更多请参考:
https://blog.csdn.net/cquwentao/article/details/51423371
save()和saveLayer()区别
- 相同点
- saveLayer可以实现save所能实现的功能
- 不同点
- saveLayer生成一个独立的图层而save只是保存了一下当时画布的状态类似于一个还原点(本来就是)。
- saveLayer因为多了一个图层的原因更加耗费内存慎用。
- saveLayer可指定保存相应区域,尽量避免2中所指的情况。
- 在使用混合模式setXfermode时会产生不同的影响。