当前位置:   article > 正文

Android canvas

android canvas

1.Canvas

Canvas指画布,表现在屏幕上就是一块区域,可以在上面使用各种API绘制想要的东西。

canvas内部维持了一个mutable Bitmap,所以它可以使用颜色值去填充整个Bitmap,此外canvas也可以使用画笔去填充整个Bitmap。这两种填充方式都会受限于clip的范围。

canvas虽然内部保持了一个Bitmap,但是它本身并不代表那个Bitmap,而更像是一个图层。我们对这个图层的平移、旋转和缩放等操作并不影响内部的Bitmap,仅仅是改变了该图层相对于内部Bitmap的坐标位置、比例和方向而已。

在Android中,获得Canvas对象主要有三种方法:

①继承View,并重写onDraw()方法。View的Canvas对象会被当做参数传递过来,在这个Canvas上进行的操作会直接反映在View中。

②调用SurfaceHolder.lockCanvas()返回一个Canvas对象。

③通过构造方法创建一个Canvas对象。

Bitmap bitmap = Bitmap.createBitmap(100f, 100f, Config.ARGB_8888); //得到一个Bitmap对象,也可以使用别的方式得到。但是要注意,该bitmap一定要是mutable(异变的)      

Canvas canvas = new Canvas(bitmap);

 

Canvas的坐标系:

画布以左上角为原点(0,0),向右为X轴的正方向,向下为Y轴的正方向:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_8,color_FFFFFF,t_70,g_se,x_16

Canvas的绘图操作:

绘制颜色 drawColor、drawRGB、drawARGB

绘制圆 drawCircle

绘制点 drawPoint

绘制直线 drawLine

绘制矩形 drawRect

绘制圆角矩形 drawRoundRect

绘制椭圆 drawOval

绘制弧形 drawArc

绘制文本 drawText

沿Path路径绘制文本 drawTextOnPath

绘制位图 drawBitmap

使用canvas.drawXXX时,系统会在一个新的透明区域绘制内容,然后迅速与屏幕当前显示内容进行重叠,这个重叠的过程也会受xfermode或blendmode的影响。

 

2.PorterDuffXfermode

android.graphics.PorterDuffXfermode类继承自android.graphics.Xfermode。在Android中用Canvas进行绘图时,可以通过使用PorterDuffXfermode将所绘制的图形的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值,从而更新Canvas中最终的像素颜色值,这样会创建很多有趣的效果。当使用PorterDuffXfermode时,需要将其作为参数传给Paint.setXfermode(Xfermode xfermode)方法,这样在用该画笔paint进行绘图时,Android就会使用传入的PorterDuffXfermode,如果不想再使用Xfermode,那么可以执行Paint.setXfermode(null)。

举个例子:

①不使用Xfermode:

@Override

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    canvas.drawARGB(255, 139, 197, 186);//设置背景色

    int canvasWidth = canvas.getWidth();

    int r = canvasWidth / 3;

    //绘制黄色的圆形

    paint.setColor(Color.YELLOW);

    canvas.drawCircle(r, r, r, paint);

    //绘制蓝色的矩形

    paint.setColor(Color.BLUE);

    canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint);

}

重写View的onDraw方法,首先将View的背景色设置为绿色,然后绘制了一个黄色的圆形,然后再绘制一个蓝色的矩形,效果如下所示:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

上面演示就是Canvas正常的绘图流程,没有使用PorterDuffXfermode。简单分析一下上面这段代码:

1)首先调用了canvas.drawARGB(255, 139, 197, 186)方法将整个Canvas都绘制成一个颜色,在执行完这句代码后,canvas上所有像素的颜色值的ARGB颜色都是(255,139,197,186),由于像素的alpha分量是255而不是0,所以此时所有像素都不透明。

2)当执行了canvas.drawCircle(r, r, r, paint)之后,Android会在所画圆的位置用黄颜色的画笔绘制一个黄色的圆形,此时整个圆形内部所有的像素颜色值的ARGB颜色都是0xFFFFFF00(YELLOW),然后用这些黄色的像素替换掉Canvas中对应的同一位置中颜色值ARGB为(255,139,197,186)的像素,这样就将黄色圆形绘制到Canvas上了。

3)当执行了canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint)之后,Android会在所画矩形的位置用蓝色的画笔绘制一个蓝色的矩形,此时整个矩形内部所有的像素颜色值的ARGB颜色都是0xFF0000FF(BLUE),然后用这些蓝色的像素替换掉Canvas中对应的同一位置中的像素,这样黄色的圆中的右下角部分的像素与其他一些背景色像素就被蓝色像素替换了,这样就将蓝色矩形绘制到Canvas上了。

这个过程虽然简单,但是了解Canvas绘图时具体的像素更新过程是真正理解PorterDuffXfermode的工作原理的基础。

②接下来,使用PorterDuffXfermode对上面的代码进行修改:

@Override

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    canvas.drawARGB(255, 139, 197, 186);//设置背景色

    int canvasWidth = canvas.getWidth();

    int r = canvasWidth / 3;

    //正常绘制黄色的圆形

    paint.setColor(Color.YELLOW);

    canvas.drawCircle(r, r, r, paint);

    //使用CLEAR作为PorterDuffXfermode绘制蓝色的矩形

    paint.setXfermode(new PorterDuffXfermode( PorterDuff.Mode.CLEAR));

    paint.setColor(Color.BLUE);

    canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint);

    //最后将画笔去除Xfermode

    paint.setXfermode(null);

}

最终效果还取决于是否关闭了硬件加速,因为PorterDuff.Mode.CLEAR不支持硬件加速:

//关闭硬件加速

setLayerType(View.LAYER_TYPE_SOFTWARE, null);

关闭硬件加速的效果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 不关闭硬件加速的效果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

同样对以上代码进行一下分析:

1)首先调用了canvas.drawARGB(255, 139, 197, 186)方法将整个Canvas都绘制成一个颜色,此时所有像素都不透明。

2)然后通过调用canvas.drawCircle(r, r, r, paint)绘制了一个黄色的圆形到Canvas上面。

3)然后执行代码paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)),将画笔的PorterDuff模式设置为CLEAR。

4)然后调用canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint)方法绘制蓝色的矩形,但是最终界面上出现了一个白色/黑色的矩形。

5)在绘制完成后,调用paint.setXfermode(null)将画笔去除Xfermode。

具体分析一下白色/黑色矩形出现的原因:一般在调用canvas.drawXXX()方法时都会传入一个画笔Paint对象,Android在绘图时会先检查该画笔Paint对象有没有设置Xfermode,如果没有设置Xfermode,那么直接将绘制的图形覆盖Canvas对应位置原有的像素;如果设置了Xfermode,那么会按照Xfermode具体的规则来更新Canvas中对应位置的像素颜色。就本例来说,在执行canvas.drawCirlce()方法时,画笔Paint没有设置Xfermode对象,所以绘制的黄色圆形直接覆盖了Canvas上的像素。当调用canvas.drawRect()绘制矩形时,画笔Paint已经设置Xfermode的值为PorterDuff.Mode.CLEAR,此时Android首先是在内存中绘制了这么一个矩形,所绘制的图形中的像素称作源像素(source,简称src),所绘制的矩形在Canvas中对应位置的矩形内的像素称作目标像素(destination,简称dst)。源像素的ARGB四个分量会和Canvas上同一位置处的目标像素的ARGB四个分量按照Xfermode定义的规则进行计算,形成最终的ARGB值,然后用该最终的ARGB值更新目标像素的ARGB值。本例中的Xfermode是PorterDuff.Mode.CLEAR,该规则比较简单粗暴,直接要求目标像素的ARGB四个分量全置为0,即(0,0,0,0),即透明色,所以通过canvas.drawRect()在Canvas上绘制了一个透明的矩形。

③继续对例子中的代码进行修改,将绘制圆形和绘制矩形相关的代码放到canvas.saveLayer()和canvas.restoreToCount()之间,代码如下:

@Override

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);       

    canvas.drawARGB(255, 139, 197, 186);//设置背景色

    int canvasWidth = canvas.getWidth();

    int canvasHeight = canvas.getHeight();

    int layerId = canvas.saveLayer(0, 0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG);

    int r = canvasWidth / 3;

    //正常绘制黄色的圆形

    paint.setColor(Color.YELLOW);

    canvas.drawCircle(r, r, r, paint);

    //使用CLEAR作为PorterDuffXfermode绘制蓝色的矩形

    paint.setXfermode(new PorterDuffXfermode( PorterDuff.Mode.CLEAR));

    paint.setColor(Color.BLUE);

    canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint);

    //最后将画笔去除Xfermode

    paint.setXfermode(null);

    canvas.restoreToCount(layerId);

}

效果图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

对上述代码进行一下分析:

1)首先调用了canvas.drawARGB(255, 139, 197, 186)方法将整个Canvas都绘制成一个颜色,此时所有像素都不透明。

2)然后将主要的代码都放到了canvas.saveLayer()以及canvas.restoreToCount()之间。

关于canvas绘图中的layer有几点需要说明:

1)canvas是支持图层layer渲染这种技术的,canvas默认就有一个layer,平时调用canvas的各种drawXXX()方法时,其实是把所有的东西都绘制到canvas这个默认的layer上面。

2)还可以通过canvas.saveLayer()新建一个layer,新建的layer放置在canvas默认layer的上部,当执行了canvas.saveLayer()之后,所有的绘制操作都绘制到了新建的layer上,而不是canvas默认的layer。

3)用canvas.saveLayer()方法产生的layer所有像素的ARGB值都是(0,0,0,0),即canvas.saveLayer()方法产生的layer初始时是完全透明的。

4)canvas.saveLayer()方法会返回一个int值,用于表示layer的ID,在对这个新layer绘制完成后可以通过调用canvas.restoreToCount(layer)或者canvas.restore()把这个layer绘制到canvas默认的layer上去,这样就完成了一个layer的绘制工作。

只是将绘制圆形与矩形的代码放到了canvas.saveLayer()和canvas.restoreToCount()之间,为什么不再像上面那样显示白色/黑色的矩形了?

在上个例子中,最终矩形区域的目标颜色都被重置为透明色(0,0,0,0)了,最后只是由于Activity背景色为白色,所以才最终显示成白色矩形。修改后,在新建的layer上面绘制,其实矩形区域的目标颜色也还是被重置为透明色(0,0,0,0)了,这样整个新建layer只有圆的3/4不是透明的,其余像素全是透明的,然后调用canvas.restoreToCount()将该layer又绘制到了Canvas上面去了。在将一个新建的layer绘制到Canvas上去时,Android会用整个layer上面的像素颜色去更新Canvas对应位置上像素的颜色,并不是简单的替换,而是Canvas和新layer进行Alpha混合。由于新建的layer中只有两种像素:完全透明的和完全不透明的,不存在部分透明的像素,并且完全透明的像素的颜色值的四个分量都为0,所以就将Canvas和新layer进行Alpha混合的规则简化了,具体来说:

①如果新建layer上面某个像素的Alpha分量为255,即该像素完全不透明,那么Android会直接用该像素的ARGB值作为Canvas对应位置上像素的颜色值。

②如果新建layer上面某个像素的Alpha分量为0,即该像素完全透明,在本例中Alpha分量为0的像素,其RGB分量也都为0,那么Android会保留Canvas对应位置上像素的颜色值。

这样当将新layer绘制到Canvas上时,完全不透明的3/4黄色圆中的像素会完全覆盖Canvas对应位置的像素,而由于在新layer上面绘制的矩形区域的像素ARGB都为(0,0,0,0),所以最终Canvas上对应矩形区域还是保持之前的背景色,这样就不会出现白色的矩形了。

 

3.Canvas的常用方法

①平移画布 translate

translate()用来实现画布坐标系平移的,即改变坐标系原点位置。画布坐标是以左上角为原点(0,0),向右是X轴正方向,向下是Y轴正方向。

void translate(float dx, float dy)

float dx:水平方向平移的距离,正数指向正方向(向右)平移的量,负数为向负方向(向左)平移的量;

float dy: 垂直方向平移的距离,正数指向正方向 (向下) 平移量,负数为向负方向 (向上) 平移量;

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    Paint paint = new Paint();

    paint.setColor(Color.GREEN);

    paint.setStyle(Paint.Style.FILL);

    // canvas.translate(100, 100);

    Rect rect1 = new Rect(0, 0, 400, 220);

    canvas.drawRect(rect1, paint);

}

这段代码先把canvas.translate(100, 100);注释掉,看原来矩形的位置,然后打开注释,看平移后的位置,对比如下图:

708a9b5f45a44ee491eb0dd33514600a.webp

为了对比明显,同一个矩形,在画布平移前画一次,平移后再画一次:

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    //构造两个画笔,一个红色,一个绿色

    Paint paintGreen = generatePaint( Color.GREEN, Paint.Style.STROKE, 3);

    Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);

    //构造一个矩形

    Rect rect = new Rect(0, 0, 400, 220);

    //在平移画布前用绿色画下边框

    canvas.drawRect(rect, paintGreen);

    //平移画布后,再用红色边框重新画下这个矩形

    canvas.translate(100, 100);

    canvas.drawRect(rect, paintRed);

}

942faaca48f94f9087688e169ac52768.webp

可以看到,在平移画布前后画同一个矩形边框,,这两个边框会重合并不会重合。也就是画布平移时,绿色框并没有移动。

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

1)调用canvas.drawRect(rect, paintGreen0) 时,产生一个Canvas透明图层,由于当时还没有对坐标系平移,所以坐标原点是(0,0);系统在Canvas上画好之后,覆盖到屏幕上显示出来,过程如下图:

759fe641a1544784a13b3056db5f390a.webp

 2)然后第二次调用canvas.drawRect(rect, paintRed)时,又会重新产生一个全新的Canvas画布,但此时画布坐标已经改变了,即向右和向下分别移动了100像素,所以此时的绘图方式为:(合成视图,从上往下看的合成方式)

a4159f017f40435bae941a0f50a5dcdd.jpg

上图展示了Canvas图层与屏幕的合成过程,由于Canvas画布已经平移了100像素,所以在画图时是以新原点来产生视图的,然后合成到屏幕上,这就是最终看到的结果了。画布移动之后,有一部分超出了屏幕的范围,那超出范围的图像显不显示呢,当然不显示了!也就是说,Canvas上虽然能画上,但超出了屏幕的范围,是不会显示的。当然,这里也没有超出显示范围,两框框而已。

translate总结:

1)每次调用canvas.drawXXXX系列函数来绘图,都会产生一个全新的Canvas画布。

2)如果在DrawXXX前,调用平移、旋转等函数来对Canvas进行了操作,那么这个操作是不可逆的。每次产生的画布最新位置都是这些操作后的位置。(关于Save()、Restore()的画布可逆问题的后面再讲)。

3)在Canvas与屏幕合成时,超出屏幕范围的图像是不会显示出来的。

②旋转画布 rotate

画布默认是围绕坐标原点旋转的,这里容易产生错觉,看起来觉得是图片旋转了,其实旋转的是画布,以后在此画布上画的东西显示出来的时候全部看起来都是旋转的。

Roate函数有两个构造函数:

void rotate(float degrees)

void rotate (float degrees, float px, float py)

第一个构造函数直接输入旋转的度数,正数是顺时针旋转,负数指逆时针旋转,它的旋转中心点是原点(0,0),也就是旋转后原点位置是不变的。

第二个构造函数除了度数以外,还可以指定旋转的中心点坐标(px,py),也就是旋转后(px,py)的位置是不变的,原点(0,0)会变了。

下面旋转一个矩形,先画出未旋转前的图形,然后再画出旋转后的图形:

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    //构造两个画笔,一个红色,一个绿色

    Paint paintGreen = generatePaint( Color.GREEN, Paint.Style.STROKE, 3);

    Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);

    //构造一个矩形

    Rect rect = new Rect(300, 10, 500, 100);

    //画出原轮廓

    canvas.drawRect(rect, paintGreen);

    //顺时针旋转画布 30度

    canvas.rotate(30);

    canvas.drawRect(rect, paintRed);

}

d30a97e799a54af7838136737f50aaf3.webp

③缩放画布 scale

public void scale (float sx, float sy)

public final void scale (float sx, float sy, float px, float py)

float sx:水平方向伸缩的比例,假设原坐标轴的比例为n,不变时为1,在变更的X轴密度为n*sx;所以,sx为小数为缩小,sx为整数为放大。

float sy:垂直方向伸缩的比例,同样,小数为缩小,整数为放大。

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    //构造两个画笔,一个红色,一个绿色

    Paint paintGreen = generatePaint( Color.GREEN, Paint.Style.STROKE, 3);

    Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);

    Rect rect = new Rect(10, 10, 200, 100);

    canvas.drawRect(rect, paintGreen);

    canvas.scale(0.5f, 1f);

    canvas.drawRect(rect, paintRed);

}

e7360fe047ee43f0bbf02dd88358ea2c.webp

④倾斜/扭曲画布 skew

void skew (float sx, float sy)

float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值;

float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值;

注意,这里全是倾斜角度的tan值,比如要在X轴方向上倾斜60度,tan60=根号3,小数对应1.732

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    //构造两个画笔,一个红色,一个绿色

    Paint paintGreen = generatePaintColor.GREEN, Paint.Style.STROKE, 3);

    Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);

    Rect rect = new Rect(10, 10, 200, 100);

    //画出原轮廓

    canvas.drawRect(rect, paintGreen);

    //X轴倾斜60度,Y轴不变

    canvas.skew(1.732f, 0);

    canvas.drawRect(rect, paintRed);

}

5ad09c9a684743619c29776d8a994b30.webp

 ⑤裁剪画布(clip系列函数)

裁剪画布是利用clip系列函数,通过与Rect、Path、Region取交、并、差等集合运算来获得最新的画布形状。除了调用save、restore函数外,这个操作是不可逆的,一旦Canvas画布被裁剪,就不能再被恢复。 

boolean clipPath(Path path)

boolean clipPath(Path path, Region.Op op)

boolean clipRect(Rect rect, Region.Op op)

boolean clipRect(RectF rect, Region.Op op)

boolean clipRect(int left, int top, int right, int bottom)

boolean clipRect(float left, float top, float right, float bottom)

boolean clipRect(RectF rect)

boolean clipRect(float left, float top, float right, float bottom, Region.Op op)

boolean clipRect(Rect rect)

boolean clipRegion(Region region)

boolean clipRegion(Region region, Region.Op op)

以上就是根据Rect、Path、Region来取得最新画布的函数。

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    canvas.drawColor(Color.RED);

    canvas.clipRect(new Rect(100, 100, 200, 200));

    canvas.drawColor(Color.GREEN);

}

先把背景色整个涂成红色,显示在屏幕上。然后裁切画布,最后最新的画布整个涂成绿色。可见绿色部分,只有一小块,而不再是整个屏幕了。

关于两个画布与屏幕合成,跟上面的合成过程是一样的。

c7e3a4914cc143de851750c5c30c51ef.webp

 

4.canvas.save()、restore()

前面讲的所有对画布的操作都是不可逆的,这会造成很多麻烦。比如,为了实现一些效果不得不对画布进行操作,但操作完了,画布状态也改变了,这会严重影响到后面的画图操作。这就需要对画布的大小和状态(旋转角度、扭曲等)进行实时保存和恢复。与画布的保存与恢复相关的函数是save()、restore()。

save():每次调用save()函数,都会把当前画布的状态进行保存,然后放入特定的栈中。(Saves the current matrix and clip onto a private stack. subsequent(随后的) calls to translate, scale, rotate,skew,concat or clipRect,clip path will all operate as usual, but when the balancing call to restore() is made,those calls will be forgotten, and the settings that existed before the save() will be reinstated(恢复原状))

restore():每次调用restore()函数,就会把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布,并在这个画布上做画。这样可以防止save()方法以后对canvas进行的平移旋转缩放裁剪等操作会继续对后续的绘制产生影响,通过该方法能够避免连带的影响。(This call balances a previous call to save(),and is used to remove all modifications to the matrix/clip state since the last save call.It is an error to call restore() more times than save() was called.)

为了更清晰的显示这两个函数的作用,下面举个例子:

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    canvas.drawColor(Color.RED);

    canvas.save();//保存当前画布大小,即整屏

    canvas.clipRect(new Rect(100, 100, 600, 600));

    canvas.drawColor(Color.GREEN);

    canvas.restore(); //恢复整屏画布

    canvas.drawColor(Color.YELLOW);

}

图像的合成过程为:(最终显示为全屏幕黄色)

f33134adc2654ac790568fe5f4be6a10.webp

下面通过多次使用save()、restore()来讲述有关保存Canvas画布状态的栈的概念,代码如下:

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    canvas.drawColor(Color.RED);

    //保存的画布大小为全屏幕大小

    canvas.save();

 

    canvas.clipRect(new Rect(100, 100, 700, 700));

    canvas.drawColor(Color.GREEN);

    //保存画布大小为Rect(100, 100, 700, 700)

    canvas.save();

 

    canvas.clipRect(new Rect(200, 200, 600, 600));

    canvas.drawColor(Color.BLUE);

    //保存画布大小为Rect(200, 200, 600, 600)

    canvas.save();

 

    canvas.clipRect(new Rect(300, 300, 500, 500));

    canvas.drawColor(Color.BLACK);

    //保存画布大小为Rect(300, 300, 500, 500)

    canvas.save();

 

    canvas.clipRect(new Rect(370, 370, 430, 430));

    canvas.drawColor(Color.WHITE);

}

a73fa6ed83284e98a708c820ef9f554b.webp

在这段代码中,总共调用了四次save操作。上面提到过,每调用一次save()操作就会将当前的画布状态保存到栈中,所以这四次save()所保存的状态的栈的状态如下:

de60b083c26b4778ba5b024eebcd5c83.webp

注意:在第四次save()之后,还对画布进行了canvas.clipRect(new Rect(370, 370, 430, 430));操作,并将当前画布画成白色背景,也就是上图中最小块的白色部分是最后的当前的画布。也就是说此时(没有restore)再使用canvas.drawXXX()画图时,只有Rect(370, 370, 430, 430)区域的绘画有效,其他区域都无效。

如果现在使用restore(),就会把栈顶的画布取出来,当做当前画布来画图,试一下:

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    ……

    canvas.clipRect(new Rect(370, 370, 430, 430));

    canvas.drawColor(Color.WHITE);

 

    canvas.restore();

    canvas.drawColor(Color.YELLOW);

}

一次restore()之后,就会把栈顶的画布状态Rect(300, 300, 500, 500)取出来,作为当前画布。现在把当前画布的背景色填充为黄色,如下图:

6f9a4bcfe0c849129996aa6b2b4bd8e4.webp

 那如果连续restore()三次,会怎样呢?

先来分析一下,然后再看效果:restore()三次就会连续出栈三次,然后把第三次restore出来的Canvas状态当做当前画布,也就是Rect(100, 100, 700, 700),所以如下代码:

protected void onDraw(Canvas canvas) {

    ……

    canvas.clipRect(new Rect(370, 370, 430, 430));

    canvas.drawColor(Color.WHITE);

 

    canvas.restore();

    canvas.restore();

    canvas.restore();

    canvas.drawColor(Color.YELLOW);

}

三次restore()操作,会依次把栈顶的画布状态取出来,作为当前画布,然后把当前画布的背景色填充为黄色:

b87f63bce66543eda513a44a57d6395d.webp

 这样就可以在黄色大小的rect上进行操作。此时如果再restore一次,就可以得到全屏幕的canvas了。

 

5.Canvas的回退栈

使用canvas的辅助函数对canvas进行操作时,这些操作都是不可逆的。比如,在绘制某个内容之前,使用clipRect(0,0,100,100),那么之后的绘制就只能在[0,0,100,100]这个矩形内,除非再通过手动调用api,让canvas回到之前的某个状态。

Canvas在进行平移、缩放、旋转、倾斜后,画布的状态也就随之改变。这可能对后面的绘图操作产生很多麻烦。比如为了某些效果不得不对画布进行一些操作,但操作完了,画布状态也就改变了。

为了避免发生这种情况,就可以在特定的位置进行保存和恢复。在进行变换前,使用save保存canvas当前的状态,然后进行变换,接着绘制想要绘制的内容,最后再通过restore恢复之前保存的状态。

如果在一次绘制中,多次调用save方法,每次save时都会把canvas的状态压入类似一个栈中,每一个状态都对应一个数字,代表其是栈中的第几个。可以通过方法restoreToCount(count),将canvas回退到指定的那个,也可以调用restore,一个一个的回退canvas的状态。

public int save()   每次调用该方法,都会把当前画布的状态进行保存,并存放在一个栈结构中。

public void restore()   每次调用该方法,都会把栈中最顶层的画布状态取出来,并按照这个画布状态恢复当前画布。如果当前栈中没有保存的画布状态,则会抛出异常。

canvas提供了restoreToCount(int saveCount)来恢复画布状态。每次调用save()方法保存画布状态时都会返回一个int型的值。可以把该值直接传入restoreToCount()方法中直接恢复画布状态。状态恢复后,会将该状态和该状态顶部的其他画布状态一同出栈。

public void restoreToCount(int saveCount)   恢复指定的画布状态

需要注意的是,不管是调用restore还是restoreToCount,都需要在save的数量范围内,否者系统就会抛出异常。

 

6.canvas.savaLayer()

canvas提供了saveLayer方法,抽取一个透明区域,执行绘制方法,随后再一并将绘制的内容,覆盖在已显示内容上。

saveLayer()方法类似save()方法的作用,但是调用savaLayer()会分配并生成一个屏幕以外的bitmap(意思是不在原来的Bitmap上),之后的所有操作都是在这个新的offscreen bitmap上。

savaLayer()是一个非常耗费性能的方法,会导致绘制相同的内容渲染时耗费两倍多的资源。当需要的形状很大时(超屏幕)禁止使用这个方法,当应用一个Xfermode、color filter或者alpha时,推荐使用硬件加速会表现更好。

从saveLayer()的这些注释可以推断的是新生成了bitmap,而一个Canvas只能对应一个bitmap(推断),所以调用saveLayer相当于新生成了一个Canvas,新的Canvas有一个默认连接的Bitmap。新生成的Canvas会修改函数中canvas的指向,所以再次利用Canvas调用函数时将作用于新生成的canvas上。(ps:这里描述新生成Canvas只是猜测,也可能是改变了Canvas对Bitmap的引用,说了会新生成一个bitmap,并且所有的操作类似clip都不会影响原来的canvas,clip本来就是对canvas的操作。所以最终很有可能是调用saveLayer改变了Canvas对Bitmap的引用)。

调用drawXXX函数生成新的layer最终都会绘制到新生成的bitmap,直到调用restore()函数,新的bitmap会被绘制到原始Canvas的连接的目标上)(可能是bitmap,也可能是前一个Layer)。

这里总结一下:

所有的东西都是绘制在bitmap上的,canvas是一个虚拟的概念,它连接着一个bitmap,东西都绘制在bitmap上,每次调用drawxx函数都生成一个透明图层(layer),最终都会覆盖绘制在bitmap上,经过渲染才显示在屏幕上,Canvas可以比屏幕大很多,但是超出屏幕范围的图像是不会显示出来的,我们也就看不到。

SaveLayer中提到了图层,什么是layer呢?Canvas 的setBitmap函数上有一段注释:

Specify a bitmap for the canvas to draw into. All canvas state such as layers, filters, and the save/restore stack are reset.

调用setBitmap时,会为canvas连接一个bitmap,所有的canvas的状态类似layers,filters和save/restore 栈都将重置。所以layer是canvas的一种状态,可以保存,它可以承载clip,matrix,图形,颜色等信息,所以每次调用draw方法都会生成一个新的图层layer。调用draw生成的图层最终会覆盖在它所依附的bitmap上。调用restore()、resoreToCount()函数以后,将恢复到Canvas对应的最原始的bitmap上进行绘制。

对layer和canvas对应的bitmap有了理解,bitmap可以看成我们平时说的画布,最终的东西都是绘制在这上面的,每次调用drawXXX方法会生成一个新的透明layer,东西被绘制在layer上,然后最终会被绘制在bitmap上。

调用savelayer函数会生成新的Bitmap,所以我认为调用saveLayer会生成一个新的Canvas连接一个新的Bitmap(或者改变了原来Canvas的bitmap的引用指向),然后再调用drawXX函数也会生成新的layer,但这个layer会被绘制到新生成的Bitmap上,其他所有的rotate,clip等操作,都会作用在新的Canvas上(或者新指向的bitmap上),不会影响原始的Canvas(原始bitmap),直到调用restore函数,才会被最终绘制到原始Canvas连接的bitmap上。所以canvas是一个包含了多种状态(clip,matrix等)的类,它有点类似坐标系(规定绘制图形的位置),所有的操作都不会影响已经绘制在上面的图形,会连接一个Bitmap,所有的东西最终都会被绘制在Bitmap上。

调用saveLayer()可以为canvas创建一个新的图层,在新的图层上的绘制并不会直接绘制在屏幕上,而是在restore()后绘制在上一个图层或者绘制在屏幕上(如果没有上一个图层)。创建一个新图层的好处之一是在处理xformode的时候,原图层上的图片和背景都会影响dst和src的合成。这时使用一个新图层是一个很好的选择。

//创建一个指定大小的图层

public int saveLayer(RectF bounds, Paint paint)

public int saveLayer(float left, float top, float right, float bottom, Paint paint)

Canvas还提供了另外两个方法用于创建指定透明度的图层,在该图层上绘制的图形都会带有指定的透明度:

//创建一个指定大小和透明度的图层。参数alpha为透明度,取值为0到255

public int saveLayerAlpha(RectF bounds, int alpha)

public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha)

使用和不使用saveLayer的大致工作流程:

①不使用layer

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 ②使用layer

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

Canvas提供的save,saveLayer等保存状态函数还提供了很多flag,类似MATRIX_SAVE_FLAG,CLIP_SAVE_FLAG, ALL_SAVE_FLAG等,默认如果调用没有flag的函数flag默认为 ALL_SAVE_FLAG就是所有的状态都保存,而且新的api把带有flag的函数都标记成了deprecated,推荐使用不带flag的函数,进行全部特性的保存。

在调用saveLayer时,可以传入一个saveFlags参数,它有如下几个参数可以设置:

MATRIX_SAVE_FLAG  只保存图层的matrix矩阵

CLIP_SAVE_FLAG  只保存大小信息

HAS_ALPHA_LAYER_SAVE_FLAG    表明该图层有透明度,和下面的标识冲突,都设置时以下面的标志为准

FULL_COLOR_LAYER_SAVE_FLAG   完全保留该图层颜色(和上一图层合并时,清空上一图层的重叠区域,保留该图层的颜色)

CLIP_TO_LAYER_SAVE_FLAG   创建图层时,会把canvas(所有图层)裁剪到参数指定的范围,如果省略这个flag将导致图层开销巨大(实际上图层没有裁剪,与原图层一样大)

ALL_SAVE_FLAG  保存所有信息

 

Canvas 在一般的情况下可以看作是一张画布,所有的绘图操作如drawBitmap, drawCircle都发生在这张画布上,这张画板还定义了一些属性比如Matrix,颜色等等。但是如果需要实现一些相对复杂的绘图操作,比如多层动画,地图(地图可以有多个地图层叠加而成,比如:政区层,道路层,兴趣点层)。Canvas提供了图层(Layer)支持,缺省情况可以看作是只有一个图层Layer。如果需要按层次来绘图,Android的Canvas可以使用SaveLayerXXX, Restore 来创建一些中间层,对于这些Layer是按照“栈结构“来管理的:  

c7fd2a6cc75b47b5bbbebbd2c03b556e.png

创建一个新的Layer到“栈”中,可以使用saveLayer, savaLayerAlpha,;从“栈”中推出一个Layer,可以使用restore,restoreToCount。但Layer入栈时,后续的DrawXXX操作都发生在这个Layer上,而Layer退栈时,就会把本层绘制的图像“绘制”到上层或是Canvas上,在复制Layer到Canvas上时,可以指定Layer的透明度(Layer),这是在创建Layer时指定的:public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)本例Layers 介绍了图层的基本用法:Canvas可以看做是由两个图层(Layer)构成的。

savelayer和saveLayerAlpha函数调用时会生成一个新的bitmap用于绘制,后续的操作都不会对原来的Canvas造成影响。调用restore或者resoreToCount()函数之后,新生成的bitmap最终会绘制到Canvas对应的原始Bitmap上,也会从canvas状态栈中获取状态信息,对canvas进行恢复。返回getSaveCount的值,没有调用过一次save,getSaveCount值为1。

saveLayerAlpha和saveLayer的区别只是saveLayerAlpha指定了新生成的bitmap的透明度。

推荐使用save,因为save不会新创建bitmap,saveLayer会创建新的bitmap,如果创建的bitmap过大会导致内存泄漏,这点在savelayer函数上有说明,如果一定要使用saveLayer一定要给出确定的大小,防止内存泄漏。

所有的save,saveLayer系列函数都有返回值,返回的是restoreToCount(),也就是调用了save次数。

没有调用任何一次save时的canvas.getSaveCount()的值为1。save,saveLayer,savelayeralpha保存画布信息共用一个栈,所以每次调用save函数getSaveCount函数都会加一,每次调用restore函数getSaveCount函数都会减一。调用restoreToCount(id)则会直接退栈到id标识的canvas状态,此时在其顶部保存的状态信息都已经被弹栈了。

多次调用save函数,可以多次进行restore恢复,restore之后进行绘制,会在当前状态canvas画布上进行绘制,受当前Canvas状态的影响。

重要:

调用save或者saveLayer系列函数是有返回值的,这个返回值就可以作为restoreToCount的函数实参,可以返回到保存之前的画布状态。

例如调用save或者saveLayer后返回saveId为2,那么现在getSaveCount的值应该为3,此时直接调用restoreToCount(2),就可以返回调用save或者saveLayer之前的状态,而且可以保存这个获取到的saveId值,在特定的位置利用restoreToCount(saveId),就可以回到生成这个saveId之前的状态。

restore ,restoreToCount两个函数都是用于恢复画布,restore直接取保存在栈中的栈顶的画布状态进行恢复,restoreToCount:是对restore的封装,可以直接弹栈直到目标位置的画布状态,当saveCount小于1时会报错。

 

需要注意,如果绘制过程需要对canvas进行多次的几何变换,那么需要倒叙来写几何变换过程。比如需要先平移再旋转,那么在写代码的时候,就需要先旋转再平移。

这里主要是因为屏幕的坐标系和canvas坐标系是两个坐标系,需要进行一定的的空间想象。

当然,也可以初始化一个Matrix,合理的使用preXXX和postXXX,对该Matrix进行几何变换操作,然后将其应用到canvas上。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/332570
推荐阅读
相关标签
  

闽ICP备14008679号