赞
踩
Android的绘图机制是核心内容之一,无论是什么样的功能最终都是以图像的形式呈现给用户。因此掌握Android的绘图技巧,有助于Android理解层次的提高,在面对产品经理提出的idea时也更有底气~
系统通过提供的Canvas对象来绘图,此类拥有各种绘制图像的API,例如 drawRect(矩形)、drawCircle(圆)、drawLine(线)、drawArc(弧)、drawPoint(点)、drawVertices(多边形)等等,通过这些API名字也可了解大致作用。但是,Canvas背后的宝藏在更深处的地方,各种基础、绚丽的效果都与之脱不了干系,两大法宝就是Canvas的裁剪合集,二维、三维Camera几何变换。
更重要的是要理解Android系统中绘制Canvas画布的概念,画布上层层叠加的纸Bitmap,图层叠加组合而成的UI呈现,这些概念运用不同于生活中的认知,需谨慎鉴别。
(此系列文章知识点相对独立,可分开阅读,不过笔者建议按照顺序阅读,理解更加深入清晰)
Android 高级UI解密 (二) :Paint滤镜 与 颜色过滤(矩阵变换)
Android 高级UI解密 (一) :Paint图形文字绘制 与 高级渲染
此篇涉及到的知识点如下:
上图展示的是最基本的Canvas绘制图形API,简单理解即可知其作用含义,此处不再举例讲解,后续将挑出重点部分学习。
Canvas中有关裁剪的API可分为三类: clipPath
,clipRect
,clipRegion
,以上几乎可以实现任意形状的裁剪。通过三类API的名字可以想到其中的区别:
注意
在学习以下API使用之前,应当明确裁剪的概念,生活中认为裁剪是对存在的图形进行切割,但在Android系统中并非如此,裁剪的主要对象是画布canvas而非图形!裁剪后的画布区域,在此上面绘制图像才会显示出来,并且每次调用canvas.drawXXX都会创建一个新的视图层。
绘制图像是需要基于画布canvas的基础上,因此需要注意调用API顺序:先裁剪画布区域,再在画布上面绘制图像!(若顺序弄反,调用draw的图像已经绘制出来,后续再调用clip,并没有对之前的图像裁剪生效,只对下一次draw生效)
(1)clipRect
代码示例:
canvas.clipRect(350+width()/4,20+height()/4,350+width*3/4,20+height()*3/4);
canvas.drawBitmap(bitmap, 350, 20, mPaint);
图片效果示例:
注意
在裁剪指定区域绘制完毕后,注意是否需要恢复绘制范围,否则后续的所有绘制都会被裁切!使用方法如下:
//保存视图层
canvas.save();
canvas.clipRect(350+width()/4,20+height()/4,350+width*3/4,20+height()*3/4);
canvas.drawBitmap(bitmap, 350, 20, mPaint);
//恢复原有试图层
canvas.restore();
(2)clipPath
代码示例:
Path path=new Path();
path.addCircle(350+width/2, 20+height/2,width/2,Path.Direction.CCW);
canvas.save();
canvas.clipPath(path);
canvas.drawBitmap(bitmap, 350, 20, paint);
canvas.restore();
图片效果示例:
(3)clipRegion
前两个方法可能很常见,但是Region区域的使用比较少见。方便理解从微积分的角度分析,任意绘制的一块区域都是由一块块小区域拼接的。例如一张图片放大很多倍后,你会发现它也是一块块很小的像素块组成的。
Region.Op是个枚举类,定义了Region支持的区域间运算种类,各个种类如下:
这一部分“Canvas的变换”都在强调Canvas画布这个概念,以上API也都是针对画布进行变换,因为很容易将Canvas绘制的画布区域和屏幕区域混淆,但两者是有差别的。这里需要引入“视图层Layer”的概念,每次canvas执行drawXXX
的时候就会新建一个新的画布图层。
在对画布canvas进行裁剪或几何变换时需要知道这是一个不可逆的过程,除非搭配使用
save()
、restore()
使用保存恢复图层状态。
阅读自此,你可能对“图层”的概念还是有些模糊,举个例子,如上图是笔者在SketchBookPro绘画工具上绘制的一张图片(真是煞费苦心啊,笔者献出了自己拙劣的绘画功底,见谅~)比较正规点的绘画工具它都会都有“图层”的概念,便于修改,例如上图中图层1的内容就是绿色的大树和底下一条波浪线,也就是草原啦;图层二就是绿树上的一颗红苹果;图层三就是树下的小人儿,牛顿是也。
咋一看,这就是一幅平面2D图画,但你要从“图层”的角度来看,这可是由3层图层依次叠加而形成的,例如下图所示。
“图层”在真正的实践中最大作用就是便于修改,例如PM过来一看俗,都是苹果砸牛顿,我要换成梨子,在Android的绘制过程中可没有什么“橡皮擦”给你修改,就算此处用橡皮擦修改,一旦遇到重叠的部分,那就全擦去了。如今引入了“图层”的概念,苹果位于图层2,可以很轻易的删去图层2,新建图层绘制梨子即可。
对于Android绘制而言,“图层”概念的引用,使得各个图层之间互相独立,可以轻易对单个指定图层进行修改、删去,绘制修改起来更加简单。
Canvas为开发者提供了图层(Layer)的支持,而这些Layer(图层)是按”栈结构“来进行管理的,如下图:
(1)save() 和 restore()
int save()
:保存当前的matrix 矩阵和剪切状态到一个私人堆栈,也就是说保存当前Canvas的状态然后作为一个Layer(图层)添加到Canvas栈中,即入栈操作。void restore()
:恢复之前Canvas的状态,即出栈操作。需要注意的是Layer 并不是一个实际的类,连同“视图层”只是一个抽象的相对概念。由于是栈结构,调用restore()
的次数不应大于调用save()
的次数。
实例测试
如上图所示,先绘制一个红色矩形,再绘制一个蓝色矩形。现在的需求是要将红色矩形旋转。在绘制红色矩形之间调用canvas.rotate(10)
?
emmm,说对了一半,如果只是简单的调用,你会发现连带着蓝色矩形也旋转了。此处需要用到“图层”的概念,在旋转之前先调用save
保存图层,在旋转且绘制蓝色矩形过后,调用restore
恢复图层,再去绘制蓝色矩形即可。
以下是正确代码和示例图:
canvas.save();
// 保存并旋转画布
canvas.rotate(10);
//绘制一个红色矩形
mPaint.setColor(getResources().getColor(R.color.morePink));
canvas.drawRect(mViewWidth / 2F - 200, 200, mViewWidth / 2F + 200, 600, mPaint);
canvas.restore();
//绘制一个蓝色的矩形
mPaint.setColor(getResources().getColor(R.color.moreBlue));
canvas.drawRect(mViewWidth / 2F - 100, 300, mViewWidth / 2F + 100, 500, mPaint);
在了解以上内容后再来总结一下Canvas和Bitmap的关系:
在此之前我们称呼Canvas为画布,更准确的说法应该是是一个容器。如果把Canvas理解成画板,那所谓的“图层”就像是画板上的一张张透明“纸”,而这些“纸”对应到Android就是封装在Canvas中的Bitmap。
(2)saveLayerXXX
int saveLayer(float left, float top, float right, float bottom, Paint paint, int saveFlags)
查看此API,可发现它与save
方法相比多了好些参数,前四个则是位置参数,接着Paint画笔参数,和标识符参数(后续讲)。作用则是指定保存的模式和保存到区域。
实例测试
上述的例子完全可以将save()
方法替换成如下代码,效果相同。
canvas.saveLayer(0, 0, mViewWidth, mViewHeight, null, Canvas.ALL_SAVE_FLAG);
saveLayer
的特点就是可以自行设定需要保存的区域,如上代码是将区域设置成全屏幕,再举个例子,将保存的区域分别设置为红色矩形、蓝色矩形,最终显示效果会是如何呢?代码如下:
//测试实例1(只需要修改save方法)
canvas.saveLayer(mViewWidth / 2F - 200, 200, mViewWidth / 2F + 200,600, null, Canvas.ALL_SAVE_FLAG);
//测试实例2(只需要修改save方法)
canvas.saveLayer(mViewWidth / 2F - 100, 300, mViewWidth / 2F + 100, 500, null, Canvas.ALL_SAVE_FLAG);
哟呵,吓一跳!我只是修改了一下保存区域,这两种测试实例都不是理想的效果,一个个来解释:(注意黑框是指定的保存区域,笔者为了便于理解后期加上的)
由此可见,saveLayerXXX
和save
方法的区别在于:
saveLayerXXX
方法会将所有的操作存到一个新的Bitmap中而不影响当前Canvas的Bitmap;save
方法则是在当前的Bitmap中进行操作,并且只能针对Bitmap的形变和裁剪进行操作;也许你会疑惑saveLayerXXX
为啥会有指定保存范围这个设定呢?
上述区别已解释saveLayerXXX
会将操作存在一个新的Bitmap中,而对于Android而言,使用Bitmap稍有不当就会内存溢出,甚至OOM。因此根据参数尽可能创建一个小的Bitmap。
int saveLayer(float left, float top, float right, float bottom, int alpha, int saveFlags)
除以上讲解的saveLayer
方法之外,还提供有saveLayerAlpha
,顾名思义就是保存画布时设置画布的透明度,不再举例实践。
(3)标识符saveFlags
在saveLayer
和saveLayerAlpha
方法中都见识过saveFlags参数,不仅如此,save
重载的参数方法中也有这个参数,如下表查看其详细含义:
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
方法,,意义分别为:对当前图层执行裁剪操作需要对齐图层边界,当前图层的色彩模式至少需要是8位色,在当前图层中将需要使用逐像素Alpha混合模式。(此处只做简单介绍,并不常用,与本篇主题差距稍大,有兴趣者可查阅以下资料自行了解)
(4)restoreToCount(int saveCount)
恢复图层有两个方法,restore
在一开始就讲解过,相当于一次出栈操作,而调用此方法则可以恢复到指定的图层。这是如何做到的呢?
所有的save
、saveLayer
和saveLayerAlpha
方法都有一个int型的返回值,该返回值相当于一个标识,确定当前保存操作的唯一ID编号。因此可以利用restoreToCount(int saveCount)
方法来指定在还原的时候还原到指定图层修改操作。
如下图所示每一个Canvas都有这样的一个Stack栈。每次调用save
方法会保存一个图层ID入栈,若没有人为调用save
方法,则所有的操作会默认保存到Default Stack ID中。因此若此时已创建3个图层,即位于saveID3了,若想要回到saveID1的状态,则需要调用两次restore
方法,不过只要在调用save
方法时获得返回值ID,则可以直接调用restoreToCount(int saveCount)
回溯到理想状态!
实例测试
实践一个简单的例子来体会体会,首先看上图左侧,很明显笔者建立了三个图层,依次是红色、蓝色、橙色矩形,此处从图层二也就是蓝色矩形绘制之前调用了Canvas的rotate
方法,因此后续绘制图层三的橙色矩形也受约制一起旋转了。此时要求绘制的图形是上图右侧,给出红色矩形中间位置绘制天蓝色矩形。
如果直接在图层三上根据给出的坐标绘制天蓝色矩形,那么此部分肯定会受到图层二旋转的约制,因此在绘制之前必须要回滚到图层1,此时你要两个选择:多次调用restore()
方法和直接使用restoreToCount(int saveCount)
回溯到指定图层。毫无疑问选择后者,代码如下:
//绘制一个红色矩形
int saveID1 = canvas.save(Canvas.ALL_SAVE_FLAG);
mPaint.setColor(getResources().getColor(R.color.morePink));
canvas.drawRect(mViewWidth / 2F - 200, 200, mViewWidth / 2F + 200, 600, mPaint);
//绘制一个蓝色的矩形
int saveID2 = canvas.save(Canvas.ALL_SAVE_FLAG);
// 旋转画布
canvas.rotate(10);
mPaint.setColor(getResources().getColor(R.color.moreBlue));
canvas.drawRect(mViewWidth / 2F - 100, 300, mViewWidth / 2F + 100, 500, mPaint);
int saveID3 = canvas.save(Canvas.ALL_SAVE_FLAG);
//绘制一个淡黄色矩形
mPaint.setColor(getResources().getColor(R.color.littlePink));
canvas.drawRect(mViewWidth / 2F - 50, 350, mViewWidth / 2F + 50, 450, mPaint);
canvas.restoreToCount(saveID1);
// canvas.restore();canvas.restore();
mPaint.setColor(getResources().getColor(R.color.littleBlue));
canvas.drawRect(mViewWidth / 2F - 170, 370, mViewWidth / 2F + 170, 430, mPaint);
translate
、rotate
、scale
、skew
变换方法;Translate
、Rotate
、Scale
、Skew
变换方法及自定义;rotate
、setLocation
三维变换方法;注意
在学习Canvas的裁剪API时,因为每次调用drawXXX
方法都会创建一个新的视图层,故而使用API的顺序是先裁剪画布再调用drawXXX
方法绘制图形。此处几何变换的4个API也是相对于画布Canvas作变化,但是想要对图形进行有序性的连续变换时,例如先移动再旋转,一定要先调用旋转API再调用移动API,因为这是反序性的!同时在实际操作中也需要搭配save()
、restore()
方法使用。
Canvas的几何变化内部原理使用的是Matrix类,Matrix矩阵是几何变换背后的代数原理。
(1)平移Tanslate
void translate (float dx, float dy)
API作用:用指定的转换对当前matrix 进行预处理。
参数说明: x、y轴移动的距离。
注意:此API的功能就是移动画布位置,再再次强调的是每次canvas执行drawXXX的时候就会新建一个新的画布图层。
实例测试
来证明以上理论,做一个简单的测试如下,首先在(100, 100)的位置用蓝色画笔绘制一个矩形,接着调用此API方法移动画布x、y轴各50像素,用红色画笔绘制一个矩形。代码如下,查看显示效果:
mPaint.setColor(getResources().getColor(R.color.moreBlue));
RectF r = new RectF(100, 100, 250, 250);
canvas.drawRect(r, mPaint);
mPaint.setColor(getResources().getColor(R.color.morePink));
canvas.translate(50,50);
canvas.drawRect(r, mPaint);
如图很显然,在只移动画布位置的情况下,矩阵的位置也随之改变,说明第二个红色矩形是在新的画布图层上绘制的。另外需要强调的是移动画布是一个不可逆的过程,除非使用save()
和restore()
来搭配使用,代码示例如下:
//保存此刻画布设置状态
canvas.save();
canvas.translate(50,50);
canvas.drawRect(r, mPaint);
//恢复画布设置状态
canvas.restore();
(2)缩放Scale
void scale (float sx, float sy)
API作用:用指定的比例预先缩放当前矩阵matrix 。
参数说明: x、y轴缩放的比例。
注意:设置的参数是对x、y轴的缩放系数,即“画布”会被缩放,同时意味着画布里面所有的绘制的东西都会被缩放。
实例测试
查看以下示例,设置的图片位置是举例top、left各为50像素,第一张是原图展示;第二张调用此API,将x、y轴各压缩0.5,这里意味着这个“画布”缩小,因此不仅是画布里的内容缩小,其间隔距离也会被缩小;第三张图同理。
canvas.drawBitmap(bitmap, 50, 50, mPaint);
canvas.save();
canvas.scale(1.5f, 0.5f);
canvas.drawBitmap(bitmap, 50, 50, mPaint);
canvas.restore();
canvas.save();
canvas.scale(1.5f, 1.5f);
canvas.drawBitmap(bitmap, 600, 50, mPaint);
canvas.restore();
(3)旋转Rotate
rotate(float degrees)
rotate(float degrees, float px, float py)
API作用:用指定的旋转预先缩放当前矩阵。围绕坐标原点旋转degrees度,值为正顺时针。
参数说明: degrees为旋转角度,px和py为指定旋转的中心点坐标(px,py)。
注意:此处需要再三强调的是旋转是指整个“画布”进行旋转,因此其绘制内容具体坐标位置是根据旋转后的“画布”而定。
实例测试
//旋转Rotate
canvas.drawBitmap(bitmap, 200, 20, mPaint);
canvas.save();
canvas.rotate(30);
canvas.drawBitmap(bitmap, 200, 20, mPaint);
canvas.restore();
(4)斜拉画布Skew
skew(float sx, float sy)
API作用:使用指定的偏斜预处理当前矩阵。
参数说明: sx为x轴方向上倾斜的对应角度,sy为y轴方向上倾斜的对应角度。
注意:参数的类型虽然为float ,两个值都是tan值!比如要在x轴方向上倾斜60度,那么小数值对应:tan 60 = 根号3 = 1.732,在设置时应填写1.732。
实例测试
//斜拉画布Skew
canvas.drawBitmap(bitmap, 200, 20, mPaint);
canvas.save();
canvas.skew(1.73f, 0);//X轴方向上倾斜60度,tan60=根号3
canvas.drawBitmap(bitmap, 200, 20, mPaint);
canvas.restore();
其实在上一篇博客 Android 高级UI解密 (二) :滤镜 与 颜色过滤(矩阵变换)中讲解过颜色矩阵ColorMatrix,其中说到
“美颜相机中的图片美白原理就是将红色、绿色、蓝色进行位移,可以获得不同的效果,而其中的计算则可以借助矩阵完成。”
再结合上一点中介绍Canvas的几何变换内部原理就是Matrix矩阵,可知Matrix的强大性,重要的是除了Canvas的4个API几何变换方法,我们可以直接使用Matrix的方法代替之。
(1)Matrix优势
preXXX
、postXXX
方法,正序反序随你心意~(2)使用Matrix的步骤
Translate
、Rotate
、Scale
、Skew
变换方法;concat(matrix)
方法运用Matrix。Matrix matrix = new Matrix();
......
matrix.reset();
matrix.postTranslate();
matrix.postRotate();
canvas.save();
canvas.concat(matrix);
//canvas.setMatrix(matrix)
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();
在Canvas中使用Matrix矩阵有如下两个API方法:
Canvas.concat(matrix)
:用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换;Canvas.setMatrix(matrix)
:用 Matrix 直接替换 Canvas 当前的变换矩阵,即抛弃 Canvas 当前的变换,改用 Matrix 的变换。(注意:此方法在不同的手机系统中显示效果不一致,建议使用concat
方法)Caves虽然是个二维图形变换工具,但是也可以通过几何变换去实现三维效果。值得庆幸的是官方已为开发者提供了Camera类,其内部会把这些变换转化成Matrix,用二维来模拟三维。
void applyToCanvas(Canvas canvas)
调用Camera的applyToCanvas
方法即可运用到Canvas上。Camera 的三维变换有三类:旋转、平移、移动相机。
(1)旋转Rotate
API作用:在指定的轴上旋转角度;
参数说明: X、Y、Z轴旋转的量;
实例测试
//Camera
Camera camera = new Camera();
canvas.save();
camera.rotateX(30); // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
canvas.drawBitmap(bitmap, 200, 20, mPaint);
canvas.restore();
如上展示效果指定X轴旋转30度,图片倒是有点三维feel了,可是发现图片显然不对称啊。因为Camera 的旋转是以左上角原点为轴心进行X轴旋转,按照正常的逻辑应当以图形中点为轴心进行旋转,可是Camera并没有提供设置轴心的方法,因此我们只能手动移动画布Canvas。具体做法就是:
translat
移动画布将原点移动到需要绘制的bitmap中点;translat
移动画布将需要绘制的bitmap中点移动到原点;drawBitmap
绘制bitmap。emmmm,其中这个移动来移动去的感觉有点坑爹,参考代码如下,之后的效果图片就对称了:
//Camera
Camera camera = new Camera();
canvas.save();
canvas.translate(bitmap.getWidth()/2+200,bitmap.getHeight()/2+20);
camera.save();
camera.rotateX(30); // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
camera.restore();
canvas.translate(-bitmap.getWidth()/2-200,-bitmap.getHeight()/2-20);
canvas.drawBitmap(bitmap, 200, 20, mPaint);
canvas.restore();
(2)移动相机setLocation
void setLocation(float x, float y, float z)
API作用:修改相机位置;
参数说明: X、Y、Z轴旋转的角度;
此处参数单位不是像素,而是 inch英寸。这种设计源自 Android 底层的图像引擎 Skia 。在 Skia 中,Camera 的位置单位是英寸,英寸和像素的换算单位在 Skia 中被写死为了 72 像素,而 Android 中把这个换算单位照搬了过来
查看这个API的作用可能会有点懵,waht?相机?确实如此,Camera不同于Canvas二维坐标系,本身代表的是三维,即XYZ轴。在此之上,工作模型还包括了一个虚拟的Camera对象,它的默认位置在Z轴负方向,也就是坐标原点的正前方的位置。
其工作原理就是以Camera为出发点,把三维模型往View上做一个投影,投影就是实际显示的图像。在默认情况下,投影与图像是一样的,但在使用API修改基本参数后,投影就改变了。
上面三张图即可明白Camera投影的工作原理,Camera 的坐标系如下:
以下GIF动图演示的是使用Camera完成旋转的过程,其中包括使用Canvas的translate
方法将图片中点移动到轴心,再旋转X轴,旋转完后再移动回来,再绘制的整个过程,配合此GIF更易了解。
wait, wait, wait,笔者刚才检查的时候发现此节都在啵得啵得Camera的概念,还没细讲这个API呢,其实了解此概念之后,即可了解这个照射光速的发起点,也就是Camera,可以通过此API设置它的位置。
一般可以运用在例如旋转时导致图片呈现过大,可以设置Z轴位置,增大其值相当于挪远照相机,图像成像自然就会变小。在此不再举例,详细可查看扔物线一文。
(3)平移Translate
void translate(float x, float y, float z)
API作用:在指定的轴上移动角度;
参数说明: X、Y、Z轴旋转的角度;
至于此API,在了解第二点后可以很容易的理解使用此API会对XYZ移动,例如增大Z轴,相当于将照相机挪远,图像自然变小。例如如下例子演示:
//原图
canvas.drawBitmap(bitmap, 0, 0, mPaint);
Camera camera = new Camera();
canvas.save();
camera.save();
camera.translate(0,0,300); // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
camera.restore();
//设置Camera之后图像变小
canvas.drawBitmap(bitmap, 0, 0, mPaint);
canvas.restore();
在演示以上效果后,但是笔者并不打算详细讲解,因为Canvas自己本身也有translate
方法,其拉远拉近效果可以使用canvas的缩放API完成,改变图像可以完成的事情,没有必要去改变三维坐标轴,因此了解即可。
【声明:“Camera三维变换”部分引用并参考了扔物线一文,以上黑色截图来自于扔物线讲解的视频,若有不妥,请联系笔者删去。这一部分笔者想要使用简单的例子讲解很难说明白,但扔物线写的这篇文章中配合了一个小视频动画演示,简直是醍醐灌顶,一点就透,因此笔者把重点制作成图片、GIF粘贴过来,墙裂推荐~】
hhha,想不到吧,去年开始写的系列写到一半笔者又去写热修复,时隔多月还是回来填坑了。事出有因,Android UI这一块其实说来说去就是Paint、Canvas、Path、PathMeasure、动画、绘制顺序、自定义View/Layout,如今网络上类似的讲解文章多如牛毛,笔者一开始的打算是通过此系列总结通透这块内容,但是发现有的内容太基础了,有的点详细讲解内容太多了,想要写出精华实在难以下笔。推荐多逛逛帖子,观摩学习别人的博客文章,你可以发现每个人写文章的详略点不同,讲解逻辑能力,有的文章简直让笔者欲罢不能,不由得朝天大喊,精辟!
最后笔者心里有了大概的谱,在跟自己妥协的同时保证文章的质量,卷土重来:)
若有错误,虚心指教~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。