Android canvas

2023-05-16

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上。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android canvas 的相关文章

  • Android上如何模拟后台Activity因内存不足而被系统杀死的过程?

    我正在处理 内存不足 不再有后台进程 问题 当这种情况发生时 我的活动处于后台并被杀死 我正在尝试保存并加载实例状态来解决它 但因为它并不是每次都会发生 在这种情况下我应该如何测试我的活动 Thanks 您可以通过 adb 强制进程终止 g
  • 让协程等待之前的调用

    我还没有完全掌握 Kotlin 协程 基本上我希望协程在执行之前等待任何先前的调用完成 下面的代码似乎可以工作 但它正在做我认为它正在做的事情吗 private var saveJob Job null fun save saveJob s
  • Android 中的 Sugar ORM:更新 SQLite 中保存的对象

    我是在 Android 上使用 SQLite 和 Sugar ORM 进行应用程序开发的新手 并尝试阅读 Sugar ORM 文档 但没有找到有关如何更新 SQLite 中保存的对象的任何信息 更改对象属性后还可以保存对象吗 就像是 Cus
  • android中根据屏幕尺寸计算图像尺寸

    我正在尝试根据屏幕尺寸计算图像高度和宽度 我从后端获取 5 x 7 尺寸的图像 为了将像素乘以 72 进行转换 我有 360 X 504 尺寸的图像 对于 360 X 504 我的动态透明矩形区域将显示为 1 223 x 1 179 即 8
  • Android PhoneGap 插件,UI 选项卡栏,调整 WebView 大小

    我正在创建一个美味的 PhoneGap 插件 希望一旦它能被打开 准备好了 插件基本完成了 我只需要一个漂亮的用户界面 相互作用 简而言之 我想创建一个 本机 android 工具栏组件 如果您实现 PhoneGap UIControls
  • 如何向开发人员发送崩溃报告?

    我开发 Android 应用程序 但在某些情况下我的应用程序force close 如果出现以下情况 我如何向开发人员发送包含详细信息的电子邮件force close随时发生 The ACRA https github com ACRA a
  • Android中如何检测WIFI连接何时建立?

    我需要检测何时通过 WIFI 建立网络连接 发送什么广播来确定已建立有效的网络连接 我需要验证是否存在有效的 HTTP 网络连接 我应该监听什么以及需要进行哪些额外测试才能知道是否存在有效连接 您可以注册一个BroadcastReceive
  • 在 Android Lollipop 中从 Uri 中裁剪照片后总是返回 Null?

    我尝试在拍照或挑选照片后从 Uri 中裁剪图像 我的代码是这样的 public static void cropImage Uri uri Activity activity int action code Intent intent ne
  • Android Fragment onCreateView 与手势

    我正在尝试在片段中使用手势 我在 FragmentActivity 中有以下内容来处理我的详细信息片段 我试图发生的情况是 当在视图上检测到滑动时 将该视图内的数据替换为上一个或下一个条目 如果有更好的方法来处理这个问题 我完全同意 然而
  • Android:使 Dialog 周围的所有内容都比默认值更暗

    我有一个具有以下样式的自定义对话框 它显示了一个无边框对话框 后面的任何内容都会 稍微 变暗 我的设计师希望背后的一切都比 Android 的默认设置更暗 但不是完全黑色 有这样的设置吗 我能想到的唯一解决方法是使用全屏活动而不是对话框 只
  • 使用 Android Studio 进行调试永远停留在“等待调试器”状态

    UPDATE The supposed重复是一个关于陷入 等待调试器 执行时Run 而这个问题就陷入了 等待调试器 执行时Debug 产生问题的步骤不同 解决方案也不同 每当我尝试使用Android Studio的调试功能时 运行状态总是停
  • Emma 不生成coverage.ec

    我设置了艾玛 它曾经对我有用 然后我们更改了源代码 现在它没有生成coverage ec根本不 它确实生成coverage em 测试临近结束时 出现错误消息 exec INSTRUMENTATION CODE 0 echo Downloa
  • Android 纹理仅显示纯色

    我正在尝试在四边形上显示单个纹理 我有一个可用的 VertexObject 它可以很好地绘制一个正方形 或任何几何对象 现在我尝试扩展它来处理纹理 但纹理不起作用 我只看到一种纯色的四边形 坐标数据位于 arrayList 中 the ve
  • android textview 有字符限制吗?

    我正在尝试在 android TextView 中输入超过 2000 3000 个字符 它不显示任何内容 任何一份指南是否对 android textview 有字符限制或什么 我在G3中做了一些小测试 我发现 如果activtiy布局中有
  • 屏幕开/关检测

    在这里 我试图确定屏幕是否打开 但按下电源锁定 解锁按钮时它似乎不起作用 应用程序运行没有错误 但 if else 中的代码似乎没有效果 Edited现在代码可以工作了 谢谢Olgun 但媒体播放器播放不会停止 并且每次在屏幕上 离屏时都会
  • 是否可以使用 CardView 为浮动操作按钮制作阴影?

    I know CardView不是为此而设计的 但理论上如果cardCornerRadius view size 2它应该导致圆圈 我错过了什么吗 绘制真实的动画阴影并不困难 您可以尝试在 Froyo 等任何 Android 设备上实现 L
  • Android AdMob:addView 在返回活动之前不会显示广告

    我正在尝试在游戏顶部添加横幅广告 我的活动使用带有自定义 SurfaceView 的relativelayout 我希望广告与 SurfaceView 重叠 广告会加载并可点击 但不会绘制到屏幕上 当我离开活动并返回时 会绘制广告 例如 通
  • 使用Intent拨打电话需要权限吗?

    在我的一个应用程序中 我使用以下代码来拨打电话 Intent intent new Intent Intent ACTION CALL Uri parse startActivity intent 文档说我确实需要以下清单许可才能这样做
  • 如何正确编写AttributeSet的XML?

    我想创建一个面板适用于 Android 平台的其他小部件 http code google com p android misc widgets 在运行时 XmlPullParser parser getResources getXml R
  • 将焦距(以毫米为单位)转换为像素 - Android

    在 Android 中 我当前正在访问camera s焦距通过使用getFocalLength in Camera1 Camera2不是一个选择 我正在尝试完全填充当前的计算 focal length pix focal length m

随机推荐

  • Python3中的urllib.request模块

    Python 3 x版本后的urllib和urllib2 现在的Python已经出到了3 5 2 在Python 3以后的版本中 xff0c urllib2这个模块已经不单独存在 xff08 也就是说当你import urllib2时 xf
  • Python 正则re模块之findall()详解

    1 先说一下findall 函数的两种表示形式 import re kk 61 re compile r 39 d 43 39 kk findall 39 one1two2three3four4 39 1 2 3 4 注意此处findall
  • 用python实现将文件拷贝到指定目录

    基本方法 import os import shutil alllist 61 os listdir u 34 D notes python 资料 34 for i in alllist aa bb 61 i split 34 34 if
  • Ubuntu16.04忘记用户登录密码以及管理员密码,重置密码的解决方案

    1 问题现象 xff1a 由于自己想修改一下当前用户名 xff0c 结果乱改了部分配置文件导致登陆时 xff0c 原先的密码失效 2 问题原因 问题原因 xff0c 搞不懂 xff0c 只是修改了 etc shadow和 etc sudoe
  • 点赋科技:本地生活,如何开启复苏之路

    目前 xff0c 全球经历 这 场前所未有的疫情大流行 已经结束 xff0c 尽管 许多国家和地区的经济和社会都受到了影响 然而 xff0c 做好本地生活的复苏规划和推进 xff0c 将有助于在疫情之后尽快走出经济低迷期 xff0c 恢复社
  • 1130, "Host 'xxxx' is not allowed to connect to this MySQL server"

    问题描述 xff1a 1 在centos装好mysql后 xff0c 在python3程序中通过pymysql远程连接mysql xff0c 但是报 Host 39 39 not allowed connect错误 解决方法 xff1a 1
  • Pycharm配置Git教程

    1 使用场景 平时习惯在windows下开发 xff0c 但是我们又需要实时将远方仓库的代码clone到本地 xff0c 也许要将自己修改的代码push到远端服务器 xff0c 有很多方法可以实现这个需求 xff0c 但是所用的编辑软件不一
  • Ubuntu18.04安装NVIDIA驱动后,循环登录,登录界面进不去,输完密码又回到登录界面

    我安装的是Ubuntu18 04 5 xff0c 3090公版显卡 xff0c 在安装好驱动之后 xff0c 一直循环在登录界面 xff0c 输入密码之后一闪又回到登录界面 xff0c 重装了多次驱动还是不行 解决方法 xff1a 后来发现
  • 安装达梦数据库选择安装路径时提示“无写入权限”

    在使用中标麒麟的Linux系统虚拟机安装达梦数据库时 xff0c 遇到了选择安装路径时 xff0c 数据库安装程序报 无写入权限 问题 经过一番折腾后发现 xff0c 问题原因时系统的临时目录空间太小所导致的 解决方法 xff1a 1 重新
  • Docker部署MySQL单机版

    简单版 一 查看本机是否有MySQL及MySQL端口 防止端口占用 xff09 ps ef grep mysql 二 拉取MySQL镜像 docker pull mysql 5 7 三 运行MySQL镜像 docker run d p 33
  • win10无法关机解决方法

    win10无法关机怎么办 下面阐述一下处理的过程 1 左键双击控制面板 控制面板已放到桌面 xff0c 再单击电源选项 2 在打开的电源选项窗口 xff0c 左键点击 xff1a 选择电源按纽的功能 xff0c 打开系统设置 3 在系统设置
  • 小米电视访问电脑共享文件夹

    输入win 43 R打开运行窗口 输入control进入控制面板 点击 网络和internet 网络共享中心 更改高级共享设置 a 专用 网络设置如图 xff1a b 来宾或公用 网络设置如图 xff1a c 所有网络 设置如图 xff1a
  • 让Everything搜索结果更清爽

    Everything的文件搜索功能很强大 xff0c 但是默认设置下搜索出的结果过于丰富 xff0c 总是会有一些乱七八糟的后缀名文件 xff08 如下图 xff09 xff0c 或许我们并不想搜索出那些文件 这时我们需要对它设置里的排除列
  • 上机 Qt5.14.2 编程应用

    上机 Qt5 14 2 编程应用 关于QT Qt是一个1991年由Qt Company开发的跨平台C 43 43 图形用户界面应用程序开发框架 它既可以开发GUI程序 xff0c 也可用于开发非GUI程序 xff0c 比如控制台工具和服务器
  • Android Studio报错:Error:Could not find com.android.tools.build:gradle:4.1 记一次不长记性的坑

    本文地址 xff1a https blog csdn net zengsidou article details 79797417 看字面意思 xff0c 这个问题是Gradle没有对应版本 在搜索引擎没有找到方法之后 xff0c 尝试自己
  • VBox关闭dhcp

    VBox关闭dhcp C Program Files Oracle VirtualBox gt VBoxManage exe list dhcpservers NetworkName HostInterfaceNetworking Virt
  • Android 使用LottieAnimationView 做启动动画

    lt xml version 61 34 1 0 34 encoding 61 34 utf 8 34 gt lt RelativeLayout xmlns android 61 34 http schemas android com ap
  • Android OkHttp★

    1 OkHttp OkHttp是Square公司开发的一个处理网络请求的开源项目 是目前Android使用最广泛的网络框架 OkHttp的特点 支持HTTP 2并允许对同一主机的所有请求共享一个socket连接 如果非HTTP 2 则通过连
  • Android GestureDetector★★★

    1 GestureDetecor 用户触摸屏幕时会产生许多手势 xff0c 一般通过重写View类的onTouch 方法可以处理一些触摸事件 xff0c 但是这个方法太过简单 xff0c 如果需要处理一些复杂的手势 xff0c 用这个接口就
  • Android canvas

    1 Canvas Canvas指画布 xff0c 表现在屏幕上就是一块区域 xff0c 可以在上面使用各种API绘制想要的东西 canvas内部维持了一个mutable Bitmap xff0c 所以它可以使用颜色值去填充整个Bitmap