Android 中的范围裁切和几何变换
真的很抽象,很难理解。
范围裁切
在Canvas
类中有很多用于范围裁切的函数,首先来介绍一下clipRect()
,这是一个用于裁剪矩形区域的函数,只有在指定的矩形区域内绘制的内容才会被显示出来。该函数接收 4 个参数,代表所裁剪矩形的位置(左上右下),这和定义一个矩形是一致的:
1 |
|
程序运行,将会看到图片被裁成原本大小的四分之一。
接下来是clipPath()
,clipPath()
允许创建更复杂的裁剪区域,这与它所接收的参数是一个 Path 对象有关,和clipRect()
一样,只有在该区域内绘制的内容才会被显示出来:
1 |
|
在上面的代码中,定义了一个 Path 对象,并向其中添加了一个椭圆,随后为这个椭圆设置了位置参数,使其成为矩形图片的内切圆,最后调用clipPath()
并将这个 Path 对象传入以实现裁切效果,最终的效果就好像各大社交 app 中的头像一样,把一个矩形的图片限制在一个圆形的头像框内。
值得一提的是,在先前的笔记中,这种将矩形裁成其内切圆的效果,也可以通过 Xfermode 的混合操作来实现,它和clipPath()
的差距就在于裁切后的图片是否含有锯齿。对于clipPath()
来说,是有锯齿的,这是因为clipPath()
会按照 Path 对象提供的路径进行严格裁切,所裁切出来的区域存在的锯齿是无法通过 Paint 对象的ANTI_ALIAS_FLAG
来进行修正的。反观 Xfermode 则可以在使用 Paint 对象进行绘制时向其设置抗锯齿属性,让最终效果看起来更平滑。
此外还有clipOutRect()
和clipOutPath()
,这两个函数的效果与clipRect()
和clipPath()
是相反的,也就是在被裁切的区域之外绘制的内容才会被显示出来,这里就不记录用例了。
几何变换
View 和 Canvas 有各自的坐标系,几何变换改变的是 Canvas 的坐标系,二维变换通常用到这四个方法:
translate(float dx, float dy)
: 用于平移坐标系,它将坐标系沿着 x 轴和 y 轴分别平移 dx 和 dy 个像素,之后所有的绘制操作都会在新的坐标系上进行。rotate(float degrees)
: 用于旋转坐标系,它将坐标系以原点为中心旋转 degrees 度,之后所有的绘制操作都会在新的坐标系上进行。rotate(float degrees, float px, float py)
:三参数版本,坐标系将以横坐标为 px 纵坐标为 py 的点为中心旋转 degrees 度。
scale(float sx, float sy)
: 用于缩放坐标系,它将坐标系沿着 x 轴和 y 轴分别缩放 sx 和 sy 倍,之后所有的绘制操作都会在新的坐标系上进行。skew(float sx, float sy)
: 用于扭曲坐标系,它将坐标系沿着 x 轴和 y 轴分别扭曲 sx 和 sy 度,之后所有的绘制操作都会在新的坐标系上进行。
这些方法的作用对象都是 Canvas 坐标系,而不是图像或者图形本身,所以在进行多次变换的情况下,不同的调用顺序将会出现不一样的结果,就比如先平移后旋转和先旋转后平移是完全不一样的,所以调用这些方法的顺序就显得尤为讲究。
这里有一个技巧,对于如何在变换后仍然能绘制出理想结果也许有帮助:如果能够清楚地知道每次调用变换方法后,新的 Canvas 坐标系的位置,那么就可以按照正常的思路来进行变换;反之,如果关注点在于图像,不考虑坐标系的变换,或者说假设坐标系一直位于起始位置没动过,那么就可以将原来的写法倒过来写,就能得到理想结果。
Matrix
上述提到的translate(float dx, float dy)
等方法都是 Canvas 中用于进行二维变换的方法,而Matrix
类也有一系列方法可以提供二维变换:
setTranslate(float dx, float dy)
:清除 Matrix 中的所有其它变换,然后设置新的平移变换。两个参数分别表示 x 和 y 方向上的位移。preTranslate(float dx, float dy)
:将新的平移变换与 Matrix 中的现有变换进行前乘,即新的平移变换会首先被应用。该方法在实现复合变换时会用到,两个参数分别表示 x 和 y 方向上的位移。postTranslate(float dx, float dy)
:与上面的相反,即后乘,该变换会靠后被应用。
setRotate()
:设置旋转变换。preRotate()
:同上。postRotate()
:同上。
setScale()
:设置缩放变换。preScale()
:同上。postScale()
:同上。
setSkew()
:设置斜切变换。preSkew()
:同上。postSkew()
:同上。
Matrix 的变换更适合被复用,因为它的作用对象并不一定是 Canvas 坐标系,简单来说就是谁应用了 Matrix ,谁就会变换。例如将其应用给 Bitmap :
1 |
|
或者也可以将其应用给 Canvas ,如果是这样的话,所有的变换又将会影响到坐标系了:
1 |
|
要将 Matrix 应用给 Canvas 一般有两个方法可以调用,分别是setMatrix()
和concat()
,前者是将 Canvas 的当前变换矩阵替换为指定的 Matrix ,后者则是将指定的 Matrix 与 Canvas 的当前变换矩阵进行合并。由于上面的代码中,Canvas 本身也没有发生变换,所以调用两个方法得到的效果都是一致的。
三维变换
三维变换通常会使用到Camera
类,Camera
类是 Android 提供的一个用于实现三维变换效果的类,它可以实现从相机视角的角度来对 Canvas 进行平移、旋转、缩放等操作。进一步说,Camera 的变换操作将默认以坐标系原点为中心进行,并且其所产生的变换不是针对具体某一个物体,而是会影响整个场景中的所有物体。
三维变换涉及到三维(Camera)坐标系,其具有以下特点:
- 增加了一个垂直于屏幕的 z 轴,指向屏幕内的为正方向。
- 原点(三轴交汇处)默认在 View 左上角。
- 存在一个 Camera 模型,可以将其想象成用户的眼睛,其位于屏幕外的 z 轴上,对 Camera 坐标系上的图像进行投影,投射到 Canvas 坐标系上的内容将是最终的绘制结果。
- y 轴的正方向由往下为正变为了往上为正。
接下来通过一个旋转 x 轴从而达到翻起图形的效果的例子来说明一下:
1 |
|
在上面的代码中,尝试通过调用 Camera 对象的rotateX()
来让 Camare 坐标系沿 x 轴进行旋转。程序运行后,可以看到在 Canvas 上绘制的矩形确实实现了翻起的效果,但是整个图像整体又过于偏右了,因为理想中的结果是,这个矩形在被翻转后,将会变成一个等腰梯形。
为什么结果应该是等腰梯形?这主要是前面提到的 Camera 会对其坐标系上的图像进行投影,而在现实生活中,人眼在观察物体的时候是没有投影的,就算把一个本子或者是一张纸放在我们面前进行 x 轴翻转,最终的结果也仍然是矩形。
但为什么实际运行的结果又不是等腰梯形?这是因为理想中的变换操作是建立在 Camera 坐标系的原点位于图形或图像中心进行的,但实际上前面也提到,原点默认位于屏幕左上角,又因为 Camera 是位于原点的 z 轴上的,所以当往右投影时,投射出来的结果会往右倾斜就好理解了。
正确的做法应该是让 Camera 坐标系的原点处在 Bitmap 的中心,这样投射出来的结果就是正确的了。而为了达到这个目的,方法之一是:
- 移动 Canvas 坐标系,让 z 轴刚好贯穿 Bitmap 的中心。
- 调用
applyToCanvas()
进行投射。 - 投射完成后再把 Canvas 坐标系移回原处。
根据上面的思路修改前面的代码:
1 |
|
即便思路很清晰,程序运行却仍然没有得到理想的结果。这是因为三维变换一般都会用到前面提到的技巧,也就是在正常思路下的代码写完以后,按照正常思路,把代码再「倒着写」一遍,这样一来,上面的代码就会变成:
1 |
|
再次运行就可以得到想要的结果了。
移动 Camera
在Camera
类中,有一个方法叫setLocation(float x, float y, float z)
,该方法用于设定 Camera 的位置,默认位置为(0, 0, -8)。其中第三个参数,也就是垂直于屏幕的距离的远近,对绘制结果的影响最大,它的变化会影响投影时图像或图形的大小,所以这个方法经常用来对拥有不同屏幕像素密度的设备进行适配。至于另外两个参数,一般用处不大。
这个物理模型其实也好理解,离屏幕近,看到的东西就大,反之就小,默认值为-8
就代表 Camera 默认就是在屏幕外的,已经有一个高度了,所以就算不手动调用setLocation()
,也仍然可以实现一些 3D 效果,就比如上面的投影。但是一个写死的值是不能成为最佳方案的,为了适配上面提到的不同屏幕像素密度的设备,就需要进行以下修改:
1 |
|
由于resources.displayMetrics.density
是屏幕像素密度,会根据不同的设备而变化,故而-8F * resources.displayMetrics.density
整体就是一个动态的值,至于-8
其实也不一定是-8
,也可以是-7
或者-9
,具体是哪个值可以根据具体的显示效果来调整,直到满意为止。
简单运用
接下来通过将范围裁切和几何变换进行配合,实现一个 Bitmap 对半折效果(从下往上翻):
1 |
|
并没有现成的方法能够实现 Bitmap 对半折(一半翻起来另一半不翻)的效果,所以自行实现的思路就是在几何变换的基础上加上范围裁切,先裁切下半部分,然后进行翻转,最后裁切上半部分,将两部分重合即可。
这里额外需要提及的就是调用了 Canvas 的save()
和restore()
来保存和恢复状态,这是因为在第一次裁切以后,Canvas 的可绘制区域已被改变,需要通过保存裁切前的状态,并在裁切完成后进行恢复,才能保证绘制下半部分时能正常工作。