iOS开发经验(6)-Transform

目录(transform)

  1. 基础及矩阵概念
  2. 2D仿射
  3. 3D仿射
1. 基础及矩阵概念
仿射变换.png
  • 在iOS的动画效果中,变换是很常见的,包括仿射变换和3D变换等。变换的终极原理就是矩阵的乘法运算,3D变换涉及到三维透视投影的一些原理知识。
  • transform基本概念
    transform在矩阵变换的层面上改变视图的显示效果,完成旋转、形变、平移等等操作。在它被修改的同时,视图的frame也会被真实改变。有两个数据类型用来表示transform,分别是CGAffineTransformCATransform3D。前者作用于UIView,后者为layer层次的变换类型。基于后者可以实现更加强大的功能。
    UIView可以通过设置transform属性做变换,但实际上它只是封装了内部图层的变换。
    CALayer同样也有一个transform属性,但它的类型是CATransform3D,而不是CGAffineTransformCALayer对应于UIViewtransform属性叫做affineTransform
  • 矩阵概念
    矩阵 × 坐标:当你用矩阵乘以一个坐标时,它们的乘积就是一个变换后的新坐标。
    齐次坐标:为了把二维图形的变化统一在一个坐标系里,引入了齐次坐标的概念,即把一个图形用一个三维矩阵表示,其中第三列总是(0,0,1)(为了能让矩阵做乘法),用来作为坐标系的标准。所以所有的变化都由前两列完成
    为了能让矩阵做乘法:左边矩阵的列数一定要和右边矩阵的行数个数相同,所以要给矩阵填充一些标志值,使得既可以让矩阵做乘法,又不改变运算结果,并且没必要存储这些添加的值,因为它们的值不会发生变化,但是要用来做运算。因此,通常会用3×3(而不是2×3)的矩阵来做二维变换.
    当对图层应用变换矩阵,图层矩形内的每一个点都被相应地做变换,从而形成一个新的四边形的形状。CGAffineTransform中的“仿射”的意思是无论变换矩阵用什么值,图层中平行的两条线在变换之后任然保持平行,CGAffineTransform可以做出任意符合上述标注的变换
    单位矩阵
CGAffineTransformIdentity
  • 仿射变换的原理和计算
    仿射变化原理是数学中的矩阵原理(线性代数-矩阵分析),要弄明白仿射矩阵对作用点的影响,还得先看看矩阵的乘法怎么计算。

    基础-矩阵的乘法
    计算规则:

    • 不符合交换律(A和B是矩阵,AB不一定等于BA)
    • 当矩阵A的列数等于矩阵B的行数是,才可以计算
    • 计算的结果矩阵C的行数等于A的行数,列数等于B的列数(如A是m×n矩阵和B是n×p矩阵,它们的乘积C是一个m×p矩阵 )
    • 结果矩阵C的第 i 行第 j 列的元素Cij 等于矩阵A的第 i 行的元素与矩阵B的第 j 列对应元素乘积之和
矩阵A = 
[1  1]
[2  0]
矩阵B = 
[0  2  3]
[1  1  2]
计算过程:
矩阵C = A * B =    
[(1 * 0 + 1 * 1) (1 * 2 + 1 * 1) (1 * 3 + 1 * 2)] 
[(2 * 0 + 0 * 1) (2 * 2 + 0 * 1) (2 * 3 + 0 * 2)]   
矩阵C =
[1 3 5]
[0 4 6]
  • 仿射变换的矩阵计算
    仿射计算中,(以二维坐标为例,坐标点为x,y)我们设我们的坐标点矩阵为
坐标点矩阵
A = [x y 1]

仿射变换基础矩阵为:

B = 
[a  b  0]
[c  d  0]
[tx ty 1]

根据矩阵计算规则我们知道A x B的结果是一个1行3列的矩阵,设A x B得到的新矩阵C ,那么C的矩阵应该为

C = [(a*x+c*y+tx) (b*x+d*y+ty) 1]
设C为 = [x' y' 1] , 那么可以得到
x' = a*x + c*y + tx
y' = b*x + d*y + ty
2. 2D仿射
  • 仿射是什么
    仿射变换可以理解为
    • 对坐标进行放缩,旋转,平移后取得新坐标的值。
    • 经过对坐标轴的放缩,旋转,平移后原坐标在在新坐标领域中的值。
    • transform是一个矩阵,变换后的点坐标等于之前的点坐标乘以矩阵

当操纵一个变换的时候,初始生成一个什么都不做的变换很重要--也就是创建一个CGAffineTransform类型的空值,矩阵论中称作单位矩阵,Core Graphics同样也提供了一个方便的常量:CGAffineTransformIdentity
iOS封装了几个好用的CGAffineTrans的API去实现仿射变换的效果
注意:带Make的,起点固定,每次控制的事件只针对起点。不带Make的:为一个变换再加上平移,针对上一个位置,不针对起点。

//位移仿射
CGAffineTransformMakeTranslation
CGAffineTransformTranslate
//旋转仿射
CGAffineTransformMakeRotation
CGAffineTransformRotate
//缩放仿射
CGAffineTransformMakeScale
CGAffineTransformScale
//叠加仿射效果
CGAffineTransformConcat
//仿射矩阵方法,可以直接做效果叠加
CGAffineTransformMake (sx,shx,shy,sy,tx,ty)

带make的方法是直接返回一个仿射变换效果,不带make方法是用来给已有的仿射效果叠加效果
类似于CGAffineTransformConcat方法的作用
CATransform3D也对应一组相似的api,这个后面在研究到它的时候仔细说说.

在大多数情况下用这几个封装好的仿射方法基本就能实现大多数的需求。但是既然是研究仿射,可以看CGAffineTransform最基础的那个仿射方法的使用,能理解这个方法的使用,基本上就知道仿射是个什么意思了。

CGAffineTransformMake(CGFloat a,CGFloat b,CGFloat c,CGFloat d,CGFloat tx,CGFloat ty)

这个方法的6个参数可以拼出一个矩阵

//仿射矩阵
[a  b  0]
[c  d  0]
[tx ty 1]

仿射变化的定义有点复杂,我自己理解就是: 点p(以二维坐标为例)通过仿射矩阵C 后变成新的点p' 。
以上面的缩放仿射变化CGAffineTransformScale为例。就是view中每一个点p通过矩阵C后,view中的每一个新的点p'组成的新的图形,相比之前的view有了缩放的效果。

这步很关键。根据这个公式,那么仿射矩阵就可以分成5种分别对应:平移(Translation),缩放(Scale),翻转(Flip),旋转(Rotation),剪切(Shear)

平移演化

设a,d=1 c,b = 0 那么
x' = a*x + c*y + tx
y' = b*x + d*y + ty
就变成了
x' = x + tx
y' = y + ty

这个不就是新的点P'(x' y') 是根据原来的P在x和y分别添加了一个常量就可以得到了嘛? 所以这个就是仿射中的平移效果了。把a,b,c,d带入仿射的基础矩阵,就得了仿射的位移矩阵

//仿射基础矩阵
[a  b  0]
[c  d  0]
[tx ty 1]
//仿射位移矩阵
[1  0  0]
[0  1  0]
[tx ty 1]

再来看看代码

向右移动300的仿射效果
CGAffineTransform translate = CGAffineTransformMakeTranslation(300, 0)
使用仿射基础方法 
CGAffineTransform translate = CGAffineTransformMake(1,0,0,1,300,0)

缩放演化

x' = a*x + c*y + tx
y' = b*x + d*y + ty
设 c,b,tx,ty = 0 ,得到
x' = a*x
y' = d*y

新坐标(x',y')和原坐标是倍数关系,这个就可以用来做缩放效果了。所以得到缩放矩阵

//仿射缩放矩阵
[a  0  0]
[0  d  0]
[0  0  1]
//也可以写成
[sx  0  0]
[0  sy  0]
[0   0  1]
代码
//x和y都放大1倍
CGAffineTransform scaleAffine = CGAffineTransformMakeScale(2, 2)
//使用仿射基础方法
CGAffineTransform scaleAffine = CGAffineTransformMake(2,0,0,2,0,0)

剪切演化

x' = a*x + c*y + tx
y' = b*x + d*y + ty
设 a,d = 1 tx,ty = 0 ,得到
x' = x + cy
y' = y + bx

x和y的变化总是相互关联,这个就是剪切(Shear)

//仿射剪切矩阵
[1  b  0]
[c  1  0]
[0  0  1]
//也可以写成
[1  shx  0]
[shy  1  0]
[0    0  1]
代码
//使用仿射基础方法CGAffineTransformMake,设置x和y都为0.5的斜切
CGAffineTransform shearAffine = CGAffineTransformMake(1,0.5,0.5,1,0,0)

剪切效果只能用CGAffineTransformMake实现,但是通过CATransform3DMakeRotation可以有类似的效果
旋转演化

//仿射旋转矩阵
[cosa   sina  0]
[-sina  cosa  0]
[0      0     1]
代码
CGAffineTransform rotation = CGAffineTransformMake(CGFloat( cos(M_PI_4) ), CGFloat( sin(M_PI_4) ), -CGFloat( sin(M_PI_4) ), CGFloat( cos(M_PI_4) ), 0, 0)

翻转演化
我不知道使用CGAffineTransformMake()如何设置矩阵可以实现翻转效果。我阅读的所有Flip效果都是使用CATransform3D实现的,代码如下

//Flip仿射,要是有3D去实现
CATransform3D flip = CATransform3DMakeRotation(angle, x, y, z)
//示例
CATransform3D flipX = CATransform3DMakeRotation(CGFloat(M_PI), 1, 0, 0)
CATransform3D flipY = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)
CATransform3D flipZ = CATransform3DMakeRotation(CGFloat(M_PI), 0, 0, 1)

总结一下CATransform3DMakeRotation方法的6个参数

在不考虑旋转时,CATransform3DMakeRotation6个参数可以写成
//sx,sy:缩放因子
//shx,shy:斜切因子
//tx,ty:移动因子
CGAffineTransformMake (sx,shx,shy,sy,tx,ty)

这个是一个初始化矩阵,带入矩阵算法计算后的结构会得到
x'=x , y'=y
它的作用是清除之前对矩阵设置的仿射效果,或者用来初始化一个原始无效果的仿射矩阵

矩阵初始值。[ 1 0 0 1 0 0 ]
[ 1 0 0 ]
[ 0 1 0 ]
[ 0 0 1 ]

方法:

 //用来连接两个变换效果并返回。返回的t = t1 * t2
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2)
//矩阵初始值。[ 1 0 0 1 0 0 ]
CGAffineTransformIdentity
//检查是否有做过仿射效果
CGAffineTransformIsIdentity(transform)
//检查2个仿射效果是否相同
CGAffineTransformEqualToTransform(transform1,transform2)
//仿射效果反转(反效果,比如原来扩大,就变成缩小)
CGAffineTransformInvert(transform)
  • 应用
    放射矩阵一个常用的情形就是根据用户的手势来相应的改变视图的变换
    UIPanGestureRecognizer 对应位移
    UIPinchGestureRecognizer 对应缩放
    UIRotationGestureRecognizer 对应旋转
    通常如果需要看到实时的手指移动视图就相应的变换的技巧就是,每次接收到对应的gesture时间就相应的改变view的transform,然后吧这个gesture对应的translationscalerotation置为初始值。
3. 3D仿射
  • 三维坐标系:视角垂直与屏幕而言,x轴向右,y轴向下,z轴垂直屏幕向外。和UIView严格的二维坐标系不同,CALayer存在于一个三维空间当中。CALayer还有另外两个属性,zPositionanchorPoint
    最实用的功能就是改变图层的显示顺序。
self.greenView.layer.zPosition=1.0f;
  • 锚点 anchorPoint
    图层的锚点anchorPoint:是一个CGPoint值,x,y取值范围(0~1),默认为(0.5,0.5) 对于图层本身而言,顾名思义,锚点就用来定位图层的点。锚点有两个职能:
    (1)与position一同确定图层相对于父图层的位置;
    (2)通过设置该属性来实现视图围绕不同位置旋转。记得在视图添加到父视图之后在进行设置,因为frame也是相对于锚点的。
作为图层旋转、平移、缩放的中心
self.menuViewController.view.layer.anchorPoint = CGPointMake(1.0,0);

当图层发生变换时,这个点永远位于图层变换之前anchorPoint的位置。当改变一个图层的position,你也改变了它的锚点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position),这样所有的3D图层都共享一个锚点。

  • 关于锚点(anchorPoint)与position
  • position是决定了layer显示在父控件的哪个位置,默认position是该layer的中心点.
  • anchorPoint决定了layer自身显示在position的哪个位置.
  • position与锚点的默认点为中心点
  • position总是与锚点显示在同一位置
  • layer是否发生偏移有锚点决定
  • 3D仿射矩阵
    3D仿射矩阵类似于2D仿射,3D仿射也有一个基础矩阵,并且比2D的多一个维度
CGAffineTransform transform = CGAffineTransformIdentity;
结果:(CGAffineTransform) transform = (a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0)
[m11  m12  m13  m14]
[m21  m22  m23  m24]
[m31  m32  m33  m34]
[m41  m42  m43  m44]

矩阵的计算过程和2D类似,最后也能得到矩阵中每个位置的值对3D仿射效果的作用。我直接把结果贴出来。

平移因子: m41(x位置) m42(y位置) m43(z位置) 
缩放因子: m11(x位置) m22(y位置)
切变因子: m21(x位置) m12(y位置)
旋转因子: m13(x位置) m31(y位置)
透视因子: m34(有旋转才能看出效果)

m34的默认值是0,我们可以通过设置m34为(-1.0 / d)来应用透视效果,d代表了视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?大概估算一个就行了。
因为视角相机实际上并不存在,所以可以根据屏幕上的显示效果自由决定它放置的位置。通常500-1000就已经很好了,但对于特定的图层有时候更小后者更大的值会看起来更舒服,减少距离的值会增强透视效果,所以一个非常微小的值会让它看起来更加失真,然而一个非常大的值会让它基本失去透视效果
3D仿射常用的方法

//位移3D仿射
CATransform3DMakeTranslation
CATransform3DTranslation
//旋转3D仿射:x-y-z轴的有个确定的范围(介于-1 和+1之间)
CATransform3DMakeRotation
CATransform3DRotation
angle 是所需要变化的角度
x y z 设置为零表示不围绕该轴做旋转,如果设置为1,则绕该轴做旋转。
//缩放3D仿射
CATransform3DMakeScale
CATransform3DScale
//叠加3D仿射效果
CATransform3DConcat
//仿射基础3D方法,可以直接做效果叠加
CGAffineTransformMake (sx,shx,shy,sy,tx,ty)
矩阵初始值:[ 1 0 0 0 1 0 0 0 1 0 0 0]

这个是一个初始化矩阵,带入矩阵算法计算后的结构会得到

x'=x , y'=y , z'=z

它的作用是清除之前对矩阵设置的仿射效果,或者用来初始化一个原始无效果的仿射矩阵

矩阵初始值:
[ 1 0 0 0 ]
[ 0 1 0 0 ]
[ 0 0 1 0 ]
[ 0 0 0 1 ]
//矩阵初始值。[ 1 0 0 0 1 0 0 0 1 0 0 0]
CATransform3DIdentity
//检查是否有做过仿射3D效果
CATransform3DIsIdentity(transform)
//检查是否是一个仿射3D效果
CATransform3DIsAffine(transform)
//检查2个3D仿射效果是否相同
CATransform3DEqualToTransform(transform1,transform2)
//3D仿射效果反转(反效果,比如原来扩大,就变成缩小)
CATransform3DInvert(transform)
//2D仿射转换3D仿射
CATransform3DGetAffineTransform(transform)
CATransform3DMakeAffineTransform(transform)

CATransform3D与CGAffineTransform的转换

//将一个CGAffinrTransform转化为CATransform3DCATransform3D 
CATransform3DMakeAffineTransform (CGAffineTransform m);
//判断一个CATransform3D是否可以转换为CAAffineTransformbool
CATransform3DIsAffine (CATransform3D t);
//将CATransform3D转换为CGAffineTransformCGAffineTransform
CATransform3DGetAffineTransform (CATransform3D t);

当我们改变过一个view.transform属性或者view.layer.transform的时候需要恢复默认状态的话,记得先把他 们重置为

view.transform = CGAffineTransformIdentity;
view.layer.transform = CATransform3DIdentity;

CALayertransform属性是是个CATransform3D类型的数据,默认值为CATransform3DIdentity。需要注意的是,CALayer是有隐式动画的,如果你想关掉隐式动画,用

[CATransaction setDisableActions:YES];

另外,要特地说明一下矩阵中的一个参数m34,m34影响透视效果.当然,z方向上得有变化才会有透视效果,数值越大,透视效果越明显.正值/负值都有意义,导致透视方向的不同。d越大,效果越不明显,d越小,效果越明显甚至导致失真。d的一个推荐的值是500-1000之间。

  • 透视效果
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)
CATransform3D transform = CATransform3DIdentity;
// 透视效果
transform.m34 = 0.0005; 
transform = CATransform3DRotate(transform,(M_PI/180*40), 0, 1, 0);
[layer setTransform:transform];
注意我们使用的旋转常量是`M_PI_4`,而不是你想象的`45`,因为iOS的变换函数使用弧度而不是角度作为单位。
弧度用数学常量`pi`的倍数表示,一个`pi`代表`180`度,所以四分之一的`pi`就是`45`度。

第二行一定要写在第三行的前面;m34:透视效果m34= -1/M,M越小,透视效果越明显,必须在有旋转效果的前提下,才会看到透视效果。

  • 合并两个CATransform3D(CATransform3DConcat)
CATransform3D CATransform3DConcat ( CATransform3D a, CATransform3D b );
  • CATransform3DInvert效果反转(反效果,比如原来扩大,就变成缩小)
CATransform3D CATransform3DInvert ( CATransform3D t );
  • CATransform3D与CGAffineTransform的转换
//将一个CGAffinrTransform转化为CATransform3DCATransform3D 
CATransform3DMakeAffineTransform (CGAffineTransform m);
//判断一个CATransform3D是否可以转换为CAAffineTransformbool 
CATransform3DIsAffine (CATransform3D t);
//将CATransform3D转换为CGAffineTransformCGAffineTransform 
CATransform3DGetAffineTransform (CATransform3D t);
  • 减轻锯齿-shouldRasterize
    属性的默认值是NO,可将其设置为YES来减轻锯齿的效果。记得动画结束时将其设置为 NO
self.menuViewController.view.layer.shouldRasterize = NO;
  • content
    设置CALayercontents属性为一个image来设置图片,设置contentGravity来指定图层内容的拉伸方式。
    iOS CATransform3D ,旋转之后出现的锯齿处理方法
layer.shouldRasterize = YES;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容