还是先贴出爱哥地址:http://blog.csdn.net/aigestudio/article/details/42677973
如之前所说,canvas方法分为几类,clipXXX算一类,各种drawXXX算一类,还有一类则是对Canvas的各种变换操作,这节我们将具体看看关于Canvas变换操作的一些具体内容。
1、层的概念
如上图所示,位于最底层的是一个圆,第二层是一个蓝色的椭圆,最顶层是两个蓝色的圆,三个层中不同元素最终构成右边的图像这就是图层最直观也是最简单的体现。在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();
}
}
效果图:
2、save对层的影响
当调用save后,所有的操作都是在当前保存的层上操作,当前层的大小就是你要操作的实际大小(可以加入裁剪canvas试试效果),不能画出比当前层更大的区域了,当然想画出更大,就restore吧。
1)如果先旋转画布,再画图:
@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();
}
注意:我们对Canvas(实际上Android中大多数与坐标有关的)进行操作时,默认情况下是以控件的左上角为原点坐标的。效果图:
2)画图,保存,再旋转画布,再画图
@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();
}
效果图:
如果把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可让我们自行设定需要保存的区域,保存后如果没有restore,则保存的区域就是可操作的最大区域。
效果有点类似于clipRect。上面说了saveLayerXXX会将操作保存到一个新的Bitmap中,而这个Bitmap的大小取决于我们传入的参数的大小,Bitmap是个相当危险的对象,很多朋友在操作Bimtap时不太理解其原理经常导致OOM,在saveLayer时我们会依据传入的参数获取一个相同大小的Bitmap,虽然这个Bitmap是空的但是其会占用一定的内存空间,我们希望尽可能小的保存该区域,而saveLayer则提供了这样的功能,顺带提一下,onDraw()传入的Canvas对象的Bitmap在Android没引入HW之前理论上是无限大的,实际上其依然是根据你的图像来不断计算的,而在引入HW之后,该Bitmap受到限制,具体多大大家可以尝试画一个超长的path运行下你就可以在Logcat中看到warning。
saveLayerAlpha可设置画布的透明度。
save()也有个重载方法save(int saveFlags),而saveLayer和saveLayerAlpha你也会发现又一个类似的参数,那么这个参数是干什么的?在Canvas中有六个常量值:
除了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表示当前图层的色彩模式至少需要8位色,HAS_ALPHA_LAYER_SAVE_FLAG表示在当前图层中将需要使用逐像素Alpha混合模式。平时用ALL_SAVE_FLAG即可。
所有的save、saveLayer和saveLayerAlpha都返回一个int型的返回值,该返回值作为一个标志给予了一个你当前保存操作的唯一ID编号,我们可利用restoreToCount(int saveCount)来指定还原时还原哪个保存操作。
save()如果没有配合restore()或者restoreToCount()的话,其实没什么作用。可以自己试试:仅有save()时,去掉save()也能实现同样的效果。
比如有裁剪操作时,restore()和restoreToCount()即可还原到指定的保存画布的大小。
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栈:
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的约束旋转:
除此外,这个矩形除了被旋转,还被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);
效果图:
saveID2已经不再对下面的saveID3起作用,就是说调用canvas.restore()后标识着上一个save操作结束或者说回滚了。
每当我们调用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输出如下:
3、接下来看看Canvas中的变换操作
变换无非就是:平移、旋转、缩放和错切(skew)。Canvas也继承了变换的精髓,同样提供了这几种相应的方法。再次强调: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();
}
}