你的位置:首页 > 操作系统

[操作系统]iOS核心动画高级技巧之核心动画(三)


iOS核心动画高级技巧之CALayer(一)

iOS核心动画高级技巧之图层变换和专用图层(二)
iOS核心动画高级技巧之核心动画(三)
iOS核心动画高级技巧之性能(四)
iOS核心动画高级技巧之动画总结(五)

 隐式动画

  隐式动画主要作用于CALayer的可动画属性上面,UIView对应的layer是不可以的,只要你改变属性的值,它不是突兀的直接改变过去,而是一个有一个动画的过程,这个时间等属性你可以通过事务(CATransaction)来控制,如果你不自己提供一个事务,它的默认时间是0.25秒,当然这个可动画属性是需要触发的,如果你一上来就设置一个值,可能看不到动画效果.

 1 redLayer = CALayer() 2 redLayer.backgroundColor = UIColor.redColor().CGColor 3 redLayer.frame = CGRectMake(50, 100, 100, 100) 4 self.view.layer.addSublayer(redLayer) 5  6 NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: NSSelectorFromString("animate"), userInfo: nil, repeats: false) 7  8 func animate() { 9   CATransaction.begin()10   CATransaction.setAnimationDuration(12)11   12   var redC = CGFloat(arc4random() % 256 ) / 255.013   var greenC = CGFloat(arc4random() % 256 ) / 255.014   var blueC = CGFloat(arc4random() % 256 ) / 255.015   16   self.redLayer.backgroundColor = UIColor(red: redC, green: greenC, blue: blueC, alpha: 1).CGColor17   18   CATransaction.commit()19 }

  在上面这个transaction中加上一个完成块可以使它在在动画完成的时候做一个0.25秒的默认动画,demo中有两种动画方式都可以(2d和3d)

 1 CATransaction.begin() 2 CATransaction.setAnimationDuration(3) 3 CATransaction.setCompletionBlock { () -> Void in 4 /*3d动画 5 var transform = self.redLayer.transform 6 transform = CATransform3DRotate(transform, CGFloat(M_PI_4), 0, 0, 1) 7 self.redLayer.transform = transform 8 */ 9 //这个是2d的动画10 var transform = self.redLayer.affineTransform()11 transform = CGAffineTransformRotate(transform, CGFloat(M_PI_4))12 self.redLayer.setAffineTransform(transform)

  layer之所以能做隐式动画是因为对应的属性有对应的action,这个action可以通过layer的delegate的代理方法actionforLayer:forkey获得,也可以通过设置layer的actions属性实现,两种方法都之所以UIView没有隐式动画,是因为它对应的layer对应的delegate是它自己,而它的actionforlayer:forkey方法每次都是返回nil,所以它没有对应的action,所以不能做隐式动画,如果你想让一个view有隐式动画的话可以重写它的actionforlayer方法,跟它对应的key返回一个action,这个action的类型是CAtransition类型.另外,在UIView的beginAnimations和commitAnimations方法中间它的actionfoylayer方法会有返回值可以做隐式动画,如果你不想让普通的layer做隐式动画可以调用CATransaction的setDisableActions方法禁止,直接设置actions为nil没什么反应

1 var transition = CATransition()2 transition.type = kCATransitionPush//设置出现方式,默认为fade3 transition.subtype = kCATransitionFromLeft//设置出现方向4 redLayer.actions = ["backgroundColor":transition]

  layer能做隐式动画,但是你获取可动画的属性它的值还是最后设置的值,那是因为它并不是一个layer生成的动画,它有modelLayer一般是返回layer本身,它还有一个presenttationLayer呈现图层,我们设置的值是给modelLayer的,而做动画的是presentationLayer.有两种情况你可能需要用到呈现图层,一种是你需要获得动画过程中layer的位置,另一种是你需要在动画过程中响应用户交互.下面这个demo效果是如果你点击到了方块则变颜色,没点到则方块移动到你点击的位置

 1 redLayer = CALayer() 2 redLayer.backgroundColor = UIColor.redColor().CGColor 3 redLayer.frame = CGRectMake(50, 100, 100, 100) 4 self.view.layer.addSublayer(redLayer) 5  6 override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) { 7   var point = ( touches as NSSet ) .anyObject()?.locationInView(self.view) 8    9   if (self.redLayer.presentationLayer().hitTest(point!) != nil) {10     var redC = CGFloat(arc4random() % 256 ) / 255.011     var greenC = CGFloat(arc4random() % 256 ) / 255.012     var blueC = CGFloat(arc4random() % 256 ) / 255.013     14     self.redLayer.backgroundColor = UIColor(red: redC, green: greenC, blue: blueC, alpha: 1).CGColor15   }else {16     CATransaction.begin()17     CATransaction.setAnimationDuration(2.0)18     self.redLayer.position = point!19     CATransaction.commit()20   }21 }

   显式动画

CABasicAnimation(属性动画) 

  CABasicAnimation动画和隐式动画类似,它可以设置起始和结束值和delegate,下面这个例子和上面的隐式动画类似

 1   redLayer = CALayer() 2   redLayer.backgroundColor = UIColor.redColor().CGColor 3   redLayer.frame = CGRectMake(50, 100, 100, 100) 4   self.view.layer.addSublayer(redLayer) 5    6   NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: NSSelectorFromString("animate"), userInfo: nil, repeats: false) 7 } 8  9 func animate () {10   var redC = CGFloat(arc4random() % 256 ) / 255.011   var greenC = CGFloat(arc4random() % 256 ) / 255.012   var blueC = CGFloat(arc4random() % 256 ) / 255.013   var color = UIColor(red: redC, green: greenC, blue: blueC, alpha: 1).CGColor14   15   var animate = CABasicAnimation()16   animate.duration = 8.017   animate.keyPath = "backgroundColor"18   animate.toValue = color19   animate.delegate = self20   self.redLayer .addAnimation(animate, forKey: nil)21 }22 23 override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {24   CATransaction.begin()25   26   CATransaction.setDisableActions(true)27   var animate = anim as! CABasicAnimation28   self.redLayer.backgroundColor = animate.toValue as! CGColorRef29   CATransaction.commit()30 }

View Code

  注意在代码中,stop的代理方法中的隐式动画我们是禁用的,要不然它会做两次动画,如果是view的layer动画就不需要,因为view的隐式动画默认就禁止了,如果有多个动画需要代理方法,可以在添加动画的时候设置key,在代理方法中通过key获取animate.还有更简便的KVC来获取,animate.setValue(redView,forKey:"redView"),在代理方法中用valueForKey获取

CAKeyframeAnimation(关键帧动画)

  关键帧动画见名知意,你设置values属性中的每一帧的值,然后iOS就会按照你设置的值来做动画,你还可以对应设置它对应的时间,这个很强大后面会说到,现在先说另一个强大的功能,它可以沿着路径来做动画,只要跟它的path属性设置一个CGBezierPathRef类型的值就可以了.其实沿着路径做动画就和设置一个个values的值是一样的,路径也是由一个个position的值的点组成的,沿着路径做动画,物体要跟着路径调整方向,你可以同时跟它的方向做rotate动画,但是可能引起冲突或其它问题,它又一个rotationMode 直接设置成rotateAuto就可以了.demo很简单,列举了.

  还有一点需要注意的是:在做transform做动画时用transform.rotation等来做动画会好很多,一方面可以用byValue来设置值,另一方面position/scale/rotation也不会有冲突.你直接设置transform.position的值其实本身是没有用的,因为它就没有这个属性,只是iOS内部用KVC把transform.position的值用CAValueFuction转换成了transform对应的矩阵值

CAAnimationGroup(组动画)

  CAAnimationGroup动画组也是顾名思义可以做一个动画组,只要跟它的animations属性设置一个动画数组就行了,每个数组项的值是一个basicAnimation或者keyframeAnimation

CATransition(过渡动画)

  这里首先需要注意的是CATransition是一个动画,而CATransaction是一个动画事务,都是核心动画里面的两个概念,所以要区别开来.应该说CATransition是核心动画里最简单最好用但是最容易被人忽略的动画了,下面跟imageview设置image的时候给一个fadein的动画,你看是多简单.

 1  NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: NSSelectorFromString("animate"), userInfo: nil, repeats: false) 2    3   self.img = UIImageView(image: UIImage(named: "111.png")) 4   self.view.addSubview(self.img) 5 } 6  7 func animate () { 8   var transition = CATransition() 9   transition.type = kCATransitionFade10   self.img.layer .addAnimation(transition, forKey: nil)11   12   self.img.image = UIImage(named: "222.png")13 }

 

  对于CAtransition来说,在自己创建的layer中,它是默认加上的,而在view关联的屠城,它是被禁用的,毕竟它还是得提供你一种简单的正常的设置属性的方法,它是对整个图层树都有效,如果添加了transition会跟它的子图层都加上整个效果,比如跟tabbar切换的渐变效果(大多数VC切换都可以在代理方法或其它方法中写动画)

 1   var root = UITabBarController() 2   root.viewControllers = [ViewController(),OneViewController()] 3   root.delegate = self 4   self.rootVC = root 5   6   self.window?.rootViewController = root 7   self.window?.makeKeyAndVisible() 8    9   return true10 }11  func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {12   var transition = CATransition()13   transition.type = kCATransitionFade14   transition.duration = 315   self.rootVC.view.layer.addAnimation(transition, forKey: nil)16 }

  UiView提供了transitionFromView的方法,如果只跟一个view做动画直接可以用它和加上transition动画的效果是一样的,但是一般它对子图层不起作用,在动画运行的过程中可以remove掉动画,一个按钮添加动画,一个按钮控制删除动画,而动画是使用的byValue,所以效果就是看起来是暂停.

  下面的代码做的是暂停动画的功能,在暂停的时候更新model树为presentlayer的值.

 1   redLayer = CALayer() 2   redLayer.backgroundColor = UIColor.redColor().CGColor 3   redLayer.frame = CGRectMake(50, 100, 100, 100) 4   self.view.layer.addSublayer(redLayer) 5    6   var beginBtn = UIButton(frame: CGRectMake(50, 300, 100, 30)) 7   beginBtn.setTitle("开始", forState: UIControlState.Normal) 8   beginBtn.addTarget(self, action: "begin", forControlEvents: UIControlEvents.TouchUpInside) 9   self.view.addSubview(beginBtn)10   11   var stopBtn = UIButton(frame: CGRectMake(200, 300, 100, 30))12   stopBtn.setTitle("暂停", forState: UIControlState.Normal)13   stopBtn.addTarget(self, action: "stop", forControlEvents: UIControlEvents.TouchUpInside)14   self.view.addSubview(stopBtn)15 }16 17 func begin() {18   var animate = CABasicAnimation()19   animate.keyPath = "transform.rotation"20   animate.duration = 2.021   animate.byValue = CGFloat(M_PI / 4)22   animate.delegate = self23 //    animate.fillMode = kCAFillModeForwards24 //    animate.removedOnCompletion = false25   self.redLayer .addAnimation(animate, forKey: "animate")26 }27 func stop() {28   self.redLayer.transform = self.redLayer.presentationLayer().transform29   self.redLayer.removeAnimationForKey("animate")30 }

View Code

图层时间

  beginTime/speed/timeoffset 三个动画属性都是相对概念,分别表示对应duration的开始时间,动画的速度(会改变动画结束的时间),让动画瞬间快进到某一点.下面是让动画暂停的第二种方法.

 1   redLayer = CALayer() 2   redLayer.backgroundColor = UIColor.redColor().CGColor 3   redLayer.frame = CGRectMake(50, 100, 100, 100) 4   self.view.layer.addSublayer(redLayer) 5    6   var beginBtn = UIButton(frame: CGRectMake(50, 300, 100, 30)) 7   beginBtn.setTitle("开始", forState: UIControlState.Normal) 8   beginBtn.addTarget(self, action: "resumeLayer", forControlEvents: UIControlEvents.TouchUpInside) 9   self.view.addSubview(beginBtn)10   11   var stopBtn = UIButton(frame: CGRectMake(200, 300, 100, 30))12   stopBtn.setTitle("暂停", forState: UIControlState.Normal)13   stopBtn.addTarget(self, action: "pauseLayer", forControlEvents: UIControlEvents.TouchUpInside)14   self.view.addSubview(stopBtn)15   16   self.pauseTime = 0.017   self.startAnimate()18 }19 20 func startAnimate () {21   var animate = CABasicAnimation()22   animate.keyPath = "transform.rotation"23   animate.duration = 20.024   animate.byValue = CGFloat(M_PI * 10)25   animate.delegate = self26   self.redLayer .addAnimation(animate, forKey: "animate")27 }28 29 func pauseLayer () {30   //获取当前动画的时间31   self.pauseTime = self.redLayer.convertTime(CACurrentMediaTime(), fromLayer: nil)32   //停止运动33   self.redLayer.speed = 0.034   //设置它呆在目前的状态不变,不然因为speed为0,layer变回了最初动画的值35   self.redLayer.timeOffset = self.pauseTime36 }37 38 func resumeLayer () {39   //获取暂停开始的时间40   var pausedTime = self.pauseTime41   //设置速度timeOffset等为正常值42   self.redLayer.speed = 1.043   self.redLayer.timeOffset = 0.044   self.redLayer.beginTime = 0.045   46   var timeSincePause = self.redLayer.convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime47  //设置开始时间为初始和现在的时间差48   self.redLayer.beginTime = timeSincePause49 }

View Code

  removeOnCompletion 设置为no,将会在动画结束后仍然保持前一步的状态,然后把fillMode设置成modeforwards就可以让它动画执行后保持在原界面,不用回到初始值.

动画速度

  设置CAAnimationtimingFunction属性可以控制动画的速度,而CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,它需要values这个数组长度和它的长度比它大1,这样就可以控制每一段动画的速度了。timingFunction的值是CAMediaTimingFunction类型的,它还有一个初始化方法可以自定义时间曲线,它是一个三次贝塞尔缓冲函数,可以通过起始点、终点、两个控制点来初始化,默认的CAMediaTimingFunction值其实可以这样初始化得来。

  对于设置values数组长度为5,timingFunctions数组长度也为4,相当于做了四段关键帧动画,而这每段动画的时间曲线由timingFunction的每个项来控制,而这4段动画的时间则是均分during的时间,你还可以设置keyTimes的值,它也是一个数组,它和timingFunction的数组长度一样,控制每段动画的时间。用这个特性几乎可以做大多数规则动画了。

  通过上面的方法我们已经基本能够做任何动画了,但是我要做一个弹簧效果或者球落地的效果都需要设置多个values和timingFunction和keytimes,而且需要精确计算还效果不逼真。所以我们直接可以设置它的values值就是动画的路径,由于时间是平均的,而values值不同,最后就会产生速度上的差异,就会产生绚丽的效果。产生路径的函数这个网站有提供:http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm

定时器

  上面我们说过设置一系列的values,然后根据时间间隔相同来做动画,既然添加这个关键帧动画只是让它在每个相同的时间内运动一定的已知的距离,这直接用NSTime就可以解决了,每一次遍历values数组的值,让它位移到那就可以了。其实这就是核心动画的本质,它存在一个问题就是NSTime是添加到NSRunloop中的,而iOS每个线程管理着一个NSRUNloop,它的每次事件是添加到任务列表里去,权限比较低,如果一个屏幕有很多动画的话就有可能有延迟,然后就可能出现卡顿的现象,动画就不流畅,你还可以用CADisplayLink代替它,它和NSTime的原理是一样的,都是 一定时间执行一个方法,而且他们可以设置自己的优先级,而不同的是NSTime是被添加到任务列表中,它在屏幕刷新的时候就一定会调用一次,屏幕刷新率一般是60次每秒,而NSTime是添加到任务列表中,它会被其它任务阻塞,在主线程的任务有 处理触摸事件、发送和接受网络数据包、执行使用gcd的代码、处理计时器行为、屏幕重绘等。FB的POP框架也是使用了CADisplayLink。