使用.transform旋转带来的坑

jopen 8年前

这礼拜都在玩树莓派,Xcode都几乎没打开过,该收收心了。OmniFocus里攒了一堆已过期的待办,feedly也有好多未读,都又到周末了,先把上个礼拜留下来的几个准备写的Topic写了吧。

关于UIView的旋转,踩了一个小坑,看了看也没人说过。大家都知道使用.transform可以做旋转,用起来也很简单。在我的UIView Demo中(见上周Blog UIController中Slider监听回调具体实现的分离 ),我也添加了旋转的功能,开始是在handler中这么写的(back是一个UIView):

let x = (Float(ctl.show.frame.width) - conf[0] )/2  let y = (Float(ctl.show.frame.height) - conf[1] )/2  back.frame.size = CGSize(width: CGFloat(conf[0], height: CGFloat(conf[1])  back.frame.origin = CGPoint(x: Int(x), y: Int(y))  back.transform = CGAffineTransformMakeRotation( CGFloat(Double(conf[7] * M_PI / 180.0))

前面几句修改大小和位置,最后一句做旋转。没添加旋转之前缩放和位移都是对的,添加了旋转之后效果却不是我想象的那样以图形中心为圆心转动,而是这样有时候会自己拉扁的:

添加了打印信息以后发现,经过旋转的View.frame,不管是size还是position都变掉了,并且完全不是按照希望的样子变化的。不论我把旋转放在整个片段的哪里,都不对,frame的size总是不受控制。后来只好把scaling的部分改成了CGAffineTransformScale()。去看了看transform系列的文档,它其实是一类矩阵变换方式,旋转、缩放、平移只是其中的一些常用算法,系统给出了这些功能的API。如果你数学够好,其实也是可以自己设置这种变换的映射方式,最后转换后的View,其frame size是包含目标图形的一个最小矩形。而我猜,我碰到的问题,是因为系统对于旋转的处理有点特殊,因为scaling时拿到的参数不正确(当时的frame还没有画成缩放后的样子)导致。

于是我将UIView的变化都改为用CGAffineTransform系列函数进行,又发现对transform的操作先后顺序会影响最终的效果。出于兴趣我在playground里面做了点测试,先申请了一个100*300的矩形,然后做了两个transform的参数分别表示旋转90度和缩放至200*200(长宽缩放比例为1:2和1:0.667):

let demo = UIView(frame:CGRectMake(0.0, 0.0, 100.0,  300.0))  //缩放参数  let sx =  200 / demo.frame.width  let sy =  200 / demo.frame.height  let transf_scale = CGAffineTransformMakeScale(sx, sy)  //旋转参数  let transf_rotate = CGAffineTransformMakeRotation(CGFloat(Double(90) * M_PI  / 180.0))

对这个View分别进行缩放和旋转操作,理想的情况是变成一个200*200的正方形。四个测试Case分别如下:两个先缩放再旋转,两个先旋转再缩放,后两个用的是CGAffineTransformConcat()函数直接跑。

//case 1, rotate(scale, x)  demo.transform = CGAffineTransformRotate(transf_scale, CGFloat(Double(90) * M_PI  / 180.0))  demo.frame.size     //600*67    //case 2, scale(rotate, x)  demo.transform = CGAffineTransformScale(transf_rotate, sx, sy)  demo.frame.size     //200*200    //case 3, scale+rotate  demo.transform = CGAffineTransformConcat(transf_scale, transf_rotate)  demo.frame.size     //200*200    //case 4, rotate+scale  demo.transform = CGAffineTransformConcat(transf_rotate, transf_scale)  demo.frame.size     //600*67

有兴趣的可以把这一段code放到playground里面自己跑跑,看看会是什么结果。case2 & case3,结果如我们所想象的那样,是个200*200的正方形;而case1 & case4,却变成了600*66.7的长条矩形。看起来正如下图的几种情况:

图中,暗红色部分是先做缩放再做旋转,有了正确的结果,对应scale(rotate, x) & scale+rotate;银色部分则是先旋转再缩放,成了错误的样子,对应rotate+scale & rotate(scale, x)。原来这是有顺序要求的。那么问题又来了:CGAffineTransformXXX(t, args)系列的函数,感觉上应该是对t用新的参数做叠加,为什么结果却是反的呢?于是我去查了它们的头文件,看到是这么定义的:

/* Scale t’ by (sx, sy)’ and return the result:

t’ = [ sx 0 0 sy 0 0 ] * t */

@available(iOS 2.0, *)

public func CGAffineTransformScale(t: CGAffineTransform, _ sx: CGFloat, _ sy: CGFloat) -> CGAffineTransform

/* Rotate t’ by angle’ radians and return the result:

t’ = [ cos(angle) sin(angle) -sin(angle) cos(angle) 0 0 ] * t */

@available(iOS 2.0, *)

public func CGAffineTransformRotate(t: CGAffineTransform, _ angle: CGFloat) -> CGAffineTransform

/* Concatenate t2’ to t1’ and return the result:

t’ = t1 * t2 */

@available(iOS 2.0, *)

public func CGAffineTransformConcat(t1: CGAffineTransform, _ t2: CGAffineTransform) -> CGAffineTransform

</div>

于是真相大白:原来, CGAffineTransformScale(t1, args)= t-scale*t1,而CGAffineTransformConcat(t1, t2)= t1 *t2,它们的顺序是反的 。这个坑有点绕得人人晕晕的:flushed:

最后,综上所述,对于transform的使用,我总结了以下几点:

  1. transform参数,事实上是一些形变映射矩阵的叠加。
  2. Scale的时候设置的参数,代表的是缩放的系数,不是目标size,所以如果有其他transform参数在此之前做,要重新计算系数
  3. CGAffineTansform系列本质是矩阵运算,顺序很重要
  4. CGAffineTransformConcat(t1,t2)的顺序是t1->t2, 而CGAffineTransformXXX(t1, (args to t2))的顺序是t2->t1,不要搞错
</div>

来自: http://conanwhf.gitcafe.io/2016/01/16/transf/