🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
来看看这一节我们的重点,上一节因为之前从未涉及Canvas的clipXXX方法所以我们优先对其做了一定的介绍并顺带将Path类的方法做了一个小结,如我之前所说Canvas方法可以分为几类,clipXXX算一类,各种drawXXX又是一类,还有一类则是对Canvas的各种变换操作,这一节我们将来具体看看关于Canvas变换操作的一些具体内容,在讲解之前呢我们先来了解一个关于“层”的设计理念,为什么说它是一个设计理念呢?因为在很多很多的地方,当然不止是开发,还有设计等领域你都能见到它的踪影,那么何为图层呢?大家小时候一定都画过画,比如下面这种: ![](https://box.kancloud.cn/2016-05-30_574c260bdcf64.png) 一个松鼠、几棵树、两个鸟、一个日,这几个简单的图画其实就包含了最简单“层”的概念,由图我们可以知道松鼠一定是在树和地面的前面,而树和地面的关系呢则比较模糊,可以是树在前也可以是地面在前,日肯定是在最底层的,两只鸟我们按照一般逻辑可以推测在日的上一层也就是倒数第二层,那么从底层到顶层我们就有这样的一个层次关系:日-鸟-树/地面-地面/树-松鼠,这么一说大家觉得好像也是,但是目测没毛用啊……意义何在,别急,想像一下,这时候如果你不想要松鼠而是想放一只猫在前面……或者你想把松鼠放在树的后面“藏”起来……这时你就蛋疼了,不停地拿橡皮擦擦啊擦草啊草,一不小心还得把其他的擦掉一块,这时候你就会想可以不可以有这么一个功能能让不同的元素通过一定的次序单独地画在一张大小一致“纸”上直到画完最后一个元素后把这些所有“纸”上的元素都整合起来构成一幅完整的图画呢?这样一个功能的存在能大大提高我们绘图的效率还能实现更多的绘图功能,基于这样的一个假想,“层”的概念应运而生: ![](https://box.kancloud.cn/2016-05-30_574c260c12764.png) 如上图所示,位于最底层的是一个圆,第二层是一个蓝色的椭圆,最顶层的是两个蓝色的圆,三个层中不同的元素最终构成右边的图像,这就是图层最直观也是最简单的体现。在Android中我们可以使用Canvas的saveXXX和restoreXXX方法来模拟图层的类似效果: ~~~ public class LayerView extends View { private Paint mPaint;// 画笔对象 private int mViewWidth, mViewHeight;// 控件宽高 public LayerView(Context context, AttributeSet attrs) { super(context, attrs); // 实例化画笔对象并设置其标识值 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { /* * 获取控件宽高 */ mViewWidth = w; mViewHeight = h; } @Override protected void onDraw(Canvas canvas) { /* * 绘制一个红色矩形 */ mPaint.setColor(Color.RED); canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint); /* * 保存画布并绘制一个蓝色的矩形 */ canvas.save(); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restore(); } } ~~~ 如代码所示,我们先在onDraw方法中绘制一个红色的大矩形再保存画布绘制了一个蓝色的小矩形: ![](https://box.kancloud.cn/2016-05-30_574c260c592c8.png) 此时我们尝试旋转一下我们的画布: ~~~ @Override protected void onDraw(Canvas canvas) { // 旋转画布 canvas.rotate(30); /* * 绘制一个红色矩形 */ mPaint.setColor(Color.RED); canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint); /* * 保存画布并绘制一个蓝色的矩形 */ canvas.save(); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restore(); } ~~~ 如代码所示顺时针旋转30度,这里要注意,我们在对Canvas(实际上大多数Android中的其他与坐标有关的)进行坐标操作的时候,默认情况下是以控件的左上角为原点坐标的,效果如下: ![](https://box.kancloud.cn/2016-05-30_574c260c6f30d.png) 可以看到两个矩形都一起飞了,可是我们只想让蓝色的飞而红色的不动怎么办呢?很简单,我们只在保存的图层里操作即可: ~~~ @Override protected void onDraw(Canvas canvas) { /* * 绘制一个红色矩形 */ mPaint.setColor(Color.RED); canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint); /* * 保存画布并绘制一个蓝色的矩形 */ canvas.save(); mPaint.setColor(Color.BLUE); // 旋转画布 canvas.rotate(30); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restore(); } ~~~ 可以看到,我们只针对蓝色的矩形进行了旋转: ![](https://box.kancloud.cn/2016-05-30_574c260c7f943.png) 至此结合上一节对Canvas的一些原理阐述我们该对它有个全新的认识,之前我们一直称其为画布,其实更准确地说Canvas是一个容器,如果把Canvas理解成画板,那么我们的“层”就像张张夹在画板上的透明的纸,而这些纸对应到Android则是一个个封装在Canvas中的Bitmap。 除了save()方法Canvas还给我们提供了一系列的saveLayerXXX方法给我们保存画布,与save()方法不同的是,saveLayerXXX方法会将所有的操作存到一个新的Bitmap中而不影响当前Canvas的Bitmap,而save()方法则是在当前的Bitmap中进行操作,并且只能针对Bitmap的形变和裁剪进行操作,saveLayerXXX方法则无所不能,当然两者还有很多的不同,我们稍作讲解。虽然save和saveLayerXXX方法有着很大的区别但是在一般应用上两者能实现的功能是差不多,上面的代码我们也可以改成这样: ~~~ @Override protected void onDraw(Canvas canvas) { /* * 绘制一个红色矩形 */ mPaint.setColor(Color.RED); canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint); /* * 保存画布并绘制一个蓝色的矩形 */ canvas.saveLayer(0, 0, mViewWidth, mViewHeight, null, Canvas.ALL_SAVE_FLAG); mPaint.setColor(Color.BLUE); // 旋转画布 canvas.rotate(30); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restore(); } ~~~ 当然实现的效果也是一样的就不多说了。saveLayer可以让我们自行设定需要保存的区域,比如我们可以只保存和蓝色方块一样的区域: ~~~ @Override protected void onDraw(Canvas canvas) { /* * 绘制一个红色矩形 */ mPaint.setColor(Color.RED); canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint); /* * 保存画布并绘制一个蓝色的矩形 */ canvas.saveLayer(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, null, Canvas.ALL_SAVE_FLAG); mPaint.setColor(Color.BLUE); // 旋转画布 canvas.rotate(30); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restore(); } ~~~ 这时候如果你运行就会发现蓝色的方块已经不见了,因为我们图层的大小就这么点,超出的部分就不能被显示了,这时我们改小画布旋转: `canvas.rotate(5); ` 你就可以看到旋转后的蓝色方块的一角: ![](https://box.kancloud.cn/2016-05-30_574c260c93e1d.png) 是不是有点类似于clipRect的效果呢?那么很多朋友会好奇为什么会有这样一种保存一小块画布区域的功能呢?其实原因很简单,上面我们说了saveLayerXXX方法会将操作保存到一个新的Bitmap中,而这个Bitmap的大小取决于我们传入的参数大小,Bitmap是个相当危险的对象,很多朋友在操作Bitmap时不太理解其原理经常导致OOM,在saveLayer时我们会依据传入的参数获取一个相同大小的Bitmap,虽然这个Bitmap是空的但是其会占用一定的内存空间,我们希望尽可能小地保存该保存的区域,而saveLayer则提供了这样的功能,顺带提一下,onDraw方法传入的Canvas对象的Bitmap在Android没引入HW之前理论上是无限大的,实际上其依然是根据你的图像来不断计算的,而在引入HW之后,该Bitmap受到限制,具体多大大家可以尝试画一个超长的path运行下你就可以在Logcat中看到warning。 好了,闲话不扯,接着说,除了saveLayer,Canvas还提供了一个saveLayerAlpha方法,顾名思义,该方法可以在我们保存画布时设置画布的透明度: ~~~ @Override protected void onDraw(Canvas canvas) { /* * 绘制一个红色矩形 */ mPaint.setColor(Color.RED); canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint); /* * 保存画布并绘制一个蓝色的矩形 */ canvas.saveLayerAlpha(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, 0x55, Canvas.ALL_SAVE_FLAG); mPaint.setColor(Color.BLUE); // 旋转画布 canvas.rotate(5); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restore(); } ~~~ 我们将saveLayer替换成saveLayerAlpha并设置透明值为0x55,运行可得如下效果: ![](https://box.kancloud.cn/2016-05-30_574c260ca3cd7.png) 可见蓝色的方块被半透明了。such easy!如果大家留心,会发现save()也有个重载方法save (int saveFlags),而saveLayer和saveLayerAlpha你也会发现又一个类似的参数,那么这个参数是干嘛用的呢?在Canvas中有六个常量值: ![](https://box.kancloud.cn/2016-05-30_574c260cb6970.png) 这六个常量值分别标识了我们在调用restore方法后还原什么,六个标识位除了CLIP_SAVE_FLAG、MATRIX_SAVE_FLAG和ALL_SAVE_FLAG是save和saveLayerXXX方法都通用外其余三个只能使saveLayerXXX方法有效,ALL_SAVE_FLAG很简单也是我们新手级常用的标识保存所有,CLIP_SAVE_FLAG和MATRIX_SAVE_FLAG也很好理解,一个是裁剪的标识位一个是变换的标识位,CLIP_TO_LAYER_SAVE_FLAG、FULL_COLOR_LAYER_SAVE_FLAG和HAS_ALPHA_LAYER_SAVE_FLAG只对saveLayer和saveLayerAlpha有效,CLIP_TO_LAYER_SAVE_FLAG表示对当前图层执行裁剪操作需要对齐图层边界,FULL_COLOR_LAYER_SAVE_FLAG表示当前图层的色彩模式至少需要是8位色,而HAS_ALPHA_LAYER_SAVE_FLAG表示在当前图层中将需要使用逐像素Alpha混合模式,关于色彩深度和Alpha混合大家可以参考维基百科,这里就不多说,这些标识位,特别是layer的标识位,大大超出了本系列的范畴,我就不多说了,平时使用大家可以直接ALL_SAVE_FLAG,有机会将单独开一篇剖析Android对色彩的处理。 所有的save、saveLayer和saveLayerAlpha方法都有一个int型的返回值,该返回值作为一个标识给与了一个你当前保存操作的唯一ID编号,我们可以利用restoreToCount(int saveCount)方法来指定在还原的时候还原哪一个保存操作: ~~~ @Override protected void onDraw(Canvas canvas) { /* * 绘制一个红色矩形 */ mPaint.setColor(Color.RED); canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint); /* * 保存并裁剪画布填充绿色 */ int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200); canvas.drawColor(Color.GREEN); /* * 保存画布并旋转后绘制一个蓝色的矩形 */ int saveID2 = canvas.save(Canvas.MATRIX_SAVE_FLAG); // 旋转画布 canvas.rotate(5); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restoreToCount(saveID1); } ~~~ 如上代码所示,我们第一次保存画布并获取其返回值: `int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG); ` 然后对画布进行裁剪并填色,第二次保存画布并获取其返回值: `int saveID2 = canvas.save(Canvas.MATRIX_SAVE_FLAG); ` 然后绘制一个蓝色的矩形,最后我们只还原了了saveID1的画布状态,运行一下你会发现好像效果没什么不同啊: ![](https://box.kancloud.cn/2016-05-30_574c260cd6400.png) 然后我们试试 `canvas.restoreToCount(saveID2); ` 发现效果还是一样…………很多童鞋就困惑了,是哪不对么?没有,其实都是对的,你觉得奇怪是你还不理解save和restore,这里我在restore之后再绘制一个矩形: ~~~ @Override protected void onDraw(Canvas canvas) { /* * 绘制一个红色矩形 */ mPaint.setColor(Color.RED); canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint); /* * 保存并裁剪画布填充绿色 */ int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200); canvas.drawColor(Color.GREEN); /* * 保存画布并旋转后绘制一个蓝色的矩形 */ int saveID2 = canvas.save(Canvas.MATRIX_SAVE_FLAG); // 旋转画布 canvas.rotate(5); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restoreToCount(saveID2); mPaint.setColor(Color.YELLOW); canvas.drawRect(mViewWidth / 2F - 400, mViewHeight / 2F - 400, mViewWidth / 2F + 400, mViewHeight / 2F + 400, mPaint); } ~~~ 可以看到我在 `canvas.restoreToCount(saveID2); ` 之后又绘制了一个黄色的矩形: ![](https://box.kancloud.cn/2016-05-30_574c260ce5a84.png) 可是不管你如何调大这个矩形,你会发现它就那么大点……也就是说,这个黄色的矩形其实是被clip掉了,进一步说,我们绘制黄色矩形的这个操作其实说白了就是在saveID1的状态下进行的。前面我们曾说过save和saveLayerXXX方法有着本质的区别,saveLayerXXX方法会将所有操作在一个新的Bitmap中进行,而save则是依靠stack栈来进行,假设我们有如下代码: ~~~ @Override protected void onDraw(Canvas canvas) { /* * 保存并裁剪画布填充绿色 */ int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300); canvas.drawColor(Color.YELLOW); /* * 保存并裁剪画布填充绿色 */ int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200); canvas.drawColor(Color.GREEN); /* * 保存画布并旋转后绘制一个蓝色的矩形 */ int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.rotate(5); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); } ~~~ 此时,在Canvas内部会有这样的一个Stack栈: ![](https://box.kancloud.cn/2016-05-30_574c260d058ac.png) Canvas会默认保存一个底层的空间给我们绘制一些东西,当我们没有调用save方法时所有的绘图操作都在这个Default Stack ID中进行,每当我们调用一次save就会往Stack中存入一个ID,将其后所有的操作都在这个ID所指向的空间进行直到我们调用restore方法还原操作,上面代码我们save了三次且没有restore,stack的结构就如上图所示,此时如果我们继续绘制东西,比如: ~~~ @Override protected void onDraw(Canvas canvas) { /* * 保存并裁剪画布填充绿色 */ int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300); canvas.drawColor(Color.YELLOW); /* * 保存并裁剪画布填充绿色 */ int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200); canvas.drawColor(Color.GREEN); /* * 保存画布并旋转后绘制一个蓝色的矩形 */ int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG); // 旋转画布 canvas.rotate(5); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); mPaint.setColor(Color.CYAN); canvas.drawRect(mViewWidth / 2F, mViewHeight / 2F, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint); } ~~~ 我们在saveID3之后又画了一个青色的矩形,只要你不是傻子明眼都能看出这段代码是在saveID3所标识的空间中绘制的,因此其必然会受到saveID3的约束旋转: ![](https://box.kancloud.cn/2016-05-30_574c260d1bae5.png) 除此之外,大家还可以很明显的看到,这个矩形除了被旋转,还被clip了~也就是说saveID1、saveID2也同时对其产生了影响,此时我们再次尝试在saveID2绘制完我们想要的东西后将其还原: ~~~ /* * 保存并裁剪画布填充绿色 */ int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200); canvas.drawColor(Color.GREEN); canvas.restore(); ~~~ 同时将青色的矩形变大一点: `canvas.drawRect(mViewWidth / 2F, mViewHeight / 2F, mViewWidth / 2F + 400, mViewHeight / 2F + 400, mPaint); ` 这时我们得到什么样的效果呢: ![](https://box.kancloud.cn/2016-05-30_574c260d2cceb.png) 其实猜都猜得到,saveID2已经不再对下面的saveID3起作用了,也就是说当我们调用canvas.restore()后标志着上一个save操作的结束或者说回滚了。同理,我们再把saveID1也restore: ~~~ /* * 保存并裁剪画布填充绿色 */ int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300); canvas.drawColor(Color.YELLOW); canvas.restore(); ~~~ 这时saveID3将彻底不再受前面操作的影响: ![](https://box.kancloud.cn/2016-05-30_574c260d3e40a.png) 如果我们在绘制青色的矩形之前将saveID3也还原: ~~~ /* * 保存画布并旋转后绘制一个蓝色的矩形 */ int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.rotate(5); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restore(); ~~~ 那么这个青色的矩形将会被绘制在Default Stack ID上而不受其他save状态的影响: ![](https://box.kancloud.cn/2016-05-30_574c260d4f7a0.png) 上面我们提到的restoreToCount(int saveCount)方法接受一个标识值,我们可以根据这个标识值来还原特定的栈空间,效果类似就不多说了。每当我们调用restore还原Canvas,对应的save栈空间就会从Stack中弹出去,Canvas提供了getSaveCount()方法来为我们提供查询当前栈中有多少save的空间: ~~~ @Override protected void onDraw(Canvas canvas) { System.out.println(canvas.getSaveCount()); /* * 保存并裁剪画布填充绿色 */ int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG); System.out.println(canvas.getSaveCount()); canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300); canvas.drawColor(Color.YELLOW); canvas.restore(); /* * 保存并裁剪画布填充绿色 */ int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG); System.out.println(canvas.getSaveCount()); canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200); canvas.drawColor(Color.GREEN); canvas.restore(); /* * 保存画布并旋转后绘制一个蓝色的矩形 */ int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG); System.out.println(canvas.getSaveCount()); // 旋转画布 canvas.rotate(5); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restore(); System.out.println(canvas.getSaveCount()); mPaint.setColor(Color.CYAN); canvas.drawRect(mViewWidth / 2F, mViewHeight / 2F, mViewWidth / 2F + 400, mViewHeight / 2F + 400, mPaint); } ~~~ 运行后你会看到Logcat的如下输出: ![](https://box.kancloud.cn/2016-05-30_574c260d60cd1.png) OK,对层的了解到此为止,接下来我们主要来看看Canvas中的变换操作,说起变换,无非就几种:平移、旋转、缩放和错切,而我们的Canvas也继承了变换的精髓,同样提供了这几种相应的方法,前面的很多章节我们也都用到了,像translate(float dx, float dy)方法平移画布用了无数次,这里再次强调,translate方法会改变画布的原点坐标,原点坐标对变换的影响弥足轻重,前面也多次强调了!scale(float sx, float sy)缩放也很好理解,但是它有一个重载方法scale(float sx, float sy, float px, float py),后两个参数用于指定缩放的中心点,前两个参数用于指定横纵向的缩放比率值在0-1之间为缩小: ~~~ public class LayerView extends View { private Bitmap mBitmap;// 位图对象 private int mViewWidth, mViewHeight;// 控件宽高 public LayerView(Context context, AttributeSet attrs) { super(context, attrs); // 从资源中获取位图对象 mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.z); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { /* * 获取控件宽高 */ mViewWidth = w; mViewHeight = h; // 缩放位图与控件一致 mBitmap = Bitmap.createScaledBitmap(mBitmap, mViewWidth, mViewHeight, true); } @Override protected void onDraw(Canvas canvas) { canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.scale(1.0F, 1.0F); canvas.drawBitmap(mBitmap, 0, 0, null); canvas.restore(); } } ~~~ 当缩放比率为1时表示不缩放: ![](https://box.kancloud.cn/2016-05-30_574c260d7b55d.png) 我们改变下缩放比率: `canvas.scale(0.8F, 0.35F); ` 此时画面效果如下: ![](https://box.kancloud.cn/2016-05-30_574c260da079d.png) 可以看到缩放中心在左上角,我们可以使用scale的重载方法更改缩放中心: `canvas.scale(0.8F, 0.35F, mViewWidth, 0); ` 效果如下,很好理解: ![](https://box.kancloud.cn/2016-05-30_574c260dbe4ee.png) rotate(float degrees)和重载方法rotate(float degrees, float px, float py)类似前面也用过不少就不多说了,没接触过的只有skew(float sx, float sy)错切方法,关于错切的概念前面我们都有讲过很多,其实知道原理,方法再怎么变都不难: ~~~ @Override protected void onDraw(Canvas canvas) { canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.skew(0.5F, 0F); canvas.drawBitmap(mBitmap, 0, 0, null); canvas.restore(); } ~~~ 两个参数与scale类似表示横纵向的错切比率,上面代码的效果如下: ![](https://box.kancloud.cn/2016-05-30_574c260dd8490.png) 在之前的章节中我们曾讲过一个类似的用来专门操作变换的玩意Matrix,之前我也说过我们会在很多地方用到这畜生,Canvas也提供了对应的方法来便于我们设置Matrix直接变换Canvas: ~~~ @Override protected void onDraw(Canvas canvas) { canvas.save(Canvas.MATRIX_SAVE_FLAG); Matrix matrix = new Matrix(); matrix.setScale(0.8F, 0.35F); matrix.postTranslate(100, 100); canvas.setMatrix(matrix); canvas.drawBitmap(mBitmap, 0, 0, null); canvas.restore(); } ~~~ 运行效果如下: ![](https://box.kancloud.cn/2016-05-30_574c260e0f495.png) 好了,关于Canvas的保存还原和变换的简单操作就介绍到这吧,剩些的一些draw方法都很好理解简单,难的我前面已经陆续穿插讲了,作为自定义控件的一部分,绘制我们用了六节的篇幅去介绍,内容多主要是Android给我们提供了很完善的接口方法以至于你在上层开发的时候压根不用去管什么源码实现,接下来的章节我们会开始进入另一个重点:控件的测量,不过在此之前我想给大家结合前面学到的一些知识来做一个关于翻页效果的小例子。 该部分源码下载:[传送门](http://download.csdn.net/detail/aigestudio/8357593)