图层几何学与几何变换

rntf0629 8年前
   <p>CALayer基础介绍完成后,我们已经能过实现很多的基本的视觉效果了,但是这些效果都还是静态的远远没有动画交互带来的那种体验。动画效果的实现的基本原理就是:对平移、缩放、旋转等几何变化进行组合然后设定一个动画持续时间,然后系统就会帮我们实现这些动画帧。本文将会介绍哪些iOS中动画涉及到的几何学概念和原理。</p>    <h3>iOS图形几何学</h3>    <p>几何学的基础应用就是要在对应的坐标系统里面对事物进行布局操作,而这些布局位置也是所有动画实现的基石。iOS中涉及布局的属性有UIView中的frame、bounds、center以及对应的CALayer中的frame、bounds、</p>    <p>position。</p>    <p>对于UIView:</p>    <ul>     <li> <p>frame:子视图最小外接矩形相对于父视图最小外接矩形的位置和大小</p> </li>     <li> <p>bounds:代表自身的坐标系统,常用于判断系统</p> </li>     <li> <p>center:与CALayer中的 <em>position</em> 属性等值。</p> </li>    </ul>    <p>对于CALayer:</p>    <ul>     <li> <p>frame:子图层最小外接矩形相对于父图层最小外接矩形的位置和大小</p> </li>     <li> <p>bounds:代表自身的坐标系统,常用于判断系统</p> </li>     <li> <p>position:子图层锚点 <em>anchorPoint</em> 相对于父图层的位置</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a3b934b3e47f8663fab55d192d2411c5.jpg"></p>    <p>对于视图和图层来说 <em>frame</em> 是一个虚拟属性,这个最小外接矩形其实是根据 <em>center</em> 、 <em>position</em> 、 <em>bounds</em> 、 <em>transform</em> 等属性计算得到的。也就是说改变其中任何一个属性值都会相应地导致 <em>frame</em> 属性的变化,只不过平时使用的时候视图和图层都是没有做旋转操作无法察觉 <em>frame</em> 与 <em>bounds</em> 的区别。</p>    <p>锚点 <em>anchorPoint</em></p>    <p>从一个例子开始入手吧,想象一下,把一张A4白纸用图钉订在书桌上,如果订得不是很紧的话,白纸就可以沿顺时针或逆时针方向围绕图钉旋转,这时候图钉就起着支点的作用。我们要解释的 <em>anchorPoint</em> 就相当于白纸上的图钉,它主要的作用就是用来作为变换的支点。很明显旋转支点位置不同得到的旋转效果差别是很大的。 <em>anchorPoint</em> 的取值是相对与bounds的比列来计算的,左上角为(0,0)又下角为 (1,1),默认 <em>anchorPoint</em> 为(0.5,0.5)。</p>    <p>下面是官方iOS左手系和macOS右手系中的概念和旋转情形的图解:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2c1928c5b7e85ece9ae32404c7c79e9c.png"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5ea880929ee61b562a15599ab60a5d30.png"></p>    <p><em>anchorPoint</em> 的数值发生改变的时候,实际上移动的不是 <em>anchorPoint</em> 而是 <em>bounds</em> 。 <em>bounds</em> 会根据 <em>anchorPoint</em> 计算偏移量,然后进行反向偏移。上面说过 <em>frame</em> 是 <em>bounds</em> 最小外接矩形,那么这意味着 <em>frame</em> 会相应地移动。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8ae0e66cdabd521e165db99a57ec67c9.jpg"></p>    <p>在上图你可以发现移动前后 <em>position</em> 的数值没有发生改变,而且与 <em>anchorPoint</em> 相对与父视图的位置是一致的。同时你还可以发现如下的等式关系:</p>    <pre>  <code class="language-javascript">position.x = frame.origin.x + anchorPoint.x * bounds.size.width;   position.y = frame.origin.y + anchorPoint.y * bounds.size.height;</code></pre>    <p>但是该公式并不是正确的,它适用于与上面这种 <em>frame</em> 与 <em>bounds</em> 重合的特殊情况。现实情况远比这个复杂尤其当图形发生过旋转后。上面等式中的: <strong> anchorPoint.x <em>bounds.size.width</em> </strong> 和 <strong>anchorPoint.y bounds.size.height</strong> 在旋转图形中并不代表的∆x和∆y,还需要与变换矩阵 <em>transform</em> 进行计算。这超过了本文的内容,感兴趣的可以自己回忆一下线性代数和计算机图形学。我们唯一需要知道的就是 <em>position</em> 属性其实是根据计算得到的,它代表了 <em>anchorPoint</em> 在suplayer中的相对位置。</p>    <h3>几何变换</h3>    <p>无论是电影、游戏以及其他给你带来强烈视觉冲击的那些动画效果也包括软件应用中的那些交互动画,其实都是一系列变化过程的静态图片在添加Timeline后以你肉眼无法察觉的频率更换图片来达到的。而这些时间线上图片的状态变化无非就是平移、旋转、缩放以及它们组合起来的几何变换。下面我们开始来聊聊这些几何变换。</p>    <p>仿射变换</p>    <p>仿射变换是指在二维空间坐标系统中对图像进行平移、旋转、缩放等几何变换。在iOS系统中UIView的 <em>transform</em> 和CALayer的 <em>affineTransform</em> 属性就是用来实现这些变换的,这两个属性都对应同一个类型: CGAffineTransform。该类型其实就是我们在线性代数中常用的矩阵,它的结构如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5dee9289920fa76133698c3e9e012f13.jpg"></p>    <p>下面我们再来看下线性代数中二维空间的方式变化公式以及最终得到的计算结果:左侧为变化后的坐标,右侧为原始坐标以及变换矩阵:</p>    <p><img src="https://simg.open-open.com/show/5b0e62207b9bc64c25fdb4e5e287d9e5.jpg"></p>    <p>上面的等式中我们能够发现CGAffineTransform中的 <em>a</em> 、 <em>d</em> 两个属性对应的是缩放、 <em>tx</em> 、 <em>ty</em> 对应的是平移、 <em>b</em> 、 <em>c</em> 对应的是旋转。所以我们可以知道CGAffineTransform中:</p>    <ul>     <li> <p>rotated(by: CGFloat)函数设置的是 <em>c</em> 、 <em>d</em> 属性的值,这个值对应的是弧度切逆时针为正。</p> </li>     <li> <p>scaledBy(x: CGFloat, y: CGFloat)函数设置的值分别为 <em>a</em> 、 <em>d</em> 属性的值。</p> </li>     <li> <p>translatedBy(x: CGFloat, y: CGFloat)函数设置的值分别为 <em>tx</em> 、 <em>ty</em> 属性的值。</p> </li>    </ul>    <p>你可以对上面三个基本变换进行组合来实现自定义的变换,也就是说复杂的仿射变换可以通过拆封然后进行组合通过矩阵计算得到最终的变换 <em>CGAffineTransform</em> 各个属性的值。</p>    <h3>3D变换</h3>    <p>除了上面常用的二维仿射变换,CALayer还可以实现更复杂的3D动画。在变换的过程中屏幕到人眼将作为三维空间中的Z轴,对应的属性变量为 <em>zPosition</em> 。与仿射变换一致3D变换的实现也是基于线性代数的计算,只不过矩阵的维数比之前更多而已,对应的属性是CATransform3D类型的tramsform,下图是官方的矩阵变换计算公式以及常用的变换矩阵:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3740671e914e443136578c7da30843e7.png"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f3267c1dfd9a375214732a0bf4a82ce4.png"></p>    <p>注意:矩阵变换计算公式在数学表达上其实是错的,应该是1x4矩阵乘以4x4矩阵,但是这不影响你对文章本身的理解。</p>    <p>具体每一个属性值对应的作用你可以参照上一部分的讲解,同时对照常用变换矩阵一切就很明了。</p>    <p>透视投影</p>    <p>我们先看一下将突破绕Y轴旋转45º的代码以及效果图:</p>    <pre>  <code class="language-javascript">override func viewDidLoad() {      super.viewDidLoad()      self.view.backgroundColor = UIColor.orange      viewForLayer = UIView.init(frame: CGRect.init(x: 50, y: 50, width: self.view.bounds.width - 100, height: self.view.bounds.height/2))      viewForLayer.backgroundColor = UIColor.white      viewForLayer.layer.contents = UIImage.init(named: "YiXiu")?.cgImage      viewForLayer.layer.contentsGravity = kCAGravityResizeAspect      let  transform :CATransform3D = CATransform3DMakeRotation(CGFloat(M_PI_4), 0, 1, 0)      viewForLayer.layer.transform = transform      self.view.addSubview(viewForLayer)  }</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6bdb4ddfb13cd8ecfdb9d60e2d2fe272.png"></p>    <p>是不是很奇怪?明明设置了旋转效果,但是图片看起来仅仅是水平方向上进行了一些压缩而已。其实代码和效果都没有错,原因就处在了视角上面。我们使用的是一个等距的视角,而不是现实世界中我们眼球所处的透视视角。</p>    <p>在现实世界中因为视角的原因会让我们产生一种视觉误差,那就是远处的物体看起来会比近处的物体小。而实际上远处的物体可能比眼前的更大,上面的效果就是因为我们是一种等距视角所以显示的缩放比例是一致的也就不会产生眼球那种透视所带来的“假象”视觉效果。当然习惯的力量是强大的,虽然iOS没有提供实现透视效果的变换函数,我们还是可以通过设置属性值来实现对眼球的欺骗。这个属性就是CATransform3D矩阵中的 <em>m34</em> ,它主要就是用来设置:</p>    <pre>  <code class="language-javascript">override func viewDidLoad() {      super.viewDidLoad()      self.view.backgroundColor = UIColor.orange      viewForLayer = UIView.init(frame: CGRect.init(x: 50, y: 50, width: self.view.bounds.width - 100, height: self.view.bounds.height/2))            viewForLayer.backgroundColor = UIColor.white      viewForLayer.layer.contents = UIImage.init(named: "YiXiu")?.cgImage      viewForLayer.layer.contentsGravity = kCAGravityResizeAspect            var  transform: CATransform3D = CATransform3DIdentity           transform.m34 = -1.0 / 500      transform = CATransform3DRotate(transform,CGFloat(M_PI_4), 0, 1, 0)      viewForLayer.layer.transform = transform          self.view.addSubview(viewForLayer)        }</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1d36c2611849537b85779d2a0383f7b4.png"></p>    <p>灭点与sublayerTransform</p>    <p>当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一个点。这个点就是图形学中的灭点,通常情况下位于视图的中间。在CALayer中这个灭点与 <em>anchorPoint</em> 是重合的,这意味着我们在设置多个sublayer的时候可能因为位置的不同导致灭点的位置也不同,这直接就回导致3D显示效果会非常的差。所以对于这种多sublayer的情况,我们可以先将这些sublayer统一放在父图层的中间,然后通过变换矩阵进行平移。这样我们就能保证灭点位置的一致从而实现完美的显示效果。</p>    <p>在多sublayer情况下还有一个棘手的问题就是:如果我们要对图层作变换那么是不是意味着我们都要去对每个sublayer的 <em>m34</em> 进行设置来实现透视效果呢?这种情况下,我们可以通过设置父图层的 <em>sublayerTransform</em> 来让所有的sublayer进行自动集成来实现全部sublayer的同步变换。</p>    <h3>总结</h3>    <p>前后分篇文章概要的讲解了Core Animation架构、CALayer的基础、以及图层几何学,虽然不是很详尽但是看完后应该对Core Animation有了一些基本的认识。在这些基础上,后面我可能还会详细的带来一些特殊图层的分析和应用当然还有常见动画的实现。</p>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000007708351</p>    <p> </p>