自定义控件绘图(Canvas变换、图层等)篇二

参考

  1. https://blog.csdn.net/harvic880925/article/details/39080931
  2. //www.greatytc.com/p/f2b14c994f0f4
  3. https://blog.csdn.net/lijiuche/article/details/53467844
  4. 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)
image.png

红色框,并没有平移;

由于屏幕显示与Canvas根本不是一个概念!Canvas是一个很虚幻的概念,相当于一个透明图层,每次Canvas画图时(即调用Draw系列函数),都会产生一个透明图层,然后在这个图层上画图,画完之后覆盖在屏幕上显示。所以上面的两个结果是由下面几个步骤形成的(权值转自原博客):

  1. 调用canvas.drawRect(rect, paint1)时,产生一个Canvas透明图层,由于当时还没有对坐标系平移,所以坐标原点是(0,0),再在系统在Canvas上画好之后,覆盖到屏幕上显示出来,过程如下图:
    注意透明图层
  2. 再调用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

过程:

  1. 第一次画的过程:


    image.png
  2. 第二次画的过程:


    image.png
  3. 那么第三次呢,第三次,是以旋转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)
x缩放一半,即对应的canvas画布在x方向上密度缩小
canvas.scale(0.5f,1f, 300f,300f)效果

上图绿圆为什么可显示完全,因为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)
x方向斜切45度
x方向斜切-45度
y方向斜切45度
image.png

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)
}
image.png

上面总共调用了四次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示例图

创建一个新的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()区别

  1. 相同点
  • saveLayer可以实现save所能实现的功能
  1. 不同点
    • saveLayer生成一个独立的图层而save只是保存了一下当时画布的状态类似于一个还原点(本来就是)。
    • saveLayer因为多了一个图层的原因更加耗费内存慎用。
    • saveLayer可指定保存相应区域,尽量避免2中所指的情况。
    • 在使用混合模式setXfermode时会产生不同的影响。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容