自定义控件其实很简单1/2

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究!

炮兵镇楼

年关将至事情巨多,最近因为安排蓄谋已已久的旅行事宜很久没更我们的系列教程,约莫着有一个月了,这事情多起来啊闲都闲不下来~~那么我们闲话少说,来看看这一节我们的重点,上一节因为之前从未涉及Canvas的clipXXX方法所以我们优先对其做了一定的介绍并顺带将Path类的方法做了一个小结,如我之前所说Canvas方法可以分为几类,clipXXX算一类,各种drawXXX又是一类,还有一类则是对Canvas的各种变换操作,这一节我们将来具体看看关于Canvas变换操作的一些具体内容,在讲解之前呢我们先来了解一个关于“层”的设计理念,为什么说它是一个设计理念呢?因为在很多很多的地方,当然不止是开发,还有设计等领域你都能见到它的踪影,那么何为图层呢?大家小时候一定都画过画,比如下面这种:

一个松鼠、几棵树、两个鸟、一个日,这几个简单的图画其实就包含了最简单“层”的概念,由图我们可以知道松鼠一定是在树和地面的前面,而树和地面的关系呢则比较模糊,可以是树在前也可以是地面在前,日肯定是在最底层的,两只鸟我们按照一般逻辑可以推测在日的上一层也就是倒数第二层,那么从底层到顶层我们就有这样的一个层次关系:日-鸟-树/地面-地面/树-松鼠,这么一说大家觉得好像也是,但是目测没毛用啊……意义何在,别急,想像一下,这时候如果你不想要松鼠而是想放一只猫在前面……或者你想把松鼠放在树的后面“藏”起来……这时你就蛋疼了,不停地拿橡皮擦擦啊擦草啊草,一不小心还得把其他的擦掉一块,这时候你就会想可以不可以有这么一个功能能让不同的元素通过一定的次序单独地画在一张大小一致“纸”上直到画完最后一个元素后把这些所有“纸”上的元素都整合起来构成一幅完整的图画呢?这样一个功能的存在能大大提高我们绘图的效率还能实现更多的绘图功能,基于这样的一个假想,“层”的概念应运而生:

如上图所示,位于最底层的是一个圆,第二层是一个蓝色的椭圆,最顶层的是两个蓝色的圆,三个层中不同的元素最终构成右边的图像,这就是图层最直观也是最简单的体现。在Android中我们可以使用Canvas的saveXXX和restoreXXX方法来模拟图层的类似效果:

[java] view plaincopyprint?

  1. public class LayerView extends View {
  2. private Paint mPaint;// 画笔对象
  3. private int mViewWidth, mViewHeight;// 控件宽高
  4. public LayerView(Context context, AttributeSet attrs) {
  5. super(context, attrs);
  6. // 实例化画笔对象并设置其标识值
  7. mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
  8. }
  9. @Override
  10. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  11. /*
  12. * 获取控件宽高
  13. */
  14. mViewWidth = w;
  15. mViewHeight = h;
  16. }
  17. @Override
  18. protected void onDraw(Canvas canvas) {
  19. /*
  20. * 绘制一个红色矩形
  21. */
  22. mPaint.setColor(Color.RED);
  23. canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint);
  24. /*
  25. * 保存画布并绘制一个蓝色的矩形
  26. */
  27. canvas.save();
  28. mPaint.setColor(Color.BLUE);
  29. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  30. canvas.restore();
  31. }
  32. }

如代码所示,我们先在onDraw方法中绘制一个红色的大矩形再保存画布绘制了一个蓝色的小矩形:

此时我们尝试旋转一下我们的画布:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. // 旋转画布
  4. canvas.rotate(30);
  5. /*
  6. * 绘制一个红色矩形
  7. */
  8. mPaint.setColor(Color.RED);
  9. canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint);
  10. /*
  11. * 保存画布并绘制一个蓝色的矩形
  12. */
  13. canvas.save();
  14. mPaint.setColor(Color.BLUE);
  15. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  16. canvas.restore();
  17. }

如代码所示顺时针旋转30度,这里要注意,我们在对Canvas(实际上大多数Android中的其他与坐标有关的)进行坐标操作的时候,默认情况下是以控件的左上角为原点坐标的,效果如下:

可以看到两个矩形都一起飞了,可是我们只想让蓝色的飞而红色的不动怎么办呢?很简单,我们只在保存的图层里操作即可:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. /*
  4. * 绘制一个红色矩形
  5. */
  6. mPaint.setColor(Color.RED);
  7. canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint);
  8. /*
  9. * 保存画布并绘制一个蓝色的矩形
  10. */
  11. canvas.save();
  12. mPaint.setColor(Color.BLUE);
  13. // 旋转画布
  14. canvas.rotate(30);
  15. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  16. canvas.restore();
  17. }

可以看到,我们只针对蓝色的矩形进行了旋转:

至此结合上一节对Canvas的一些原理阐述我们该对它有个全新的认识,之前我们一直称其为画布,其实更准确地说Canvas是一个容器,如果把Canvas理解成画板,那么我们的“层”就像张张夹在画板上的透明的纸,而这些纸对应到Android则是一个个封装在Canvas中的Bitmap。

除了save()方法Canvas还给我们提供了一系列的saveLayerXXX方法给我们保存画布,与save()方法不同的是,saveLayerXXX方法会将所有的操作存到一个新的Bitmap中而不影响当前Canvas的Bitmap,而save()方法则是在当前的Bitmap中进行操作,并且只能针对Bitmap的形变和裁剪进行操作,saveLayerXXX方法则无所不能,当然两者还有很多的不同,我们稍作讲解。虽然save和saveLayerXXX方法有着很大的区别但是在一般应用上两者能实现的功能是差不多,上面的代码我们也可以改成这样:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. /*
  4. * 绘制一个红色矩形
  5. */
  6. mPaint.setColor(Color.RED);
  7. canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint);
  8. /*
  9. * 保存画布并绘制一个蓝色的矩形
  10. */
  11. canvas.saveLayer(0, 0, mViewWidth, mViewHeight, null, Canvas.ALL_SAVE_FLAG);
  12. mPaint.setColor(Color.BLUE);
  13. // 旋转画布
  14. canvas.rotate(30);
  15. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  16. canvas.restore();
  17. }

当然实现的效果也是一样的就不多说了。saveLayer可以让我们自行设定需要保存的区域,比如我们可以只保存和蓝色方块一样的区域:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. /*
  4. * 绘制一个红色矩形
  5. */
  6. mPaint.setColor(Color.RED);
  7. canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint);
  8. /*
  9. * 保存画布并绘制一个蓝色的矩形
  10. */
  11. canvas.saveLayer(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, null, Canvas.ALL_SAVE_FLAG);
  12. mPaint.setColor(Color.BLUE);
  13. // 旋转画布
  14. canvas.rotate(30);
  15. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  16. canvas.restore();
  17. }

这时候如果你运行就会发现蓝色的方块已经不见了,因为我们图层的大小就这么点,超出的部分就不能被显示了,这时我们改小画布旋转:

[java] view plaincopyprint?

  1. canvas.rotate(5);

你就可以看到旋转后的蓝色方块的一角:

是不是有点类似于clipRect的效果呢?那么很多朋友会好奇为什么会有这样一种保存一小块画布区域的功能呢?其实原因很简单,上面我们说了saveLayerXXX方法会将操作保存到一个新的Bitmap中,而这个Bitmap的大小取决于我们传入的参数大小,Bitmap是个相当危险的对象,很多朋友在操作Bitmap时不太理解其原理经常导致OOM,在saveLayer时我们会依据传入的参数获取一个相同大小的Bitmap,虽然这个Bitmap是空的但是其会占用一定的内存空间,我们希望尽可能小地保存该保存的区域,而saveLayer则提供了这样的功能,顺带提一下,onDraw方法传入的Canvas对象的Bitmap在Android没引入HW之前理论上是无限大的,实际上其依然是根据你的图像来不断计算的,而在引入HW之后,该Bitmap受到限制,具体多大大家可以尝试画一个超长的path运行下你就可以在Logcat中看到warning。

好了,闲话不扯,接着说,除了saveLayer,Canvas还提供了一个saveLayerAlpha方法,顾名思义,该方法可以在我们保存画布时设置画布的透明度:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. /*
  4. * 绘制一个红色矩形
  5. */
  6. mPaint.setColor(Color.RED);
  7. canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint);
  8. /*
  9. * 保存画布并绘制一个蓝色的矩形
  10. */
  11. canvas.saveLayerAlpha(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, 0x55, Canvas.ALL_SAVE_FLAG);
  12. mPaint.setColor(Color.BLUE);
  13. // 旋转画布
  14. canvas.rotate(5);
  15. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  16. canvas.restore();
  17. }

我们将saveLayer替换成saveLayerAlpha并设置透明值为0x55,运行可得如下效果:

可见蓝色的方块被半透明了。such easy!如果大家留心,会发现save()也有个重载方法save (int saveFlags),而saveLayer和saveLayerAlpha你也会发现又一个类似的参数,那么这个参数是干嘛用的呢?在Canvas中有六个常量值:

这六个常量值分别标识了我们在调用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)方法来指定在还原的时候还原哪一个保存操作:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. /*
  4. * 绘制一个红色矩形
  5. */
  6. mPaint.setColor(Color.RED);
  7. canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint);
  8. /*
  9. * 保存并裁剪画布填充绿色
  10. */
  11. int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG);
  12. canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200);
  13. canvas.drawColor(Color.GREEN);
  14. /*
  15. * 保存画布并旋转后绘制一个蓝色的矩形
  16. */
  17. int saveID2 = canvas.save(Canvas.MATRIX_SAVE_FLAG);
  18. // 旋转画布
  19. canvas.rotate(5);
  20. mPaint.setColor(Color.BLUE);
  21. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  22. canvas.restoreToCount(saveID1);
  23. }

如上代码所示,我们第一次保存画布并获取其返回值:

[java] view plaincopyprint?

  1. int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG);

然后对画布进行裁剪并填色,第二次保存画布并获取其返回值:

[java] view plaincopyprint?

  1. int saveID2 = canvas.save(Canvas.MATRIX_SAVE_FLAG);

然后绘制一个蓝色的矩形,最后我们只还原了了saveID1的画布状态,运行一下你会发现好像效果没什么不同啊:

然后我们试试

[java] view plaincopyprint?

  1. canvas.restoreToCount(saveID2);

发现效果还是一样…………很多童鞋就困惑了,是哪不对么?没有,其实都是对的,你觉得奇怪是你还不理解save和restore,这里我在restore之后再绘制一个矩形:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. /*
  4. * 绘制一个红色矩形
  5. */
  6. mPaint.setColor(Color.RED);
  7. canvas.drawRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint);
  8. /*
  9. * 保存并裁剪画布填充绿色
  10. */
  11. int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG);
  12. canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200);
  13. canvas.drawColor(Color.GREEN);
  14. /*
  15. * 保存画布并旋转后绘制一个蓝色的矩形
  16. */
  17. int saveID2 = canvas.save(Canvas.MATRIX_SAVE_FLAG);
  18. // 旋转画布
  19. canvas.rotate(5);
  20. mPaint.setColor(Color.BLUE);
  21. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  22. canvas.restoreToCount(saveID2);
  23. mPaint.setColor(Color.YELLOW);
  24. canvas.drawRect(mViewWidth / 2F - 400, mViewHeight / 2F - 400, mViewWidth / 2F + 400, mViewHeight / 2F + 400, mPaint);
  25. }

可以看到我在

[java] view plaincopyprint?

  1. canvas.restoreToCount(saveID2);

之后又绘制了一个黄色的矩形:

可是不管你如何调大这个矩形,你会发现它就那么大点……也就是说,这个黄色的矩形其实是被clip掉了,进一步说,我们绘制黄色矩形的这个操作其实说白了就是在saveID1的状态下进行的。前面我们曾说过save和saveLayerXXX方法有着本质的区别,saveLayerXXX方法会将所有操作在一个新的Bitmap中进行,而save则是依靠stack栈来进行,假设我们有如下代码:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. /*
  4. * 保存并裁剪画布填充绿色
  5. */
  6. int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG);
  7. canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300);
  8. canvas.drawColor(Color.YELLOW);
  9. /*
  10. * 保存并裁剪画布填充绿色
  11. */
  12. int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG);
  13. canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200);
  14. canvas.drawColor(Color.GREEN);
  15. /*
  16. * 保存画布并旋转后绘制一个蓝色的矩形
  17. */
  18. int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG);
  19. canvas.rotate(5);
  20. mPaint.setColor(Color.BLUE);
  21. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  22. }

此时,在Canvas内部会有这样的一个Stack栈:

Canvas会默认保存一个底层的空间给我们绘制一些东西,当我们没有调用save方法时所有的绘图操作都在这个Default Stack ID中进行,每当我们调用一次save就会往Stack中存入一个ID,将其后所有的操作都在这个ID所指向的空间进行直到我们调用restore方法还原操作,上面代码我们save了三次且没有restore,stack的结构就如上图所示,此时如果我们继续绘制东西,比如:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. /*
  4. * 保存并裁剪画布填充绿色
  5. */
  6. int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG);
  7. canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300);
  8. canvas.drawColor(Color.YELLOW);
  9. /*
  10. * 保存并裁剪画布填充绿色
  11. */
  12. int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG);
  13. canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200);
  14. canvas.drawColor(Color.GREEN);
  15. /*
  16. * 保存画布并旋转后绘制一个蓝色的矩形
  17. */
  18. int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG);
  19. // 旋转画布
  20. canvas.rotate(5);
  21. mPaint.setColor(Color.BLUE);
  22. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  23. mPaint.setColor(Color.CYAN);
  24. canvas.drawRect(mViewWidth / 2F, mViewHeight / 2F, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint);
  25. }

我们在saveID3之后又画了一个青色的矩形,只要你不是傻子明眼都能看出这段代码是在saveID3所标识的空间中绘制的,因此其必然会受到saveID3的约束旋转:

除此之外,大家还可以很明显的看到,这个矩形除了被旋转,还被clip了~也就是说saveID1、saveID2也同时对其产生了影响,此时我们再次尝试在saveID2绘制完我们想要的东西后将其还原:

[java] view plaincopyprint?

  1. /*
  2. * 保存并裁剪画布填充绿色
  3. */
  4. int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG);
  5. canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200);
  6. canvas.drawColor(Color.GREEN);
  7. canvas.restore();

同时将青色的矩形变大一点:

[java] view plaincopyprint?

  1. canvas.drawRect(mViewWidth / 2F, mViewHeight / 2F, mViewWidth / 2F + 400, mViewHeight / 2F + 400, mPaint);

这时我们得到什么样的效果呢:

其实猜都猜得到,saveID2已经不再对下面的saveID3起作用了,也就是说当我们调用canvas.restore()后标志着上一个save操作的结束或者说回滚了。同理,我们再把saveID1也restore:

[java] view plaincopyprint?

  1. /*
  2. * 保存并裁剪画布填充绿色
  3. */
  4. int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG);
  5. canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300);
  6. canvas.drawColor(Color.YELLOW);
  7. canvas.restore();

这时saveID3将彻底不再受前面操作的影响:

如果我们在绘制青色的矩形之前将saveID3也还原:

[java] view plaincopyprint?

  1. /*
  2. * 保存画布并旋转后绘制一个蓝色的矩形
  3. */
  4. int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG);
  5. canvas.rotate(5);
  6. mPaint.setColor(Color.BLUE);
  7. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  8. canvas.restore();

那么这个青色的矩形将会被绘制在Default Stack ID上而不受其他save状态的影响:

上面我们提到的restoreToCount(int saveCount)方法接受一个标识值,我们可以根据这个标识值来还原特定的栈空间,效果类似就不多说了。每当我们调用restore还原Canvas,对应的save栈空间就会从Stack中弹出去,Canvas提供了getSaveCount()方法来为我们提供查询当前栈中有多少save的空间:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. System.out.println(canvas.getSaveCount());
  4. /*
  5. * 保存并裁剪画布填充绿色
  6. */
  7. int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG);
  8. System.out.println(canvas.getSaveCount());
  9. canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300);
  10. canvas.drawColor(Color.YELLOW);
  11. canvas.restore();
  12. /*
  13. * 保存并裁剪画布填充绿色
  14. */
  15. int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG);
  16. System.out.println(canvas.getSaveCount());
  17. canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200);
  18. canvas.drawColor(Color.GREEN);
  19. canvas.restore();
  20. /*
  21. * 保存画布并旋转后绘制一个蓝色的矩形
  22. */
  23. int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG);
  24. System.out.println(canvas.getSaveCount());
  25. // 旋转画布
  26. canvas.rotate(5);
  27. mPaint.setColor(Color.BLUE);
  28. canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);
  29. canvas.restore();
  30. System.out.println(canvas.getSaveCount());
  31. mPaint.setColor(Color.CYAN);
  32. canvas.drawRect(mViewWidth / 2F, mViewHeight / 2F, mViewWidth / 2F + 400, mViewHeight / 2F + 400, mPaint);
  33. }

运行后你会看到Logcat的如下输出:

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之间为缩小:

[java] view plaincopyprint?

  1. public class LayerView extends View {
  2. private Bitmap mBitmap;// 位图对象
  3. private int mViewWidth, mViewHeight;// 控件宽高
  4. public LayerView(Context context, AttributeSet attrs) {
  5. super(context, attrs);
  6. // 从资源中获取位图对象
  7. mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.z);
  8. }
  9. @Override
  10. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  11. /*
  12. * 获取控件宽高
  13. */
  14. mViewWidth = w;
  15. mViewHeight = h;
  16. // 缩放位图与控件一致
  17. mBitmap = Bitmap.createScaledBitmap(mBitmap, mViewWidth, mViewHeight, true);
  18. }
  19. @Override
  20. protected void onDraw(Canvas canvas) {
  21. canvas.save(Canvas.MATRIX_SAVE_FLAG);
  22. canvas.scale(1.0F, 1.0F);
  23. canvas.drawBitmap(mBitmap, 0, 0, null);
  24. canvas.restore();
  25. }
  26. }

当缩放比率为1时表示不缩放:

我们改变下缩放比率:

[java] view plaincopyprint?

  1. canvas.scale(0.8F, 0.35F);

此时画面效果如下:

可以看到缩放中心在左上角,我们可以使用scale的重载方法更改缩放中心:

[java] view plaincopyprint?

  1. canvas.scale(0.8F, 0.35F, mViewWidth, 0);

效果如下,很好理解:

rotate(float degrees)和重载方法rotate(float degrees, float px, float py)类似前面也用过不少就不多说了,没接触过的只有skew(float sx, float sy)错切方法,关于错切的概念前面我们都有讲过很多,其实知道原理,方法再怎么变都不难:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. canvas.save(Canvas.MATRIX_SAVE_FLAG);
  4. canvas.skew(0.5F, 0F);
  5. canvas.drawBitmap(mBitmap, 0, 0, null);
  6. canvas.restore();
  7. }

两个参数与scale类似表示横纵向的错切比率,上面代码的效果如下:

在之前的章节中我们曾讲过一个类似的用来专门操作变换的玩意Matrix,之前我也说过我们会在很多地方用到这畜生,Canvas也提供了对应的方法来便于我们设置Matrix直接变换Canvas:

[java] view plaincopyprint?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. canvas.save(Canvas.MATRIX_SAVE_FLAG);
  4. Matrix matrix = new Matrix();
  5. matrix.setScale(0.8F, 0.35F);
  6. matrix.postTranslate(100, 100);
  7. canvas.setMatrix(matrix);
  8. canvas.drawBitmap(mBitmap, 0, 0, null);
  9. canvas.restore();
  10. }

运行效果如下:

好了,关于Canvas的保存还原和变换的简单操作就介绍到这吧,剩些的一些draw方法都很好理解简单,难的我前面已经陆续穿插讲了,作为自定义控件的一部分,绘制我们用了六节的篇幅去介绍,内容多主要是Android给我们提供了很完善的接口方法以至于你在上层开发的时候压根不用去管什么源码实现,接下来的章节我们会开始进入另一个重点:控件的测量,不过在此之前我想给大家结合前面学到的一些知识来做一个关于翻页效果的小例子。

该部分源码下载:传送门

时间: 2024-11-04 22:01:02

自定义控件其实很简单1/2的相关文章

自定义控件其实很简单2/3

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究! 炮兵镇楼 又要开始鸡冻人心的一刻了有木有!有木有鸡冻! = = --通过上一节的讲解呢我们大致对Android测量控件有个初步的了解,而此后呢也有不少盆友Q小窗我问了不少问题,不过其实这些问题大多都不是问题,至于到底是不是问题呢,还要等我研究下究竟可不可以把这些问题归为问题--稍等.我吃个药先.大多数盆友的反应是在对控件测量的具体方法还不是很

自定义控件其实很简单1/4

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究! 炮兵镇楼 上一回关羽操刀怒砍秦桧子龙拼命相救,岂料刘备这狗贼耍赖以张飞为祭品特殊召唤黑暗大法师消灭了场上所有逗逼,霎时间血流成河,鲜红的血液与冰冷的大地融合交汇在一起焕发出血液的煞气……那么,问题来了,请问这是使用了哪种PorterDuffXfermode? 在上一节的最后一个Example中我们做了一个橡皮擦的View,但是这个View虽然

自定义控件其实很简单3/4

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究! 炮兵镇楼 隐约雷鸣 阴霾天空 但盼风雨来 能留你在此 隐约雷鸣 阴霾天空 即使天无雨 我亦留此地 上一节我们细致地.猥琐地.小心翼翼地.犹如丝滑般抚摸.啊不,是讲解了如何去测量一个布局控件,再次强调,如我之前多次强调那样,控件的测量必须要逻辑缜密严谨,尽量少地避免出现较大的逻辑错误.在整个系列撰写的过程中,有N^N个朋友曾多次不间断地小窗我问

自定义控件其实很简单1/12

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究! 炮兵镇楼 自定义View,很多初学Android的童鞋听到这么一句话绝逼是一脸膜拜!因为在很多初学者眼里,能够自己去画一个View绝逼是一件很屌很Cool的事!但是,同样而言,自定义View对初学者来说却往往可望而不可及,可望是因为看了很多自定义View的源码好像并不难,有些自定义View甚至不足百行代码,不可及呢是因为即便看了很多文章很多类

自定义控件其实很简单1/6

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究! 炮兵镇楼 上一节我们粗略地讲了下如何去实现我们的View并概述了View形成动画的基本原理,这一节我们紧跟上一节的步伐来深挖如何去绘制更复杂的View! 通过上一节的学习我们了解到什么是画布Canvas什么是画笔Paint,并且学习了如何去设置画笔的属性如何在画布上画一个圆,然而,画笔的属性并非仅仅就设置个颜色.大小那么简单而画布呢肯定也不单

自定义控件其实很简单7/12

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究! 炮兵镇楼 要在数量上统计中国菜的品种,在地域上毫无争议地划分菜系,在今天,是一件几乎不可能完成的事--Cut----抱歉--忘吃药了,再来一遍.如果非要对自定义控件的流程进行一个简单的划分,我会尝试将其分为三大部分:控件的绘制.控件的测量和控件的交互行为.前面我们用了六节的篇幅和一个翻页的例子来对控件的绘制有了一个全新的认识但是我们所做出的所

自定义控件其实很简单5/12

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究! 炮兵镇楼 最近龙体欠安,很多任务都堆着,虽说如此,依然没有停下学习的步伐,虽然偶尔还会有点头痛,但是孤依旧在学习……自赞一个~ 在1/3中我们结束了全部的Paint方法学习还略带地说了下Matri的简单用法,这两节呢,我们将甩掉第二个陌生又熟悉的情妇:Canvas.Canvas从我们该系列教程的第一节起就嘚啵嘚啵个没完没了,几乎每个View都

自定义控件其实很简单1/3

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究! 炮兵镇楼 前几天身子骨出了点小毛病不爽,再加上CSDN抽风也木有更新,现在补上之前漏掉的1/3 上一节结尾的时候我们说到,Paint类中我们还有一个方法没讲 [java] view plaincopyprint? setShader(Shader shader) 这个方法呢其实也没有什么特别的,那么为什么我们要把它单独分离出来讲那么异类呢?难

王金战:改变一个差生真的很简单

当一个学生,反复遭遇失败的打击,他就变成了差生,没有一个学生生下来就注定是个差生.所以让一个差生变好真的很简单.    我刚参加工作的时候,不敢说这句话,但是我现在敢说,因为我多年的经历已经证明了这件事情.让一个差生变好真的很简单,怎么做呢?就是反其道而行之.差生是反复遭遇失败的打击后才产生的,让一个差生变好,就是让他反复享受到成功的喜悦,这个学生就会慢慢地变好了.    少年来我就用这样的方法和理念,帮着一个个学生走出困境,走向成功.我深深体会到,作为教师,作为家长,要学会欣赏孩子.特别是对那