Android进阶自定义Android自定义View

Android高级进阶——绘图篇(七)Canvas 与 图层(一

2018-04-24  本文已影响102人  aKaiC

开篇

前面很多篇文章都用到了图层的概念,但是一直没有详细介绍,今天这篇文章将详细的介绍 Canvas 与 图层的概念

一、如何获得一个Canvas对象

protected void onDraw(Canvas canvas) {  
    super.onDraw(canvas);  
}  
  
protected void dispatchDraw(Canvas canvas) {  
    super.dispatchDraw(canvas);  
}  

Canvas c = new Canvas(bitmap);

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

二、在OnDraw()中使用

我们一定要注意的是,如果我们用bitmap构造了一个canvas,那这个canvas上绘制的图像也都会保存在这个bitmap上,而不是画在View上,如果想画在View上就必须使用OnDraw(Canvas canvas)函数中传进来的canvas画一遍bitmap才能画到view上。
下面举个例子:

image.png
    private void init() {
        //初始化画笔
        paint = new Paint();
        //设置画笔颜色
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        paint.setTextSize(50);

        bitmap = Bitmap.createBitmap(800, 400, Bitmap.Config.ARGB_8888);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //禁用硬件加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        Canvas canvas1 = new Canvas(bitmap);
        canvas1.drawText("你是来搞笑的么!!!", 100, 100, paint);
        canvas.drawBitmap(bitmap, 100, 100, paint);
    }

可以看到,毛线也没有,这是为什么呢?
我们仔细来看一下onDraw函数:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //禁用硬件加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        Canvas canvas1 = new Canvas(bitmap);
        canvas1.drawText("你是来搞笑的么!!!", 100, 100, paint);

    }

在onDraw函数中,我们只是将文字画在了mBmpCanvas上,也就是我们新建mBmp图片上!这个图片跟我们view没有任何关系好吧,我们需要把mBmp图片画到view上才行,所以我们在onDraw中需要加下面这句,将mBmp画到view上

canvas.drawBitmap(bitmap, 100, 100, paint);

三、图层与画布

void translate(float dx, float dy)

参数说明:

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  
      
    //translate  平移,即改变坐标系原点位置  
      
    Paint paint = new Paint();  
    paint.setColor(Color.GREEN);  
    paint.setStyle(Style.FILL);  
      
//  canvas.translate(100, 100);  
    Rect rect1 = new Rect(0,0,400,220);  
    canvas.drawRect(rect1, paint);  

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

image.png

二、屏幕显示与Canvas的关系

很多童鞋一直以为显示所画东西的改屏幕就是Canvas,其实这是一个非常错误的理解,比如下面我们这段代码:

这段代码中,同一个矩形,在画布平移前画一次,平移后再画一次,大家会觉得结果会怎样?

protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);

//构造两个画笔,一个红色,一个绿色  
Paint paint_green = generatePaint(Color.GREEN, Style.STROKE, 3);  
Paint paint_red   = generatePaint(Color.RED, Style.STROKE, 3);  
  
//构造一个矩形  
Rect rect1 = new Rect(0,0,400,220);  

//在平移画布前用绿色画下边框  
canvas.drawRect(rect1, paint_green);  
  
//平移画布后,再用红色边框重新画下这个矩形  
canvas.translate(100, 100);  
canvas.drawRect(rect1, paint_red);  

}
private Paint generatePaint(int color,Paint.Style style,int width)
{
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(style);
paint.setStrokeWidth(width);
return paint;
}

代码分析:
这段代码中,对于同一个矩形,在平移画布前利用绿色画下矩形边框,在平移后,再用红色画下矩形边框。大家是不是会觉得这两个边框会重合?实际结果是这样的。

image.png

从到这个结果大家可能会狠蛋疼,我第一次看到这个结果的时候蛋都碎一地了要。淡定……
这个结果的关键问题在于,为什么绿色框并没有移动?

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

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

image.png

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

image.png

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

下面对上面的知识做一下总结:

四、旋转(Rotate)

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

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

第一个构造函数直接输入旋转的度数,正数是顺时针旋转,负数指逆时针旋转,它的旋转中心点是原点(0,0)
第二个构造函数除了度数以外,还可以指定旋转的中心点坐标(px,py)

下面以第一个构造函数为例,旋转一个矩形,先画出未旋转前的图形,然后再画出旋转后的图形;

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  
      
  
    Paint paint_green = generatePaint(Color.GREEN, Style.FILL, 5);  
    Paint paint_red   = generatePaint(Color.RED, Style.STROKE, 5);  
      
    Rect rect1 = new Rect(300,10,500,100);  
    canvas.drawRect(rect1, paint_red); //画出原轮廓  
      
    canvas.rotate(30);//顺时针旋转画布  
    canvas.drawRect(rect1, paint_green);//画出旋转后的矩形  
}   
image.png

这个最终屏幕显示的构造过程是这样的:

下图显示的是第一次画图合成过程,此时仅仅调用canvas.drawRect(rect1, paint_red); 画出原轮廓

然后是先将Canvas正方向依原点旋转30度,然后再与上面的屏幕合成,最后显示出我们的复合效果。

image.png

有关Canvas与屏幕的合成关系我觉得我已经讲的够详细了,后面的几个操作Canvas的函数,我就不再一一讲它的合成过程了。

五、缩放(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:垂直方向伸缩的比例,同样,小数为缩小,整数为放大

注意:这里有X、Y轴的密度的改变,显示到图形上就会正好相同,比如X轴缩小,那么显示的图形也会缩小。一样的。

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  
      
//  //scale 缩放坐标系密度  
    Paint paint_green = generatePaint(Color.GREEN, Style.STROKE, 5);  
    Paint paint_red   = generatePaint(Color.RED, Style.STROKE, 5);  
      
    Rect rect1 = new Rect(10,10,200,100);  
    canvas.drawRect(rect1, paint_green);  
      
    canvas.scale(0.5f, 1);  
    canvas.drawRect(rect1, paint_red);  
}   
image.png

六、扭曲(skew)

其实我觉得译成斜切更合适,在PS中的这个功能就差不多叫斜切。但这里还是直译吧,大家都是这个名字。看下它的构造函数:

void skew (float sx, float sy)

参数说明:

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

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  
      
    //skew 扭曲  
    Paint paint_green = generatePaint(Color.GREEN, Style.STROKE, 5);  
    Paint paint_red   = generatePaint(Color.RED, Style.STROKE, 5);  
      
    Rect rect1 = new Rect(10,10,200,100);  
  
    canvas.drawRect(rect1, paint_green);  
    canvas.skew(1.732f,0);//X轴倾斜60度,Y轴不变  
    canvas.drawRect(rect1, paint_red);  
}   
image.png

七、裁剪画布(clip系列函数)

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

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来取得最新画布的函数,难度都不大,就不再一一讲述。利用ClipRect()来稍微一讲

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  
      
    canvas.drawColor(Color.RED);  
    canvas.clipRect(new Rect(100, 100, 200, 200));  
    canvas.drawColor(Color.GREEN);  
}

先把背景色整个涂成红色。显示在屏幕上
然后裁切画布,最后最新的画布整个涂成绿色。可见绿色部分,只有一小块,而不再是整个屏幕了。
关于两个画布与屏幕合成,我就不再画图了,跟上面的合成过程是一样的。

image.png

八、画布的保存与恢复(save()、restore())

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

int save ()
void restore()

这两个函数没有任何的参数,很简单。
Save():每次调用Save()函数,都会把当前的画布的状态进行保存,然后放入特定的栈中;
restore():每当调用Restore()函数,就会把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布,并在这个画布上做画。
为了更清晰的显示这两个函数的作用,下面举个例子:

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  
      
    canvas.drawColor(Color.RED);  
      
    //保存当前画布大小即整屏  
    canvas.save();   
      
    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);  
      
    //恢复整屏画布  
    canvas.restore();  
      
    canvas.drawColor(Color.BLUE);  
}   

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

image.png

下面我通过一个多次利用Save()、Restore()来讲述有关保存Canvas画布状态的栈的概念:代码如下:

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  
      
    canvas.drawColor(Color.RED);  
    //保存的画布大小为全屏幕大小  
    canvas.save();  
      
    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);  
    //保存画布大小为Rect(100, 100, 800, 800)  
    canvas.save();  
      
    canvas.clipRect(new Rect(200, 200, 700, 700));  
    canvas.drawColor(Color.BLUE);  
    //保存画布大小为Rect(200, 200, 700, 700)  
    canvas.save();  
      
    canvas.clipRect(new Rect(300, 300, 600, 600));  
    canvas.drawColor(Color.BLACK);  
    //保存画布大小为Rect(300, 300, 600, 600)  
    canvas.save();  
      
    canvas.clipRect(new Rect(400, 400, 500, 500));  
    canvas.drawColor(Color.WHITE);  
}   

显示效果为:

image.png

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

image.png

注意在,第四次Save()之后,我们还对画布进行了canvas.clipRect(new Rect(400, 400, 500, 500));操作,并将当前画布画成白色背景。也就是上图中最小块的白色部分,是最后的当前的画布。

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

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  
      
    canvas.drawColor(Color.RED);  
    //保存的画布大小为全屏幕大小  
    canvas.save();  
      
    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);  
    //保存画布大小为Rect(100, 100, 800, 800)  
    canvas.save();  
      
    canvas.clipRect(new Rect(200, 200, 700, 700));  
    canvas.drawColor(Color.BLUE);  
    //保存画布大小为Rect(200, 200, 700, 700)  
    canvas.save();  
      
    canvas.clipRect(new Rect(300, 300, 600, 600));  
    canvas.drawColor(Color.BLACK);  
    //保存画布大小为Rect(300, 300, 600, 600)  
    canvas.save();  
      
    canvas.clipRect(new Rect(400, 400, 500, 500));  
    canvas.drawColor(Color.WHITE);  
      
    //将栈顶的画布状态取出来,作为当前画布,并画成黄色背景  
    canvas.restore();  
    canvas.drawColor(Color.YELLOW);  
}   

上段代码中,把栈顶的画布状态取出来,作为当前画布,然后把当前画布的背景色填充为黄色


image.png

那如果我连续Restore()三次,会怎样呢?
我们先分析一下,然后再看效果:Restore()三次的话,会连续出栈三次,然后把第三次出来的Canvas状态当做当前画布,也就是Rect(100, 100, 800, 800),所以如下代码:

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  
      
    canvas.drawColor(Color.RED);  
    //保存的画布大小为全屏幕大小  
    canvas.save();  
      
    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);  
    //保存画布大小为Rect(100, 100, 800, 800)  
    canvas.save();  
      
    canvas.clipRect(new Rect(200, 200, 700, 700));  
    canvas.drawColor(Color.BLUE);  
    //保存画布大小为Rect(200, 200, 700, 700)  
    canvas.save();  
      
    canvas.clipRect(new Rect(300, 300, 600, 600));  
    canvas.drawColor(Color.BLACK);  
    //保存画布大小为Rect(300, 300, 600, 600)  
    canvas.save();  
      
    canvas.clipRect(new Rect(400, 400, 500, 500));  
    canvas.drawColor(Color.WHITE);  
      
    //连续出栈三次,将最后一次出栈的Canvas状态作为当前画布,并画成黄色背景  
    canvas.restore();  
    canvas.restore();  
    canvas.restore();  
    canvas.drawColor(Color.YELLOW);  
}   
image.png

介绍完了 canvas的save()和restore(),其实除了save()和restore()以外,还有其它一些函数来保存和恢复画布状态,这部分我们就来看看

1、saveLayer()

saveLayer()有两个函数:

/** 
 * 保存指定矩形区域的canvas内容 
 */  
public int saveLayer(RectF bounds, Paint paint, int saveFlags)  
public int saveLayer(float left, float top, float right, float bottom,Paint paint, int saveFlags)  

第二个构造函数实际与第一个是一样的,只不过是根据四个点来构造一个矩形。
下面我们来看一下例子,拿xfermode来做下试验,来看看saveLayer都干了什么:

    private void init() {
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        srcBmp = makeSrc(width, height);
        dstBmp = makeDst(width, height);
        mPaint = new Paint();

    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
        canvas.drawColor(Color.GREEN);  
  
        int layerID = canvas.saveLayer(0, 0, width * 2, height * 2, mPaint, Canvas.ALL_SAVE_FLAG);  
        canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
        canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);  
        mPaint.setXfermode(null);  
        canvas.restoreToCount(layerID);  
    }  
  
    // create a bitmap with a circle, used for the "dst" image  
    static Bitmap makeDst(int w, int h) {  
        Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);  
        Canvas c = new Canvas(bm);  
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);  
  
        p.setColor(0xFFFFCC44);  
        c.drawOval(new RectF(0, 0, w, h), p);  
        return bm;  
    }  
  
    // create a bitmap with a rect, used for the "src" image  
    static Bitmap makeSrc(int w, int h) {  
        Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);  
        Canvas c = new Canvas(bm);  
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);  
  
        p.setColor(0xFF66AAFF);  
        c.drawRect(0, 0, w, h, p);  
        return bm;  
    }  
}  

这段代码大家应该很熟悉,这是我们在讲解setXfermode()时的示例代码,但在saveLayer前把整个屏幕画成了绿色,效果图如下:

image.png

那么问题来了,如果我们把saveLayer给去掉,看看会怎样:

protected void onDraw(Canvas canvas) {  
    super.onDraw(canvas);  
    canvas.drawColor(Color.GREEN);  
    canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
    canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);  
    mPaint.setXfermode(null);  
}  

效果图就变这样了:

image.png

我擦类……去掉saveLayer()居然效果都不一样了……
我们先回顾下Mode.SRC_IN的效果:在处理源图像时,以显示源图像为主,在相交时利用目标图像的透明度来改变源图像的透明度和饱和度。当目标图像透明度为0时,源图像就完全不显示。
再回过来看结果,第一个结果是对的,因为不与圆相交以外的区域透明度都是0,而第二个图像怎么就变成了这屌样,源图像全部都显示出来了。

这是因为在调用saveLayer时,会生成了一个全新的bitmap,这个bitmap的大小就是我们指定的保存区域的大小,新生成的bitmap是全透明的,在调用saveLayer后所有的绘图操作都是在这个bitmap上进行的。
所以:

int layerID = canvas.saveLayer(0, 0, width * 2, height * 2, mPaint, Canvas.ALL_SAVE_FLAG);  
canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);  

我们讲过,在画源图像时,会把之前画布上所有的内容都做为目标图像,而在saveLayer新生成的bitmap上,只有dstBmp对应的圆形,所以除了与圆形相交之外的位置都是空像素。
在画图完成之后,会把saveLayer所生成的bitmap盖在原来的canvas上面。
所以此时的xfermode的合成过程如下图所示:

image.png

savelayer新建的画布上的图像做为目标图像,矩形所在的透明图层与之相交,计算结果画在新建的透明画布上。最终将计算结果直接盖在原始画布上,形成最终的显示效果。

然后我们再来看第二个示例,在第二个示例中,唯一的不同就是把saveLayer去掉了;
在saveLayer去掉后,所有的绘图操作都放在了原始View的Canvas所对应的Bitmap上了

protected void onDraw(Canvas canvas) {  
    super.onDraw(canvas);  
    canvas.drawColor(Color.GREEN);  
    canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
    canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);  
    mPaint.setXfermode(null);  
}  

由于我们先把整个画布给染成了绿色,然后再画上了一个圆形,所以在应用xfermode来画源图像的时候,目标图像当前Bitmap上的所有图像了,也就是整个绿色的屏幕和一个圆形了。所以这时候源图像的相交区域是没有透明像素的,透明度全是100%,这也就不难解释结果是这样的原因了。
此时的xfermode合成过程如下:

image.png

由于没有调用saveLayer,所以圆形是直接画在原始画布上的,而当矩形与其相交时,就是直接与原始画布上的所有图像做计算的。
所以有关saveLayer的结论来了:
saveLayer会创建一个全新透明的bitmap,大小与指定保存的区域一致,其后的绘图操作都放在这个bitmap上进行。在绘制结束后,会直接盖在上一层的Bitmap上显示。

上面我们讲到了画布(Bitmap)、图层(Layer)和Canvas的概念,估计大家都会被绕晕了;下面我们下面来具体讲解下它们之间的关系。
图层(Layer):每一次调用canvas.drawXXX系列函数时,都会生成一个透明图层来专门来画这个图形,比如我们上面在画矩形时的透明图层就是这个概念。
画布(bitmap):每一个画布都是一个bitmap,所有的图像都是画在bitmap上的!我们知道每一次调用canvas.drawxxx函数时,都会生成一个专用的透明图层来画这个图形,画完以后,就盖在了画布上。所以如果我们连续调用五个draw函数,那么就会生成五个透明图层,画完之后依次盖在画布上显示。
画布有两种,第一种是view的原始画布,是通过onDraw(Canvas canvas)函数传进来的,其中参数中的canvas就对应的是view的原始画布,控件的背景就是画在这个画布上的!
另一种是人造画布,通过saveLayer()、new Canvas(bitmap)这些方法来人为新建一个画布。尤其是saveLayer(),一旦调用saveLayer()新建一个画布以后,以后的所有draw函数所画的图像都是画在这个画布上的,只有当调用restore()、resoreToCount()函数以后,才会返回到原始画布上绘制。
Canvas:这个概念比较难理解,我们可以把Canvas理解成画板,Bitmap理解成透明画纸,而Layer则理解成图层;每一个draw函数都对应一个图层,在一个图形画完以后,就放在画纸上显示。而一张张透明的画纸则一层层地叠加在画板上显示出来。我们知道画板和画纸都是用夹子夹在一起的,所以当我们旋转画板时,所有画纸都会跟着旋转!当我们把整个画板裁小时,所以的画纸也都会变小了!
这一点非常重要,当我们利用saveLayer来生成多个画纸时,然后最上层的画纸调用canvas.rotate(30)是把画板给旋转了,所有的画纸也都被旋转30度!这一点非常注意
另外,如果最上层的画纸调用canvas.clipRect()将画板裁剪了,那么所有的画纸也都会被裁剪。唯一能够恢复的操作是调用canvas.revert()把上一次的动作给取消掉!
但在利用canvas绘图与画板不一样的是,画布的影响只体现在以后的操作上,以前画上去的图像已经显示在屏幕上是不会受到影响的。
这一点一定要理解出来,下面会用到。

九、save()、saveLayer()、saveLayerAlpha()中的用法

1、saveLayer的用法

saveLayer的声明如下:

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

我们前面提到了saveLayer会新建一个画布(bitmap),后续的所有操作都是在这个画布上进行的。下面我们来分别看下saveLayer使用中的注意事项
(1)、saveLayer后的所有动作都只对新建画布有效

我们先看个例子:

public class SaveLayerUseExample_3_1 extends View{  
    private Paint mPaint;  
    private Bitmap mBitmap;  
    public SaveLayerUseExample_3_1(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        mPaint = new Paint();  
        mPaint.setColor(Color.RED);  
        mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.dog);;  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
        canvas.drawBitmap(mBitmap,0,0,mPaint);  
  
        int layerID = canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint,Canvas.ALL_SAVE_FLAG);  
        canvas.skew(1.732f,0);  
        canvas.drawRect(0,0,150,160,mPaint);  
        canvas.restoreToCount(layerID);  
    }  
}  

效果图如下:

image.png

在onDraw中,我们先在view的原始画布上画上了小狗的图像,然后利用saveLayer新建了一个图层,然后利用canvas.skew将新建的图层水平斜切45度。所以之后画的矩形(0,0,150,160)就是斜切的。
而正是由于在新建画布后的各种操作都是针对新建画布来操作的,不会对以前的画布产生影响,从效果图中也明显可以看出,将画布水平斜切45度也只影响了saveLayer的新建画布,并没有对之前的原始画布产生影响。

在saveLayer的参数中,我们可以通过指定Rect对象或者指定四个点来来指定一个矩形,这个矩形的大小就是新建画布的大小,我们举例来看一下:

public class SaveLayerUseExample_3_1 extends View {  
    private Paint mPaint;  
    private Bitmap mBitmap;  
  
    public SaveLayerUseExample_3_1(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        mPaint = new Paint();  
        mPaint.setColor(Color.RED);  
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);  
        ;  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);  
  
        int layerID = canvas.saveLayer(0, 0, 100, 100, mPaint, Canvas.ALL_SAVE_FLAG);  
        canvas.drawRect(0, 0, 500, 600, mPaint);  
        canvas.restoreToCount(layerID);  
    }  
}  

效果图如下:


image.png

在绘图时,我们先把小狗图片绘制在原始画布上的,然后新建一个大小为(0,0,100,100)大小的透明画布,然后再在这个画布上画一个(0, 0, 500, 600)的矩形。由于画布大小只有(0,0,100,100),所以(0, 0, 500, 600)这个矩形并不能完全显示出来,也只能显示出来(0,0,100,100)画布大小的部分。
那有些同学会说了,nnd,为了避免画布太小而出现问题,我每次都新建一个屏幕大小的画布多好,这样虽然是不会出现问题,但你想过没有,屏幕大小的画布需要多少空间吗,按一个像素需要8bit存储空间算,1024768的机器,所使用的bit数就是1024768*8=6.2M!所以我们在使用saveLayer新建画布时,一定要选择适当的大小,不然你的APP很可能OOM哦。

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

相比saveLayer,多一个alpha参数,用以指定新建画布透明度,取值范围为0-255,可以用16进制的oxAA表示;
这个函数的意义也是在调用的时候会新建一个bitmap画布,以后的各种绘图操作都作用在这个画布上,但这个画布是有透明度的,透明度就是通过alpha值指定的。
我们来看个示例

public class SaveLayerAlphaView extends View {  
    private Paint mPaint;  
    public SaveLayerAlphaView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        mPaint = new Paint();  
        mPaint.setColor(Color.RED);  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
  
        canvas.drawRect(100,100,300,300,mPaint);  
  
        int layerID = canvas.saveLayerAlpha(0,0,600,600,0x88,Canvas.ALL_SAVE_FLAG);  
        mPaint.setColor(Color.GREEN);  
        canvas.drawRect(200,200,400,400,mPaint);  
        canvas.restoreToCount(layerID);  
  
    }  
}  

效果图如下:

image.png

在saveLayerAlpha以后,我们画了一个绿色的矩形,由于把saveLayerAlpha新建的矩形的透明度是0x88(136)大概是50%透明度,从效果图中也可以看出在新建图像与上一画布合成后,是具有透明度的。

好了,这篇文章就先到这里,下一篇详细给大家讲解有关参数中各个Flag的意义。

上一篇下一篇

猜你喜欢

热点阅读