iOS动画-定时器动画

前言

任何动画离不开一个重要的概念——时间,CoreAnimation动画创建后在动画后续的不同时间点渲染了不同的图像帧,使值改变前后生成一个过渡的流畅动画

定时器的作用类似于CoreAnimation的操作,在定时器启动后对应的时间点插入回调任务。如果每个回调任务之间的间隔足够短,并在每个任务之间绘制图案,就能达成自制动画的效果。本文分别使用NSTimerCADisplayLink两个定时器来实现不同的动画

关于定时器

iOS开发中有三种常见的定时器:NSTimerCADisplayLink以及GCD Timer,前两个定时器在使用时要加入到某个运行的RunLoop当中,在每个回调时间点会唤醒线程,执行任务。GCD Timer依赖于派发线程,从准确度上而言要强于前两者,但是本文并不涉及这种定时器的使用。

  • NSTimer
    NSTimer是最常使用的定时器,启动后会添加到RunLoop的定时器源中,然后在后续设置好的时间点唤醒RunLoop执行回调。如果在回调时间点遇到了CPU正在执行大量指令时,普遍认为该时间点的任务会被跳过,但实际效果可能与认识有偏差。在iOS10中,NSTimer还存在着不能正常释放引用对象的bug。详细请参考下面的文章链接

  • CADisplayLink
    CADisplayLink比较特殊,它的回调频率保持16.67ms一次,与屏幕的刷新频率一样。与NSTimer相似的地方在于两者都会在回调时唤醒所在的RunLoop,但CADisplayLink会不断处理来自内核的信号,可能导致大量的不必要的资源损耗,因此使用CADisplayLink的时间应当保证尽可能的短暂,具体参考下面的文章链接

两个定时器都能协助我们很好的实现动画效果,更详细的介绍参考iOS10定时消息的改动。下面放上本篇博客的动画效果

声波动画

声波动画参照自支付鸨的咻一咻功能,现在的版本貌似取消了(ps:吐槽一句支付鸨更新之后看个余额都费劲)。从gif图中不难看到动画是由多个图层缩放消失叠加在一起实现的,其中单个缩放消失的动画在我上一篇按钮动画中有提到,基于上篇文章的动画,笔者在点击按钮的时候添加了一个NSTimer用来保证每隔一段时间新增一个动画图层。理论上来说可以将这些动画的CAShapLayer保存起来重复使用,但demo中偷懒,每次回调创建新的图层进行动画

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)
}

通过修改animation.duration来确定同一时间停留在屏幕上的图层数量。另外,由于demo中每次回调创建一个图层,为了避免长时间动画后,视图上保留的CAShapeLayer过多时,在每次动画结束后移除对应的图层。

 open func setValue(_ value: Any?, forKey key: String)

方法可以将图层通过键值对的方式保存在动画对象animation中,并在动画结束时取出图层。iOS10之前所有NSObject的子类都自动遵守了动画协议,但在iOS10中我们需要手动遵守CAAnimationDelegate

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()
        }
    }
}

另外,图层的位置是通过bounds + position来确认的,前者确认图层大小尺寸,后者确认中心点

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
}

弹性动画

认识CoreAnimation一文中展示过类似的弹性动画,这里对CoreAnimation动画的流程进行介绍

  • 判断keyPath对应属性是否为可动画属性,如果否,不执行下一步
  • 根据toValuefromValue计算出动画差值,根据duration属性计算出动画帧数,然后两者计算出每一帧的图层属性
  • 根据fillMode参数判断是否将图层的presentation设置为动画第一帧的图层属性并提交渲染
  • 逐帧设置presentation并渲染
  • 根据autoreverses判断是否逆向执行一次动画
  • 动画结束调用代理对象的animationDidStop方法,根据isRemovedOnCompletion属性判断是否移除动画
  • 如果上一步未移除动画,根据fillMode属性判断是否将图层设置为最后一帧的属性。或者将presentation同步为模型树属性

上面是笔者使用CoreAnimation对流程的大致总结,具体可能还有改动,但基本如此。根据这些步骤,笔者使用CADisplayLink在屏幕刷新时重新绘制图层实现波浪效果,在制作这个动画之前,我们先将波浪动画的gif单独放出来:


中间的弹出速度要快于两边,并且在达到最高点之后来回弹动。用弹簧动画是可以很简单的实现这种弹动效果,但是却没办法帮我们绘制这种效果,即便有人告诉你弹簧的计算公式,然后让你实现效果

对于笔者这样的学渣来说无疑是坑爹。所以为了能准确计算出中间的弹动效果,我们需要一些assistant来帮忙

@IBOutlet private weak var referView: UIView!
@IBOutlet private weak var springView: UIView!

为了不影响动画视觉,这两个view应该设置为hidden或者透明色。每次屏幕刷新时,获取两个视图的presentation的位置,然后绘制出路径,设置到图层上显示

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
}

在用户点击按钮的时候,创建定时器对象,并且给两个assistant添加对应的弹出动画。这里笔者两个弹出都使用了CASpringAnimation弹簧动画,经过多次试验,如果referView只是使用简单的移动动画,整体的弹出效果会有些不自然。只要保证左右两侧的弹动力远低于中间,就能看到很好的效果了

@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
}

其他

使用assistant是一种动画常见的方式,尤其在弹性动画方面更是家常便饭。在开发中CoreAnimation已经能够很好的应付95%的动画效果,合理的结合定时器可以让动效变得更加棒。最后吐槽一下苹果的spring动画,如果你尝试在模拟器上slow animation,很容易就看到苹果的弹性动画回弹时是对称的(⊙﹏⊙)b ,本文demo

上一篇:按钮动画

转载请注明本文作者和转载地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容