美文网首页
Canvas详解

Canvas详解

作者: echoSuny | 来源:发表于2020-07-04 01:12 被阅读0次

    Canvas,称之为画布,在Android自定义view中十分重要,几乎所有的view都是在Canvas上进行绘制的。

    画布与图层的概念

    • 图层:每次调用canvas的drawXXX系列函数时,都会生成一个透明图层来专门进行绘制
    • 画布:其实每一个画布都是一个Bitmap,所有的绘制都是在Bitmap上进行的。所以这里说的画布并不是我们经常说的Canvas。上面说了每调用一次drawXXX都会生成一个新的图层。所以如果连续调用5个draw系列函数,就会生成5个透明图层,然后在透明图层上绘制。绘制完了之后会依次覆盖在画布,也就是Bitmap上来显示。画布有两种,一种时onDraw()函数传过来的,称为原始画布。另外一种称之为人造画布,是通过new Canvas(bitmap)或者saveLayer()函数来手动创建的。尤其是通过saveLayer()创建的画布,以后所有的绘制都是在这个画布上进行的,只有调用了restore或者restoreToCount才会返回到原始画布上绘制
    • Canvas:Canvas是一个类,它只不过有绘制的功能,是画布的一种代码上的表现形式。无论是原始画布还是人造画布,最后都是通过Canvas画到Bitmap上的。可以理解成绘制的工具

    Canvas的获取方式

    1 重写onDraw()或dispatchDraw()

    在自定义view时一般都需要重写onDraw()或者dispatchDraw()方法。在这两个方法参数中都有一个Canvas对象。这个Canvas对象是view中的Canvas对象,利用这个Canvas对象绘图,效果会直接反应在view中。

    2 利用Bitmap创建

    直接只用构造函数

    public Canvas(Bitmap bitmap) { }
    

    或者

    Canvas canvas = new Canvas();
    canvas.setBitmap(bitmap);
    

    最后一种则是调用SurfaceHolder.lockCanvas()函数来获得Canvas对象。

    drawXXX

    canvas有很多绘制的函数,都是以draw开头,并且有很多功能都是一样的,只不过传的参数不同罢了。不过可以大致分为以下几种:

    • drawText() 用于绘制文字
    • drawBitmap() 用于绘制位图
    • drawColor() 用于绘制颜色
    • drawCirlce() 用于绘制圆形 与之类似的还有drawRect(),drawLine(),drawOval()等都是绘制一些图形
    • drawPath() 用于绘制路径

    Canvas变换

    除了上面的绘制函数之外,还可以对画布进行一些操作,如平移,旋转,缩放,裁剪等

    平移 translate

    要想对Canvas进行平移,需要使用其中的translate()函数。既然要进行平移,肯定要有一个原点作为参考点。默认的左上角为原点(0 , 0)。x轴向右为正方向,y轴向下为正方向。

      override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            val paint1 = Paint()
            paint1.apply {
                color = Color.GREEN
                style = Paint.Style.STROKE
                strokeWidth = 8f
            }
    
            canvas.drawRect(0f, 0f, 400f, 300f, paint1)
    
            canvas.translate(100f, 100f)
    
            val paint2 = Paint()
            paint2.apply {
                color = Color.RED
                style = Paint.Style.STROKE
                strokeWidth = 8f
            }
            canvas.drawRect(0f, 0f, 400f, 300f, paint2)
        }
    

    上面代码中显示构建了一个绿色的画笔,然后画了一个矩形。接着把画布的原点平移到了(100,100),然后构建一个红色的画笔,并绘制了一个一样大小的矩形。根据绘制出来的图形我们可以得到的结果有:

    • 每次调用draw系列函数的时候确实会创建一个新的图层来进行绘制
    • 每次在调用draw系列函数之前对画布进行的平移旋转等都是不可逆的,之后的绘制都将在这个画布上显示
    旋转 rotate
            val paint1 = Paint()
            paint1.apply {
                color = Color.GREEN
                style = Paint.Style.STROKE
                strokeWidth = 8f
            }
    
            canvas.drawRect(300f, 200f, 700f, 500f, paint1)
    
            canvas.rotate(20f)
    
            val paint2 = Paint()
            paint2.apply {
                color = Color.RED
                style = Paint.Style.STROKE
                strokeWidth = 8f
            }
            canvas.drawRect(300f, 200f, 700f, 500f, paint2)
    

    需要注意的是,如果不指定旋转中心点的话,默认还是原点。

    缩放 scale
            val paint1 = Paint()
            paint1.apply {
                color = Color.GREEN
                style = Paint.Style.STROKE
                strokeWidth = 8f
            }
    
            canvas.drawRect(300f, 200f, 700f, 500f, paint1)
    
            canvas.scale(1f,0.5f)
    
            val paint2 = Paint()
            paint2.apply {
                color = Color.RED
                style = Paint.Style.STROKE
                strokeWidth = 8f
            }
            canvas.drawRect(300f, 200f, 700f, 500f, paint2)
    

    x轴方向保持不变,y轴上缩放百分之50


    扭曲 skew

            val paint1 = Paint()
            paint1.apply {
                color = Color.GREEN
                style = Paint.Style.STROKE
                strokeWidth = 8f
            }
    
            canvas.drawRect(300f, 200f, 700f, 500f, paint1)
    
            canvas.skew(0.6f,0f)
    
            val paint2 = Paint()
            paint2.apply {
                color = Color.RED
                style = Paint.Style.STROKE
                strokeWidth = 8f
            }
            canvas.drawRect(300f, 200f, 700f, 500f, paint2)
    

    skew()里的两个参数分别为角度的正切值。例如在一个角为30度的直角三角形中,30度角对应的直角边长度为斜边的一半。假设30度对应的直角边长度为1,那么斜边就为2,根据勾股定理则可以求出另外一条边长的平方为3,那么开方之后则约为1.732。那么对应的三十度的正切值就为对边1比上邻边1.732,约等于0.57。上面代码中则直接给了0.6,相当于把画布在x轴方向上倾斜了30度左右。


    image.png

    图层与画布

    public int saveLayer(RectF bounds, Paint paint) { }
    public int saveLayer(float left, float top, float right, float bottom, Paint paint, int saveFlags) { }
    

    这两个函数的功能是一样的,都是为了保存指定矩形区域的Canvas的内容,只不过参数不同。其中saveFlags有很多种,包括:

    • ALL_SAVE _FLAG 保存所有内容
    • MATRIX_SAVE_FLAG 仅保存Canvas的matix数组
    • CLIP_SAVE_ FLAG 仅保存Canvas的当前大小
    • HAS_ALPHA_LAYER_SAVE_FLAG 标识新建的bitmap具有透明图,在与上层画布结合时,透明位置显示上层图像。与FULL_COLOR_LAYER_SAVE_FLAG冲突,若二者同时存在,以HAS_ALPHA_LAYER_SAVE_FLAG为主
    • FULL_COLOR_LAYER_SAVE_FLAG 标识新建的bitmap颜色完全独立,与上层画布结合时,显清空上层画布再覆盖上去
    • CLIP_TO_LAYER_SAVE_FLAG 在保存图层之前先把当前画布根据bounds裁剪。与CLIP_SAVE_FLAG冲突。若同时存在,以CLIP_SAVE_FLAG为主
      这6个flag中,saveLayer()函数都可以使用,而save()函数只能使用前三个。
      在调用saveLayer()函数时,会生成一个全新的画布,画布的大小就是参数中指定的大小,并且新生成的画布是全透明的。那么在调用savaLayer()函数后的所有绘制都是在这个指定的透明画布上。
      先来看一个例子:
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            // 先把整个画布涂成绿色
            canvas.drawColor(Color.GREEN)
            // 生成一个全新的画布,并在上面绘制
            val layerId =
                canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null, Canvas.ALL_SAVE_FLAG)
            // 绘制目标图像 目标图像为黄色的圆
            canvas.drawBitmap(dstBitmap, 0f, 0f, paint)
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
            // 绘制源图像 源图像是一个正方形 原点为目标图像的圆心
            canvas.drawBitmap(srcBitmap, 400f / 2, 400f / 2, paint)
            paint.xfermode = null
            // 恢复画布
            canvas.restoreToCount(layerId)
        }
    

    下面把canvas.saveLayer()和canvas.restoreToCount()两行注释掉,再来看一下效果:


    这其中的区别就是因为调用saveLayer()之后会创建一个新的透明画布,并在这个画布上绘制。于是根据代码来看,紧接着就会绘制一个黄色的圆在这个透明的画布上。(由于绿色是在saveLayer()之前绘制的,所以这个透明画布上只有一个黄色的圆)。下面使用了Xfermode来绘制源图像。由于Xfermode的存在,会把之前画布上的所有内容作为源图像。而之前的画布正是调用saveLayer()生成的透明画布,此时上面有一个黄色的圆。而每次调用canvas的drawXXX()系列函数的时候,都会创建一个新的图层进行绘制,绘制完了之后又会叠加到最近的画布上。因为Xfermode使用了SRC_IN的算法(SRC_IN算法是以显示源图像为主,当源图像和目标图像相交时,利用目标图像的透明度来改变源图像的透明度。如果目标图像透明度为0时,原图像就完全不显时),而除了圆形之外都是透明的,所以源图像正方形除了和圆相交的部分都变成透明的了。整个流程大概是这样的:



    不使用saveLayer()的大概过程是这样的:



    与saveLayer()类似的还有一个saveLayerAlpha()函数。区别就是后者带有透明度。

    未完待续。。。

    相关文章

      网友评论

          本文标题:Canvas详解

          本文链接:https://www.haomeiwen.com/subject/vmteqktx.html