iOS动画-定时器动画

fi867989 7年前
   <h2>前言</h2>    <p>任何动画离不开一个重要的概念——时间, CoreAnimation 动画创建后在动画后续的不同时间点渲染了不同的图像帧,使值改变前后生成一个过渡的流畅动画</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ad453703d73f1e3974b5cc20f0039874.jpg"></p>    <p>定时器的作用类似于 CoreAnimation 的操作,在定时器启动后对应的时间点插入回调任务。如果每个回调任务之间的间隔足够短,并在每个任务之间绘制图案,就能达成自制动画的效果。本文分别使用 NSTimer 和 CADisplayLink 两个定时器来实现不同的动画</p>    <h2>关于定时器</h2>    <p>iOS开发中有三种常见的定时器: NSTimer 、 CADisplayLink 以及 GCD Timer ,前两个定时器在使用时要加入到某个运行的 RunLoop 当中,在每个回调时间点会唤醒线程,执行任务。 GCD Timer 依赖于派发线程,从准确度上而言要强于前两者,但是本文并不涉及这种定时器的使用。</p>    <ul>     <li> <p>NSTimer</p> <p>NSTimer 是最常使用的定时器,启动后会添加到 RunLoop 的定时器源中,然后在后续设置好的时间点唤醒 RunLoop 执行回调。如果在回调时间点遇到了CPU正在执行大量指令时,普遍认为该时间点的任务会被跳过,但实际效果可能与认识有偏差。在iOS10中, NSTimer 还存在着不能正常释放引用对象的bug。详细请参考下面的文章链接</p> </li>     <li> <p>CADisplayLink</p> <p>CADisplayLink 比较特殊,它的回调频率保持 16.67ms 一次,与屏幕的刷新频率一样。与 NSTimer 相似的地方在于两者都会在回调时唤醒所在的 RunLoop ,但 CADisplayLink 会不断处理来自内核的信号,可能导致大量的不必要的资源损耗,因此使用 CADisplayLink 的时间应当保证尽可能的短暂,具体参考下面的文章链接</p> </li>    </ul>    <p>两个定时器都能协助我们很好的实现动画效果,更详细的介绍参考iOS10定时消息的改动。下面放上本篇博客的动画效果</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6fbbba6d50a68f1f5db5d98c63a7d362.gif"></p>    <h2>声波动画</h2>    <p>声波动画参照自支付鸨的 咻一咻 功能,现在的版本貌似取消了(ps:吐槽一句支付鸨更新之后看个余额都费劲)。从gif图中不难看到动画是由多个图层缩放消失叠加在一起实现的,其中单个缩放消失的动画在我上一篇按钮动画中有提到,基于上篇文章的动画,笔者在点击按钮的时候添加了一个 NSTimer 用来保证每隔一段时间新增一个动画图层。理论上来说可以将这些动画的 CAShapLayer 保存起来重复使用,但demo中偷懒,每次回调创建新的图层进行动画</p>    <pre>  <code class="language-objectivec">let twinkleInteval = 0.6  @IBAction func signIn(_ sender: UIButton) {      self.timer = Timer(timeInterval: twinkleInteval, repeats: true, block: { [unowned self] (timer) in          let frame = self.signInButton.frame          let layer = self.roundLayer(with: frame)          self.view.layer.insertSublayer(layer, below: self.signInButton.layer)              self.twinkle(layer: layer)      })  }    func twinkle(layer: CAShapeLayer) {      let scale = CABasicAnimation(keyPath: "transform")      scale.toValue = NSValue(caTransform3D: CATransform3DMakeScale(4, 4, 1))        let opacity = CABasicAnimation(keyPath: "opacity")      opacity.fromValue = NSNumber(floatLiteral: 0.75)      opacity.toValue = NSNumber(floatLiteral: 0)        let animation = CAAnimationGroup()      animation.animations = [scale, opacity]      animation.duration = twinkleInteval * 3      animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)      animation.setValue(layer, forKey: layerKey)      animation.delegate = self      layer.opacity = 0      layer.add(animation, forKey: nil)  }</code></pre>    <p>通过修改 animation.duration 来确定同一时间停留在屏幕上的图层数量。另外,由于demo中每次回调创建一个图层,为了避免长时间动画后,视图上保留的 CAShapeLayer 过多时,在每次动画结束后移除对应的图层。</p>    <pre>  <code class="language-objectivec">open func setValue(_ value: Any?, forKey key: String)</code></pre>    <p>方法可以将图层通过键值对的方式保存在动画对象 animation 中,并在动画结束时取出图层。iOS10之前所有 NSObject 的子类都自动遵守了动画协议,但在iOS10中我们需要手动遵守 CAAnimationDelegate</p>    <pre>  <code class="language-objectivec">let layerKey = "layerKey"  extension XiuXiuViewController, CAAnimationDelegate {      func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {          if let layer: CALayer = anim.value(forKey: layerKey) as? CALayer {              layer.removeFromSuperlayer()          }      }  }</code></pre>    <p>另外,图层的位置是通过 bounds + position 来确认的,前者确认图层大小尺寸,后者确认中心点</p>    <pre>  <code class="language-objectivec">func roundLayer(with frame: CGRect) -> CAShapeLayer {      let layer = CAShapeLayer()      layer.path = UIBezierPath(roundedRect: frame, cornerRadius: frame.height / 2).cgPath      layer.bounds = frame      layer.position = signInButton.center      layer.fillColor = UIColor(colorLiteralRed: 34/255.0, green: 192/255.0, blue: 100/255.0, alpha: 1).cgColor      return layer  }</code></pre>    <h2>弹性动画</h2>    <p>在认识CoreAnimation一文中展示过类似的弹性动画,这里对 CoreAnimation 动画的流程进行介绍</p>    <ul>     <li>判断 keyPath 对应属性是否为可动画属性,如果否,不执行下一步</li>     <li>根据 toValue 和 fromValue 计算出动画差值,根据 duration 属性计算出动画帧数,然后两者计算出每一帧的图层属性</li>     <li>根据 fillMode 参数判断是否将图层的 presentation 设置为动画第一帧的图层属性并提交渲染</li>     <li>逐帧设置 presentation 并渲染</li>     <li>根据 autoreverses 判断是否逆向执行一次动画</li>     <li>动画结束调用代理对象的 animationDidStop 方法,根据 isRemovedOnCompletion 属性判断是否移除动画</li>     <li>如果上一步未移除动画,根据 fillMode 属性判断是否将图层设置为最后一帧的属性。或者将 presentation 同步为模型树属性</li>    </ul>    <p>上面是笔者使用 CoreAnimation 对流程的大致总结,具体可能还有改动,但基本如此。根据这些步骤,笔者使用 CADisplayLink 在屏幕刷新时重新绘制图层实现波浪效果,在制作这个动画之前,我们先将波浪动画的gif单独放出来:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/dbced0ab73cbab80b212396530c0e959.gif"></p>    <p>中间的弹出速度要快于两边,并且在达到最高点之后来回弹动。用弹簧动画是可以很简单的实现这种弹动效果,但是却没办法帮我们绘制这种效果,即便有人告诉你弹簧的计算公式,然后让你实现效果</p>    <p><img src="https://simg.open-open.com/show/04e735c1276102126b85d47af0315d54.jpg"></p>    <p>对于笔者这样的学渣来说无疑是坑爹。所以为了能准确计算出中间的弹动效果,我们需要一些 assistant 来帮忙</p>    <pre>  <code class="language-objectivec">@IBOutlet private weak var referView: UIView!  @IBOutlet private weak var springView: UIView!</code></pre>    <p>为了不影响动画视觉,这两个 view 应该设置为hidden或者透明色。每次屏幕刷新时,获取两个视图的 presentation 的位置,然后绘制出路径,设置到图层上显示</p>    <pre>  <code class="language-objectivec">func animateWave() {      let path = CGMutablePath()      path.move(to: .zero)      path.addLine(to: CGPoint(x: view.frame.width, y: 0))        let controlY = springView.layer.presentation()?.position.y      let referY = referView.layer.presentation()?.position.y        path.addLine(to: CGPoint(x: view.frame.width, y: referY!))      path.addQuadCurve(to: CGPoint(x: 0, y: referY!), control: CGPoint(x: view.frame.width / 2, y: controlY!))      path.addLine(to: .zero)      layer.path = path  }</code></pre>    <p>在用户点击按钮的时候,创建定时器对象,并且给两个 assistant 添加对应的弹出动画。这里笔者两个弹出都使用了 CASpringAnimation 弹簧动画,经过多次试验,如果 referView 只是使用简单的移动动画,整体的弹出效果会有些不自然。只要保证左右两侧的弹动力远低于中间,就能看到很好的效果了</p>    <pre>  <code class="language-objectivec">@IBAction func animate(_ sender: Any) {      let target = CGPoint(x: 0, y: view.center.y / 2)      referView.layer.position = target      springView.layer.position = target        displayLink?.invalidate()      displayLink = CADisplayLink(target: self, selector: #selector(animateWave))      displayLink?.add(to: RunLoop.current, forMode: .commonModes)        let move = CASpringAnimation(keyPath: "position")      move.fromValue = NSValue(cgPoint: .zero)      move.toValue = NSValue(cgPoint: target)      move.duration = 2        let spring = CASpringAnimation(keyPath: "position")      spring.fromValue = NSValue(cgPoint: .zero)      spring.toValue = NSValue(cgPoint: target)      spring.duration = 2      spring.damping = 7        referView.layer.add(move, forKey: nil)      springView.layer.add(spring, forKey: nil)      referView.layer.position = target      springView.layer.position = target  }</code></pre>    <h2>其他</h2>    <p>使用 assistant 是一种动画常见的方式,尤其在弹性动画方面更是家常便饭。在开发中 CoreAnimation 已经能够很好的应付 95% 的动画效果,合理的结合定时器可以让动效变得更加棒。最后吐槽一下苹果的 spring 动画,如果你尝试在模拟器上 slow animation ,很容易就看到苹果的弹性动画回弹时是</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/af5337ddfc00</p>    <p> </p>