iOS Animation 指北

a7820832 4年前
   <h2>写在前面</h2>    <p>iOS 向来以丝般顺滑的过度动画闻名,好的动画可以让用户更好地理解 app,并且可以让 app 更加有趣。有趣很重要。</p>    <p>iOS 动画(或者所有动画?)的原理简单来讲有两种:</p>    <ol>     <li>告诉系统动画对象在某几个时刻的状态(关键帧),由系统自动补全这些时刻之间的中间状态,再把所有这些状态平滑地显示出来。这种动画也叫做关键帧动画。</li>     <li>每隔一段很短的时间,重新绘制一次动画对象。</li>    </ol>    <h2>整体架构</h2>    <p>这里是一张 iOS 动画相关 framework 的架构图:</p>    <p><img src="https://simg.open-open.com/show/e11e65d36a1524cccb4ecc6709e822d4.png"></p>    <p>从图中可以看到,要实现一个 iOS 动画从上层到底层你会接触到 UIKit、Core Animation、Core Graphics/OpenGL。他们使用的难度依次递增,相应地灵活程度以及能实现效果的复杂程度也越来越高。</p>    <h2>基础知识</h2>    <h3>CALayer</h3>    <p>CALayer 是 UIView 背后用来管理显示内容的对象,它维护了一个位图以及位图的状态信息。这个位图可能是开发者用程序绘制的(比如在 drawRect 中),也可能是开发者指定的一张图片。</p>    <p>Core Animation 的所有功能都是基于 layer 的。当你通过 Core Animation 修改了 layer 的属性,Core Animation 会通过 GPU 来重新渲染位图。</p>    <p>利用 CALayer 的属性可以做很多事情,比如改变位置、大小、形状、透视角度、圆角、透明度等,利用 mask 属性还可以做出很多好玩的效果。这些属性的详细介绍在 <a href="/misc/goto?guid=4959738156213621019" rel="nofollow,noindex">这里</a> 。</p>    <p>CALayer 有很多有用的子类,常用的有下面几个:</p>    <ul>     <li>CAShapeLayer:用于绘制曲线等图形,有两个重要的属性 strokeStart & strokeEnd ,灵活使用有奇效。</li>     <li>CATextLayer:专门用来处理文字的 layer。</li>     <li>CAGradientLayer:顾名思义,用来处理渐变。</li>    </ul>    <h3>Timing function</h3>    <p>Timing function 用来描绘动画完成度随时间增加的曲线。</p>    <p>怎么理解这个含义呢,在前面提到的第二种动画实现方式中,我们设置好了关键帧后,系统需要计算出关键帧中间各个时间点的状态。</p>    <p>假设我们要完成的是一个物体沿直线从 (0,0) 移动到 (0, 100) 的动画,动画时长是 1s。系统可以以匀速将物体从起点移动到终点,也可以加速或者减速,甚至可以先加速再减速移动到终点,timing function 就是用来描绘这里的加减速。</p>    <p>看看 iOS 预定义的几种常见的 timing function:</p>    <pre>  <code class="language-objectivec">let kCAMediaTimingFunctionLinear: String  let kCAMediaTimingFunctionEaseIn: String  let kCAMediaTimingFunctionEaseOut: String  let kCAMediaTimingFunctionEaseInEaseOut: String  let kCAMediaTimingFunctionDefault: String  </code></pre>    <p><img src="https://simg.open-open.com/show/d3bfb9910807f220c35134ec6d3f889e.jpg"></p>    <p>可以看到,这里的横坐标是时间,纵坐标可以看做动画的完成度。</p>    <p>正如命名描绘的那样, kCAMediaTimingFunctionLinear 是线性增加的,而 kCAMediaTimingFunctionEaseOut 是先快后慢的。</p>    <p>你甚至还可以通过 init(controlPoints:_:_:_:) 方法,传入两个贝塞尔曲线的控制点来自定义想要的曲线。</p>    <p>这样做的意义是什么呢?因为人们在日常生活中见到的物体的运动几乎没有匀速运动的,比如汽车启动和刹车,比如杯子从桌子上掉落。合理运用 timing function 可以使动画更符合人们的经验,因而显得更加自然。</p>    <h2>UIKit</h2>    <p>UIKit 提供了一系列的基于 block 的 API。比如 UIView.animate() 系列方法。对于 view 的 <a href="/misc/goto?guid=4959738156303000254" rel="nofollow,noindex">animatable properties</a> , 你只需要在 block 中修改需要对应的参数即可,比如 frame、bounds、alpha 等。</p>    <p>如果想实现关键帧动画,UIKit 还提供了 UIView.animateKeyframes() 方法。</p>    <p>UIKit 适用于简单的动画,如移动、旋转、缩放、改变颜色等。</p>    <h2>Core Animation</h2>    <p>UIKit 实现动画适用起来非常简单,但是有很大的局限性:如果我想改变 cornerRadius 怎么办?如果我想沿一条曲线移动一个 view 怎么办?</p>    <p>这个时候就需要使用 Core Animation 了(UIKit 其实也是在 Core Animation 的基础上做了一层封装)。</p>    <p>Core Animation 分为以下几个部分:</p>    <h3>CABasicAnimation</h3>    <p>相比于 UIKit,CABasicAnimation 可以对更多的 CALayer 属性进行动画,比如 cornerRadius。完整的列表见 <a href="/misc/goto?guid=4959738156393392604" rel="nofollow,noindex">这里</a> 。</p>    <p>CABasicAnimation 使用起来非常方便:</p>    <pre>  <code class="language-objectivec">let verticalAnimation = CABasicAnimation(keyPath: "position.y") // 1  verticalAnimation.fromValue = 310  // 2  verticalAnimation.toValue = 10  // 3  verticalAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)  // 4  view.layer.add(verticalAnimation, forKey: "Fall") // 5    view.layer.position.y = 10  //6  </code></pre>    <p>简单解释一下:</p>    <ol>     <li>这里我们创建了一个 CABasicAnimation 实例,要对 layer 的 position.y 属性进行动画。</li>     <li>position.y 的初始值是 310。</li>     <li>position.y 的最终值是 10。</li>     <li>指定了动画的 timing function,后面会介绍。</li>     <li>将动画添加到 view 的 layer 上。</li>     <li>将 layer 的 model layer 属性更改为最终属性。</li>    </ol>    <p>什么是 model layer?</p>    <p>Core Animation 实际维护了三个 layer:model layer、presentation layer 和 render layer,其中的前两个我们平时会接触到,可以分别使用 CALayer 的两个属性 modelLayer 和 presentationLayer 来获得。Render layer 是系统私有的。</p>    <p>Model layer 的属性是不会变化的,如果你想得到 layer 在动画过程中实时的属性,就需要通过 presentation layer 来获取。</p>    <h3>CAKeyframeAnimation</h3>    <p>CABasicAnimation 只能让你设置一个初始状态和一个结束状态,如果你的动画需要拆解成几个连贯的动作,CAKeyframeAnimation 可以传入多个不同的值。</p>    <p>此外,CAKeyframeAnimation 还可以设置 CGPath,也就是说你可以让动画对象沿着曲线移动。</p>    <pre>  <code class="language-objectivec">let positionAnimation = CAKeyframeAnimation(keyPath: "position")  positionAnimation.path = path.cgPath  positionAnimation.isRemovedOnCompletion = false  positionAnimation.fillMode = kCAFillModeForwards    view.layer.add(positionAnimation, forKey: "MoveAlongPath")  </code></pre>    <h3>CATransition</h3>    <p>CATransition 用来进行 view 的转场动画,具体类型有以下四种:</p>    <pre>  <code class="language-objectivec">let kCATransitionFade: String  let kCATransitionMoveIn: String  let kCATransitionPush: String  let kCATransitionReveal: String  </code></pre>    <p>还有一些私有的类型,不推荐使用。</p>    <p>CATransition 是 CAAnimation 的子类,使用方法跟 CABasicAnimation 一致。</p>    <h3>CAAnimationGroup</h3>    <p>如果我想让物体在移动的同时由不透明动画到透明,就可以使用 CAAnimationGroup,把多个动画组合起来,同时添加到动画对象上。</p>    <pre>  <code class="language-objectivec">let animationA = ...  let animationB = ...  let animationC = ...    let animationGroup = CAAnimationGroup()  animationGroup.animations = [animationA, animationB, animationC];  animationGroup.duration = 0.7  animationGroup.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)  animationGroup.isRemovedOnCompletion = false  animationGroup.fillMode = kCAFillModeForwards    view.layer.add(animationGroup, forKey: "GroupAnimation")  </code></pre>    <p>此外,CAAnimationGroup 还可以设置一个 completion block,在所有动画完成时调用。</p>    <p>Core Animation 适用于较为复杂的,有多个中间状态或者包含曲线路径的动画。</p>    <h2>Core Graphics</h2>    <p>如果动画复杂到不能够用改变位置、透明度、大小等属性的组合来完成,就需要使用 Core Graphics 了。</p>    <p>Core Graphics 是一套用来绘图的框架,你可以绘制曲线、填充形状,做任何想做的事情。这个时候采用的就是前面提到的第二种动画方法:每隔一段很短的时间,重新绘制一次动画对象。</p>    <p>注意,采用 Core Graphics 绘制图形是非常消耗性能的,因为绘制工作由 CPU 完成,而且是在主线程上!如果是简单的图形可以使用 CAShapeLayer ,利用 GPU 绘制。</p>    <p>如何来保证「每隔一段很短的时间」呢?iOS 为此提供了 CADisplayLink。它可以被看作是一个特殊的 timer,在系统刷新每一帧的时候,调用开发者设置的回调来重新绘制动画对象。iOS 屏幕的刷新频率是 60帧每秒,也就是每隔约 16.7 毫秒调用一次回调。这也意味着,动画中每一帧的绘制都不应该超过 16.7 毫秒。</p>    <p>那是不是用 NSTimer 也可以达到目的?</p>    <p>并不是的。CADisplayLink 可以保证每次都是在 <strong>屏幕刷新的时刻附近</strong> 来调用回调——也就是说,你的每一帧都有约 16.7 毫秒来绘制。NSTimer 不能保证触发时刻都落在屏幕刷新的时刻附近,有可能你的一帧只有 2 毫秒来绘制。</p>    <p>Core Graphics + CADisplayLink 适用于复杂的,不能使用移动、旋转、形变组合完成的动画。</p>    <h2>大杀器 UIKit Dynamics</h2>    <p>UIKit Dynamics 是随 iOS 7 推出的一套 framework,作用是模拟真实事件的物理定律。苹果声称你可以「声明式」地编写动画,你只需要描述要做什么,而不必说明怎么做,一切都由系统帮你完成。</p>    <h2>Ref</h2>    <ul>     <li><a href="/misc/goto?guid=4959738156473186897" rel="nofollow,noindex">Core Animation Programming Guide</a></li>     <li><a href="/misc/goto?guid=4959738156561452375" rel="nofollow,noindex">Animations Explained</a></li>     <li><a href="/misc/goto?guid=4959738156642092928" rel="nofollow,noindex">Core Animation Essentials</a></li>    </ul>    <p> </p>    <p>来自:http://nixwang.com/2017/02/13/ios-animation/</p>    <p> </p>