iOS开发之让你的应用“动”起来(swift)

http://www.cnblogs.com/kenshincui/p/3972100.html

--iOS核心动画

概览

在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看到iOS中如何使用图层精简非交互式绘图,如何通过核心动画创建基础动画、关键帧动画、动画组、转场动画,如何通过UIView的装饰方法对这些动画操作进行简化等。在今天的文章里您可以看到动画操作在iOS中是如何简单和高效,很多原来想做但是苦于没有思路的动画在iOS中将变得越发简单:

  1. CALayer
    1. CALayer简介
    2. CALayer常用属性
    3. CALayer绘图
  2. Core Animation
    1. 基础动画
    2. 关键帧动画
    3. 动画组
    4. 转场动画
    5. 逐帧动画
  3. UIView动画封装
    1. 基础动画
    2. 关键帧动画
    3. 转场动画
  4. UITouch
  5. CGContextTranslateCTM 用法
  6. CGContext小记

CALayer

CALayer简介

在使用Core Animation开发动画的本质就是将CALayer中的内容转化为位图从而供硬件操作,所以要熟练掌握动画操作必须先来熟悉CALayer。

iOS开发之让你的应用“动”起来(swift)_第1张图片

CALayer常用属性

在iOS中CALayer的设计主要是了为了内容展示和动画操作,CALayer本身并不包含在UIKit中,它不能响应事件。由于CALayer在设计之初就考虑它的动画操作功能,CALayer很多属性在修改时都能形成动画效果,这种属性称为“隐式动画属性”。但是对于UIView的根图层而言属性的修改并不形成动画效果,因为很多情况下根图层更多的充当容器的做用,如果它的属性变动形成动画效果会直接影响子图层。另外,UIView的根图层创建工作完全由iOS负责完成,无法重新创建,但是可以往根图层中添加子图层或移除子图层。

下表列出了CALayer常用的属性:

属性 说明 是否支持隐式动画
anchorPoint 和中心点position重合的一个点,称为“锚点”,锚点的描述是相对于x、y位置比例而言的默认在图像中心点(0.5,0.5)的位置
backgroundColor 图层背景颜色
borderColor 边框颜色
borderWidth 边框宽度
bounds 图层大小
contents 图层显示内容,例如可以将图片作为图层内容显示
contentsRect 图层显示内容的大小和位置
cornerRadius 圆角半径
doubleSided 图层背面是否显示,默认为YES
frame 图层大小和位置,不支持隐式动画,所以CALayer中很少使用frame,通常使用bounds和position代替
hidden 是否隐藏
mask 图层蒙版
maskToBounds 子图层是否剪切图层边界,默认为NO
opacity 透明度 ,类似于UIView的alpha
position 图层中心点位置,类似于UIView的center
shadowColor 阴影颜色
shadowOffset 阴影偏移量
shadowOpacity 阴影透明度,注意默认为0,如果设置阴影必须设置此属性
shadowPath 阴影的形状
shadowRadius 阴影模糊半径
sublayers 子图层
sublayerTransform 子图层形变
transform 图层形变
    • 隐式属性动画的本质是这些属性的变动默认隐含了CABasicAnimation动画实现,详情大家可以参照Xcode帮助文档中“Animatable Properties”一节。
    • 在CALayer中很少使用frame属性,因为frame本身不支持动画效果,通常使用bounds和position代替。
    • CALayer中透明度使用opacity表示而不是alpha;中心点使用position表示而不是center。
    • anchorPoint属性是图层的锚点,范围在(0~1,0~1)表示在x、y轴的比例,这个点永远可以同position(中心点)重合,用图片的bounds确定其大小,当图层中心点固定position后,调整anchorPoint即可达到调整图层显示位置的作用(因为它永远和position重合)(position未必一定在图片的中心位置)
    import UIKit
    
    class ViewController: UIViewController {
        var size = UIScreen.mainScreen().bounds
        var WIDTH: CGFloat = 50
        override func viewDidLoad() {
            
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
            self.view.backgroundColor = UIColor.whiteColor()
            
            var layer =  CALayer()
            layer.backgroundColor = UIColor(red: 0, green: 146.0 / 255.0, blue: 1.0, alpha: 1.0).CGColor
            layer.position = CGPointMake(size.width/2, size.height/2)
            layer.bounds = CGRectMake(0, 0, WIDTH,WIDTH)
            layer.cornerRadius = WIDTH / 2
            layer.shadowColor = UIColor.greenColor().CGColor
            layer.shadowOffset = CGSizeMake(1,2)
            layer.opacity = 0.9
            layer.borderWidth = 1
            layer.borderColor = UIColor.redColor().CGColor
            self.view.layer.addSublayer(layer)
    
            let times = dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC))
            dispatch_after(times, dispatch_get_main_queue()){
                for i in 0...10 {
                    let x = CGFloat(i) / 10.0
                    for j in 0...10000{
                    }
                    layer.anchorPoint = CGPointMake (x,0.2)
                    layer.needsDisplay()
                }
            }
            
        }
        override func touchesEnded(touches:NSSet, withEvent event:UIEvent) {
            var touch = touches.anyObject() as UITouch
            var layer = self.view.layer.sublayers[0] as CALayer
            var width=layer.bounds.size.width
            if width == WIDTH {
                width = WIDTH * 4
            }else{
                width = WIDTH
            }
            layer.bounds = CGRectMake(0, 0, width, width)
            layer.position = touch.locationInView(self.view)
            layer.cornerRadius=width/2
        }
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    }

运行效果:

iOS开发之让你的应用“动”起来(swift)_第2张图片

CALayer绘图

图层绘图有两种方法,不管使用哪种方法绘制完必须调用图层的setNeedDisplay方法(注意是图层的方法,不是UIView的方法,前面我们介绍过UIView也有此方法)

  1. 通过图层代理drawLayer: inContext:方法绘制
  2. 通过自定义图层drawInContext:方法绘制

使用代理方法绘图

通过代理方法进行图层绘图只要指定图层的代理,然后在代理对象中重写-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx方法即可。需要注意这个方法虽然是代理方法但是不用手动实现CALayerDelegate,因为CALayer定义中给NSObject做了分类扩展,所有的NSObject都包含这个方法。另外设置完代理后必须要调用图层的setNeedDisplay方法,否则绘制的内容无法显示。

下面的代码演示了在一个自定义图层绘制一张图像并将图像设置成圆形,这种效果在很多应用中很常见,如最新版的手机QQ头像就是这种效果:

import UIKit

class ViewController: UIViewController {
    var size = UIScreen.mainScreen().bounds
    var WIDTH: CGFloat = 150
    override func viewDidLoad() {
        
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        self.view.backgroundColor = UIColor.whiteColor()
        
        var layer =  CALayer()
        layer.backgroundColor = UIColor(red: 0, green: 146.0 / 255.0, blue: 1.0, alpha: 1.0).CGColor
        layer.position = CGPointMake(size.width/2, size.height/2)
        layer.bounds = CGRectMake(0, 0, WIDTH,WIDTH)
        layer.cornerRadius = WIDTH / 2
        layer.masksToBounds = true
        layer.borderWidth = 2
        layer.borderColor = UIColor.redColor().CGColor
        self.view.layer.addSublayer(layer)
        layer.delegate = self
        //调用图层setNeedDisplay,否则代理方法不会被调用
        layer.setNeedsDisplay()
    }
    override func drawLayer(layer: CALayer!, inContext ctx: CGContext!) {
        CGContextSaveGState(ctx)
        //图形上下文形变,解决图片倒立的问题
        CGContextScaleCTM(ctx, 1, -1)
        CGContextTranslateCTM(ctx, 0, -WIDTH)
        var image=UIImage(named:"599750_1286086064SSce.jpg")
        //注意这个位置是相对于图层而言的不是屏幕
        CGContextDrawImage(ctx, CGRectMake(0, 0, WIDTH, WIDTH), image!.CGImage) //加一个矩形图层,用图像填充
        //CGContextFillRect(ctx, CGRectMake(0, 0, 100, 100)) //加一个矩形图层
        CGContextRestoreGState(ctx)
    }
    override func touchesEnded(touches:NSSet, withEvent event:UIEvent) {
        var touch = touches.anyObject() as UITouch
        var layer = self.view.layer.sublayers[0] as CALayer
        var width=layer.bounds.size.width
        if width == WIDTH {
            width = WIDTH * 4
        }else{
            width = WIDTH
        }
        layer.bounds = CGRectMake(0, 0, width, width)
        layer.position = touch.locationInView(self.view)
        layer.cornerRadius=width/2
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

 

运行效果:

iOS开发之让你的应用“动”起来(swift)_第3张图片  iOS开发之让你的应用“动”起来(swift)_第4张图片

使用代理方法绘制图形、图像时在drawLayer:inContext:方法中可以通过事件参数获得绘制的图层和图形上下文。在这个方法中绘图时所有的位置都是相对于图层而言的,图形上下文指的也是当前图层的图形上下文。

需要注意的是上面代码中绘制图片圆形裁切效果时如果不设置masksToBounds是无法显示圆形,但是对于其他图形却没有这个限制。原因就是当绘制一张图片到图层上的时候会重新创建一个图层添加到当前图层,这样一来如果设置了圆角之后虽然底图层有圆角效果,但是子图层还是矩形,只有设置了masksToBounds为YES让子图层按底图层剪切才能显示圆角效果。同样的,有些朋友经常在网上提问说为什么使用UIImageView的layer设置圆角后图片无法显示圆角,只有设置masksToBounds才能出现效果,也是类似的问题。

扩展1--带阴影效果的圆形图片裁切

如果设置了masksToBounds=true之后确实可以显示图片圆角效果,但遗憾的是设置了这个属性之后就无法设置阴影效果。因为masksToBounds=true就意味着外边框不能显示,而阴影恰恰作为外边框绘制的,这样两个设置就产生了矛盾。要解决这个问题不妨换个思路:使用两个大小一样的图层,下面的图层负责绘制阴影,上面的图层用来显示图片。

 
 
import UIKit

class ViewController: UIViewController {
    var size = UIScreen.mainScreen().bounds
    var WIDTH: CGFloat = 150
    override func viewDidLoad() {
        
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        self.view.backgroundColor = UIColor.whiteColor()
        var bounds = CGRectMake(0, 0, WIDTH,WIDTH)
        var position = CGPointMake(size.width/2, size.height/2)
        var cornerRadius = WIDTH / 2
        var borderWidth:CGFloat = 2
        
        //阴影图层
        var layerShadow = CALayer()
        layerShadow.bounds = bounds
        layerShadow.position = position
        layerShadow.cornerRadius = cornerRadius
        layerShadow.shadowColor = UIColor.grayColor().CGColor
        layerShadow.shadowOffset = CGSizeMake(2, 1)
        layerShadow.shadowOpacity = 1
        layerShadow.borderColor = UIColor.whiteColor().CGColor
        layerShadow.borderWidth = borderWidth
        self.view.layer.addSublayer(layerShadow)
        
        //容器图层
        var layer =  CALayer()
        layer.backgroundColor = UIColor(red: 0, green: 146.0 / 255.0, blue: 1.0, alpha: 1.0).CGColor
        layer.position = position
        layer.bounds = bounds
        layer.cornerRadius = cornerRadius
        layer.masksToBounds = true
        layer.borderWidth = borderWidth
        layer.borderColor = UIColor.whiteColor().CGColor
        
        self.view.layer.addSublayer(layer)
        layer.delegate = self
        //调用图层setNeedDisplay,否则代理方法不会被调用
        layer.setNeedsDisplay()
    }
    override func drawLayer(layer: CALayer!, inContext ctx: CGContext!) {
        CGContextSaveGState(ctx)
        //图形上下文形变,解决图片倒立的问题
        CGContextScaleCTM(ctx, 1, -1)
        CGContextTranslateCTM(ctx, 0, -WIDTH)
        var image=UIImage(named:"599750_1286086064SSce.jpg")
        //注意这个位置是相对于图层而言的不是屏幕
        CGContextDrawImage(ctx, CGRectMake(0, 0, WIDTH, WIDTH), image!.CGImage)
        //    CGContextFillRect(ctx, CGRectMake(0, 0, 100, 100));
        //    CGContextDrawPath(ctx, kCGPathFillStroke);
        CGContextRestoreGState(ctx)
    }
    override func touchesEnded(touches:NSSet, withEvent event:UIEvent) {
        var touch = touches.anyObject() as UITouch
        var layer = self.view.layer.sublayers[0] as CALayer
        var width=layer.bounds.size.width
        if width == WIDTH {
            width = WIDTH * 4
        }else{
            width = WIDTH
        }
        layer.bounds = CGRectMake(0, 0, width, width)
        layer.position = touch.locationInView(self.view)
        layer.cornerRadius=width/2
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
 
 

运行效果:

iOS开发之让你的应用“动”起来(swift)_第5张图片    iOS开发之让你的应用“动”起来(swift)_第6张图片 

CGContextSaveGState与CGContextRestoreGState的作用

使用Quartz时涉及到一个图形上下文,其中图形上下文中包含一个保存过的图形状态堆栈。在Quartz创建图形上下文时,该堆栈是空的。CGContextSaveGState函数的作用是将当前图形状态推入堆栈。之后,您对图形状态所做的修改会影响随后的描画操作,但不影响存储在堆栈中的拷贝。在修改完成后,您可以通过CGContextRestoreGState函数把堆栈顶部的状态弹出,返回到之前的图形状态。这种推入和弹出的方式是回到之前图形状态的快速方法,避免逐个撤消所有的状态修改;这也是将某些状态(比如裁剪路径)恢复到原有设置的唯一方式

扩展2--图层的形变

从上面代码中大家不难发现使用Core Graphics绘制图片时会倒立显示,对图层的图形上下文进行了反转,三个变形方法:

        CGContextScaleCTM(ctx, 0.5, -1) //缩放 (横坐标压缩)

        CGContextTranslateCTM(ctx, 0, -WIDTH) //移位(取反)

        CGContextRotateCTM(ctx, CGFloat(M_PI) / 180.0 * 10.0) //旋转(逆时针旋转10度)

iOS开发之让你的应用“动”起来(swift)_第7张图片

靠图形上下文还无法绕x轴旋转。其实可以控制图层直接旋转而不用借助于图形上下文的形变操作,只需设置图层的transform属性即可。需要注意的是transform是CATransform3D类型,形变可以在三个维度上进行,使用方法和前面介绍的二维形变是类似的,而且都有对应的形变设置方法(如:CATransform3DMakeTranslation()、CATransform3DMakeScale()、CATransform3DMakeRotation())。下面的代码通过CATransform3DMakeRotation()方法在x轴旋转180度解决倒立问题:

 
 
import UIKit

class ViewController: UIViewController {
    var size = UIScreen.mainScreen().bounds
    var WIDTH: CGFloat = 150
    override func viewDidLoad() {
        
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        self.view.backgroundColor = UIColor.whiteColor()
        var bounds = CGRectMake(0, 0, WIDTH,WIDTH)
        var position = CGPointMake(size.width/2, size.height/2)
        var cornerRadius = WIDTH / 2
        var borderWidth:CGFloat = 2
        
        //阴影图层
        var layerShadow = CALayer()
        layerShadow.bounds = bounds
        layerShadow.position = position
        layerShadow.cornerRadius = cornerRadius
        layerShadow.shadowColor = UIColor.grayColor().CGColor
        layerShadow.shadowOffset = CGSizeMake(2, 1)
        layerShadow.shadowOpacity = 1
        layerShadow.borderColor = UIColor.whiteColor().CGColor
        layerShadow.borderWidth = borderWidth
        self.view.layer.addSublayer(layerShadow)
        
        //容器图层
        var layer =  CALayer()
        layer.backgroundColor = UIColor(red: 0, green: 146.0 / 255.0, blue: 1.0, alpha: 1.0).CGColor
        layer.position = position
        layer.bounds = bounds
        layer.cornerRadius = cornerRadius
        layer.masksToBounds = true
        layer.borderWidth = borderWidth
        layer.borderColor = UIColor.whiteColor().CGColor
        //----
        layer.transform = CATransform3DMakeRotation(CGFloat(M_PI), 1, 0, 0)//angle,x,y,z
        //---
        self.view.layer.addSublayer(layer)
        layer.delegate = self
        //调用图层setNeedDisplay,否则代理方法不会被调用
        layer.setNeedsDisplay()
    }
    override func drawLayer(layer: CALayer!, inContext ctx: CGContext!) {
        var image=UIImage(named:"599750_1286086064SSce.jpg")
        CGContextDrawImage(ctx, CGRectMake(0, 0, WIDTH, WIDTH), image!.CGImage) //加一个矩形图层,用图像填充
    }
    override func touchesEnded(touches:NSSet, withEvent event:UIEvent) {
        var touch = touches.anyObject() as UITouch
        var layer = self.view.layer.sublayers[0] as CALayer
        var width=layer.bounds.size.width
        if width == WIDTH {
            width = WIDTH * 4
        }else{
            width = WIDTH
        }
        layer.bounds = CGRectMake(0, 0, width, width)
        layer.position = touch.locationInView(self.view)
        layer.cornerRadius=width/2
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
 
 

事实上如果仅仅就显示一张图片在图层中当然没有必要那么麻烦,直接设置图层contents就可以了,不牵涉到绘图也就没有倒立的问题了。

 
 
import UIKit

class ViewController: UIViewController {
    var size = UIScreen.mainScreen().bounds
    var WIDTH: CGFloat = 150
    override func viewDidLoad() {
        
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        self.view.backgroundColor = UIColor.whiteColor()
        var bounds = CGRectMake(0, 0, WIDTH,WIDTH)
        var position = CGPointMake(size.width/2, size.height/2)
        var cornerRadius = WIDTH / 2
        var borderWidth:CGFloat = 2
        
        //阴影图层
        var layerShadow = CALayer()
        layerShadow.bounds = bounds
        layerShadow.position = position
        layerShadow.cornerRadius = cornerRadius
        layerShadow.shadowColor = UIColor.grayColor().CGColor
        layerShadow.shadowOffset = CGSizeMake(2, 1)
        layerShadow.shadowOpacity = 1
        layerShadow.borderColor = UIColor.whiteColor().CGColor
        layerShadow.borderWidth = borderWidth
        self.view.layer.addSublayer(layerShadow)
        
        //容器图层
        var layer =  CALayer()
        layer.backgroundColor = UIColor(red: 0, green: 146.0 / 255.0, blue: 1.0, alpha: 1.0).CGColor
        layer.position = position
        layer.bounds = bounds
        layer.cornerRadius = cornerRadius
        layer.masksToBounds = true
        layer.borderWidth = borderWidth
        layer.borderColor = UIColor.whiteColor().CGColor
        layer.contents = UIImage(named:"599750_1286086064SSce.jpg")?.CGImage
        self.view.layer.addSublayer(layer)
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
 
 
既然如此为什么还大费周章的说形变呢,因为形变对于动画有特殊的意义。在动画开发中形变往往不是直接设置transform,而是通过keyPath进行设置。这种方法设置形变的本质和前面没有区别,只是利用了KVC可以动态修改其属性值而已,但是这种方式在动画中确实很常用的,因为它可以很方便的将几种形变组合到一起使用。同样是解决动画旋转问题,只要将前面的旋转代码改为下面的代码即可:

   layer.setValue(M_PI, forKeyPath: "transform.rotation.x")

当然,通过key path设置形变参数就需要了解有哪些key path可以设置,这里就不再一一列举,大家可以参照Xcode帮助文档中“CATransform3D Key Paths”一节,里面描述的很详细。

使用自定义图层绘图

在自定义图层中绘图时只要自己编写一个类继承于CALayer然后在drawInContext:中绘图即可。同前面在代理方法绘图一样,要显示图层中绘制的内容也要调用图层的setNeedDisplay方法,否则drawInContext方法将不会调用。

 

import UIKit

class RangeSliderTrackLayer: CALayer {
    weak var rangeSlider :RangeSlider?
    
    override func drawInContext(ctx: CGContext!) {
        if let slider = rangeSlider {
            //clip 轨道的轮廓裁截掉
            let cornerRadius = bounds.height * slider.cuvaceousness / 2.0
            let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
            //CGContextAddPath(ctx, path.CGPath)
            //fill the track
            CGContextSetFillColorWithColor(ctx, slider.trackTintColor.CGColor)
            CGContextAddPath(ctx, path.CGPath)
            CGContextFillPath(ctx)
            //fill the highlighted range
            CGContextSetFillColorWithColor(ctx,  slider.trackHighlightTintColor.CGColor)
            let lowerValuePosition = CGFloat(slider.positionForValue(slider.LowerValue))
            let UpperValuePosition = CGFloat(slider.positionForValue(slider.UpperValue))
            let rect = CGRect(x: lowerValuePosition, y: 0.0, width: UpperValuePosition - lowerValuePosition, height:bounds.height)
            CGContextFillRect(ctx, rect)
        }
    }
}

 

使用Quartz 2D在UIView中绘制图形的本质也是绘制到图层中,为了说明这个问题下面演示自定义图层绘图时没有直接在视图控制器中调用自定义图层,而是在一个UIView将自定义图层添加到UIView的根图层中(例子中的UIView跟自定义图层绘图没有直接关系)。从下面的代码中可以看到:UIView在显示时其根图层会自动创建一个CGContextRef(CALayer本质使用的是位图上下文),同时调用图层代理(UIView创建图层会自动设置图层代理为其自身)的draw: inContext:方法并将图形上下文作为参数传递给这个方法。而在UIView的draw:inContext:方法中会调用其drawRect:方法,在drawRect:方法中使用UIGraphicsGetCurrentContext()方法得到的上下文正是前面创建的上下文。

import UIKit
class KCLayer: CALayer{
    override func drawInContext(ctx: CGContext!) {
        println("3-drawInContext:\(ctx)")
        
        CGContextSetRGBFillColor(ctx, 135.0/255.0, 232.0/255.0, 84.0/255.0, 1)
        CGContextSetRGBStrokeColor(ctx, 135.0/255.0, 232.0/255.0, 84.0/255.0, 1)
        //    CGContextFillRect(ctx, CGRectMake(0, 0, 100, 100));
        //    CGContextFillEllipseInRect(ctx, CGRectMake(50, 50, 100, 100));
        CGContextMoveToPoint(ctx, 94.5, 33.5)
        
        //// Star Drawing
        CGContextAddLineToPoint(ctx,104.02, 47.39)
        CGContextAddLineToPoint(ctx,120.18, 52.16)
        CGContextAddLineToPoint(ctx,109.91, 65.51)
        CGContextAddLineToPoint(ctx,110.37, 82.34)
        CGContextAddLineToPoint(ctx,94.5, 76.7)
        CGContextAddLineToPoint(ctx,78.63, 82.34)
        CGContextAddLineToPoint(ctx,79.09, 65.51)
        CGContextAddLineToPoint(ctx,68.82, 52.16)
        CGContextAddLineToPoint(ctx,84.98, 47.39)
        CGContextClosePath(ctx)
        CGContextDrawPath(ctx, kCGPathFillStroke)
    }
}

import UIKit
class page2: UIViewController {
    override func loadView() {
        super.loadView()
        
        var layer = KCLayer()
        layer.bounds = CGRectMake(0, 0, 185, 185)
        layer.position = CGPointMake(160,284)
        layer.backgroundColor = UIColor(red: 0, green: 146.0/255.0, blue: 1.0, alpha: 1.0).CGColor
        
        //显示图层
        layer.setNeedsDisplay()
        
        self.view.layer.addSublayer(layer)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

 

运行效果:

iOS开发之让你的应用“动”起来(swift)_第8张图片

Core Animation

在iOS中实现一个动画相当简单,只要调用UIView的块代码即可实现一个动画效果,这在其他系统开发中基本不可能实现。

图片自动缩小,然后消失

import UIKit
class page3: UIViewController {
    var pic = UIImage(named: "599750_1286086064SSce.jpg")
    var WIdth = CGFloat( UIScreen.mainScreen().bounds.width)
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var view = UIImageView(image: pic)
        view.frame = CGRectMake(0, -50, WIdth, 250)
        self.view.addSubview(view)
        
        self.view.backgroundColor = UIColor.whiteColor()
        UIView.animateWithDuration(1, delay: 0.2, options:UIViewAnimationOptions.BeginFromCurrentState, animations: {()->Void in
            let y = CGFloat(rand() % Int32(self.WIdth))
            let s = CGFloat(rand() % 15 + 10)
            view.frame = CGRectMake(y, 500, s, s)
            }, completion: {(Bool)->Void in self.clear()})
    }
    func clear(){
        var aa = self.view.subviews
        for i in aa{
                if i is UIImageView{
                    i.removeFromSuperview()
                }
            }
    }
}

 

使用NSTimer与iphone的简单动画,实现飘雪效果

import UIKit

class page4: UIViewController {
    var pic = UIImage(named: "599750_1286086064SSce.jpg")
    var WIdth = Int32( UIScreen.mainScreen().bounds.width)
    override func loadView() {
        super.loadView()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.whiteColor()
        NSTimer.scheduledTimerWithTimeInterval(0.2, target: self, selector: "snow", userInfo: nil, repeats: true)
        NSTimer.scheduledTimerWithTimeInterval(30, target: self, selector: "clear", userInfo: nil, repeats: true)
    }
    func clear(){
        var aa = self.view.subviews
        println(aa.count)
        for (var i:Int = 0;i < aa.count - 100;i++){
            if aa[i] is UIImageView{
                let x = aa[i] as UIImageView
                if x.frame.origin.y == 500{
                    x.removeFromSuperview()
                }
            }
        }
    }
    
    func snow(){
        var view = UIImageView(image: pic)
        view.alpha = 0.5
        let x = CGFloat(rand()  %  WIdth)
        let y = CGFloat(rand() %  WIdth)
        let s = CGFloat(rand() % 15 + 10)
        let sp = 1.0 / Double(random() % 100) + 1
        view.frame = CGRectMake(x, -50, s, s)
        self.view.addSubview(view)
        UIView.beginAnimations("d", context: nil)
        UIView.setAnimationDuration(10*sp)
        view.frame = CGRectMake(y, 500, s, s)//设定该雪花最后的消失坐标
        UIView.setAnimationDelegate(self)
        UIView.commitAnimations()
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

iOS开发之让你的应用“动”起来(swift)_第9张图片

使用上面UIView封装的方法进行动画设置固然十分方便,但是具体动画如何实现我们是不清楚的,而且上面的代码还有一些问题是无法解决的,例如:如何控制动画的暂停?如何进行动画的组合?。。。

这里就需要了解iOS的核心动画Core Animation(包含在Quartz Core框架中)。在iOS中核心动画分为几类:基础动画、关键帧动画、动画组、转场动画。各个类的关系大致如下:

iOS开发之让你的应用“动”起来(swift)_第10张图片

 

CAAnimation:核心动画的基础类,不能直接使用,负责动画运行时间、速度的控制,本身实现了CAMediaTiming协议。

CAPropertyAnimation:属性动画的基类(通过属性进行动画设置,注意是可动画属性),不能直接使用。

CAAnimationGroup:动画组,动画组是一种组合模式设计,可以通过动画组来进行所有动画行为的统一控制,组中所有动画效果可以并发执行。

CATransition:转场动画,主要通过滤镜进行动画效果设置。

CABasicAnimation:基础动画,通过属性修改进行动画参数控制,只有初始状态和结束状态。

CAKeyframeAnimation:关键帧动画,同样是通过属性进行动画参数控制,但是同基础动画不同的是它可以有多个状态控制。

基础动画、关键帧动画都属于属性动画,就是通过修改属性值产生动画效果,开发人员只需要设置初始值和结束值,中间的过程动画(又叫“补间动画”)由系统自动计算产生。和基础动画不同的是关键帧动画可以设置多个属性值,每两个属性中间的补间动画由系统自动完成,因此从这个角度而言基础动画又可以看成是有两个关键帧的关键帧动画。

基础动画

在开发过程中很多情况下通过基础动画就可以满足开发需求,前面例子中使用的UIView代码块进行图像放大缩小的演示动画也是基础动画(在iOS7中UIView也对关键帧动画进行了封装),只是UIView装饰方法隐藏了更多的细节。如果不使用UIView封装的方法,动画创建一般分为以下几步:

1.初始化动画并设置动画属性

2.设置动画属性初始值(可以省略)、结束值以及其他动画属性

3.给图层添加动画

下面以一个移动动画为例进行演示,在这个例子中点击屏幕哪个位置落花将飞向哪里。

//
//  KCMainViewController.m
//  Animation
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置背景(注意这个图片其实在根图层)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定义一个图层
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
}



#pragma mark 移动动画
-(void)translatonAnimation:(CGPoint)location{
    //1.创建动画并指定动画属性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"position"];
    
    //2.设置动画属性初始值和结束值
//    basicAnimation.fromValue=[NSNumber numberWithInteger:50];//可以不设置,默认为图层初始状态
    basicAnimation.toValue=[NSValue valueWithCGPoint:location];
    
    //设置其他动画属性
    basicAnimation.duration=5.0;//动画时间5秒
    //basicAnimation.repeatCount=HUGE_VALF;//设置重复次数,HUGE_VALF可看做无穷大,起到循环动画的效果
    //    basicAnimation.removedOnCompletion=NO;//运行一次是否移除动画

    
    //3.添加动画到图层,注意key相当于给动画进行命名,以后获得该动画时可以使用此名称获取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Translation"];
}

#pragma mark 点击事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    CGPoint location= [touch locationInView:self.view];
    //创建并开始动画
    [self translatonAnimation:location];
}

@end

运行效果:

iOS开发之让你的应用“动”起来(swift)_第11张图片

上面实现了一个基本动画效果,但是这个动画存在一个问题:动画结束后动画图层回到了原来的位置,当然是用UIView封装的方法是没有这个问题的。如何解决这个问题呢?

前面说过图层动画的本质就是将图层内部的内容转化为位图经硬件操作形成一种动画效果,其实图层本身并没有任何的变化。上面的动画中图层并没有因为动画效果而改变它的位置(对于缩放动画其大小也是不会改变的),所以动画完成之后图层还是在原来的显示位置没有任何变化,如果这个图层在一个UIView中你会发现在UIView移动过程中你要触发UIView的点击事件也只能点击原来的位置(即使它已经运动到了别的位置),因为它的位置从来没有变过。当然解决这个问题方法比较多,这里不妨在动画完成之后重新设置它的位置。

//
//  KCMainViewController.m
//  Animation
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置背景(注意这个图片其实在根图层)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定义一个图层
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
}



#pragma mark 移动动画
-(void)translatonAnimation:(CGPoint)location{
    //1.创建动画并指定动画属性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"position"];
    
    //2.设置动画属性初始值和结束值
//    basicAnimation.fromValue=[NSNumber numberWithInteger:50];//可以不设置,默认为图层初始状态
    basicAnimation.toValue=[NSValue valueWithCGPoint:location];
    
    //设置其他动画属性
    basicAnimation.duration=5.0;//动画时间5秒
    //basicAnimation.repeatCount=HUGE_VALF;//设置重复次数,HUGE_VALF可看做无穷大,起到循环动画的效果
    //    basicAnimation.removedOnCompletion=NO;//运行一次是否移除动画
    basicAnimation.delegate=self;
    //存储当前位置在动画结束后使用
    [basicAnimation setValue:[NSValue valueWithCGPoint:location] forKey:@"KCBasicAnimationLocation"];
    
    //3.添加动画到图层,注意key相当于给动画进行命名,以后获得该动画时可以使用此名称获取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Translation"];
}

#pragma mark 点击事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    CGPoint location= [touch locationInView:self.view];
    //创建并开始动画
    [self translatonAnimation:location];
}

#pragma mark - 动画代理方法
#pragma mark 动画开始
-(void)animationDidStart:(CAAnimation *)anim{
    NSLog(@"animation(%@) start.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    NSLog(@"%@",[_layer animationForKey:@"KCBasicAnimation_Translation"]);//通过前面的设置的key获得动画
}

#pragma mark 动画结束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"animation(%@) stop.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    _layer.position=[[anim valueForKey:@"KCBasicAnimationLocation"] CGPointValue];
}
@end

上面通过给动画设置一个代理去监听动画的开始和结束事件,在动画开始前给动画添加一个自定义属性“KCBasicAnimationLocation”存储动画终点位置,然后在动画结束后设置动画的位置为终点位置。

如果运行上面的代码大家可能会发现另外一个问题,那就是动画运行完成后会重新从起始点运动到终点。这个问题产生的原因就是前面提到的,对于非根图层,设置图层的可动画属性(在动画结束后重新设置了position,而position是可动画属性)会产生动画效果。解决这个问题有两种办法:关闭图层隐式动画、设置动画图层为根图层。显然这里不能采取后者,因为根图层当前已经作为动画的背景。

要关闭隐式动画需要用到动画事务CATransaction,在事务内将隐式动画关闭,例如上面的代码可以改为:

#pragma mark 动画结束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"animation(%@) stop.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    //开启事务
    [CATransaction begin];
    //禁用隐式动画
    [CATransaction setDisableActions:YES];
    
    _layer.position=[[anim valueForKey:@"KCBasicAnimationLocation"] CGPointValue];
    
    //提交事务
    [CATransaction commit];
}

 

补充

上面通过在animationDidStop中重新设置动画的位置主要为了说明隐式动画关闭和动画事件之间传参的内容,有朋友发现这种方式有可能在动画运行完之后出现从原点瞬间回到终点的过程,最早在调试的时候没有发现这个问题,这里感谢这位朋友。其实解决这个问题并不难,首先必须设置fromValue,其次在动画开始前设置动画position为终点位置(当然也必须关闭隐式动画)。但是这里主要还是出于学习的目的,真正开发的时候做平移动画直接使用隐式动画即可,没有必要那么麻烦。

 

当然上面的动画还显得有些生硬,因为落花飘散的时候可能不仅仅是自由落体运动,本身由于空气阻力、外界风力还会造成落花在空中的旋转、摇摆等,这里不妨给图层添加一个旋转的动画。对于图层的旋转前面已经演示过怎么通过key path设置图层旋转的内容了,在这里需要强调一下,图层的形变都是基于锚点进行的。例如旋转,旋转的中心点就是图层的锚点。

//
//  KCMainViewController.m
//  Animation
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置背景(注意这个图片其实在根图层)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定义一个图层
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.anchorPoint=CGPointMake(0.5, 0.6);//设置锚点
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
}



#pragma mark 移动动画
-(void)translatonAnimation:(CGPoint)location{
    //1.创建动画并指定动画属性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"position"];
    
    //2.设置动画属性初始值、结束值
//    basicAnimation.fromValue=[NSNumber numberWithInteger:50];//可以不设置,默认为图层初始状态
    basicAnimation.toValue=[NSValue valueWithCGPoint:location];
    
    //设置其他动画属性
    basicAnimation.duration=5.0;//动画时间5秒
    //basicAnimation.repeatCount=HUGE_VALF;//设置重复次数,HUGE_VALF可看做无穷大,起到循环动画的效果
    //    basicAnimation.removedOnCompletion=NO;//运行一次是否移除动画
    basicAnimation.delegate=self;
    //存储当前位置在动画结束后使用
    [basicAnimation setValue:[NSValue valueWithCGPoint:location] forKey:@"KCBasicAnimationLocation"];
    
    //3.添加动画到图层,注意key相当于给动画进行命名,以后获得该图层时可以使用此名称获取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Translation"];
}

#pragma mark 旋转动画
-(void)rotationAnimation{
    //1.创建动画并指定动画属性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    
    //2.设置动画属性初始值、结束值
//    basicAnimation.fromValue=[NSNumber numberWithInt:M_PI_2];
    basicAnimation.toValue=[NSNumber numberWithFloat:M_PI_2*3];
    
    //设置其他动画属性
    basicAnimation.duration=6.0;
    basicAnimation.autoreverses=true;//旋转后再旋转到原来的位置

    
    //4.添加动画到图层,注意key相当于给动画进行命名,以后获得该动画时可以使用此名称获取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Rotation"];
}

#pragma mark 点击事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    CGPoint location= [touch locationInView:self.view];
    //创建并开始动画
    [self translatonAnimation:location];
    
    [self rotationAnimation];
}

#pragma mark - 动画代理方法
#pragma mark 动画开始
-(void)animationDidStart:(CAAnimation *)anim{
    NSLog(@"animation(%@) start.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    NSLog(@"%@",[_layer animationForKey:@"KCBasicAnimation_Translation"]);//通过前面的设置的key获得动画
}

#pragma mark 动画结束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"animation(%@) stop.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    //开启事务
    [CATransaction begin];
    //禁用隐式动画
    [CATransaction setDisableActions:YES];
    
    _layer.position=[[anim valueForKey:@"KCBasicAnimationLocation"] CGPointValue];
    
    //提交事务
    [CATransaction commit];
}

@end

上面代码中结合两种动画操作,需要注意的是只给移动动画设置了代理,在旋转动画中并没有设置代理,否则代理方法会执行两遍。由于旋转动画会无限循环执行(上面设置了重复次数无穷大),并且两个动画的执行时间没有必然的关系,这样一来移动停止后可能还在旋转,为了让移动动画停止后旋转动画停止就需要使用到动画的暂停和恢复方法。

核心动画的运行有一个媒体时间的概念,假设将一个旋转动画设置旋转一周用时60秒的话,那么当动画旋转90度后媒体时间就是15秒。如果此时要将动画暂停只需要让媒体时间偏移量设置为15秒即可,并把动画运行速度设置为0使其停止运动。类似的,如果又过了60秒后需要恢复动画(此时媒体时间为75秒),这时只要将动画开始开始时间设置为当前媒体时间75秒减去暂停时的时间(也就是之前定格动画时的偏移量)15秒(开始时间=75-15=60秒),那么动画就会重新计算60秒后的状态再开始运行,与此同时将偏移量重新设置为0并且把运行速度设置1。这个过程中真正起到暂停动画和恢复动画的其实是动画速度的调整,媒体时间偏移量以及恢复时的开始时间设置主要为了让动画更加连贯。

下面的代码演示了移动动画结束后旋转动画暂停,并且当再次点击动画时旋转恢复的过程(注意在移动过程中如果再次点击屏幕可以暂停移动和旋转动画,再次点击可以恢复两种动画。但是当移动结束后触发了移动动画的完成事件如果再次点击屏幕则只能恢复旋转动画,因为此时移动动画已经结束而不是暂停,无法再恢复)。

//
//  KCMainViewController.m
//  Animation
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置背景(注意这个图片其实在根图层)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定义一个图层
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.anchorPoint=CGPointMake(0.5, 0.6);//设置锚点
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
}



#pragma mark 移动动画
-(void)translatonAnimation:(CGPoint)location{
    //1.创建动画并指定动画属性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"position"];
    
    //2.设置动画属性初始值、结束值
//    basicAnimation.fromValue=[NSNumber numberWithInteger:50];//可以不设置,默认为图层初始状态
    basicAnimation.toValue=[NSValue valueWithCGPoint:location];
    
    //设置其他动画属性
    basicAnimation.duration=5.0;//动画时间5秒
//    basicAnimation.repeatCount=HUGE_VALF;//设置重复次数,HUGE_VALF可看做无穷大,起到循环动画的效果
    basicAnimation.removedOnCompletion=NO;//运行一次是否移除动画
    basicAnimation.delegate=self;
    //存储当前位置在动画结束后使用
    [basicAnimation setValue:[NSValue valueWithCGPoint:location] forKey:@"KCBasicAnimationLocation"];
    
    //3.添加动画到图层,注意key相当于给动画进行命名,以后获得该图层时可以使用此名称获取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Translation"];
}

#pragma mark 旋转动画
-(void)rotationAnimation{
    //1.创建动画并指定动画属性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    
    //2.设置动画属性初始值、结束值
//    basicAnimation.fromValue=[NSNumber numberWithInt:M_PI_2];
    basicAnimation.toValue=[NSNumber numberWithFloat:M_PI_2*3];
    
    //设置其他动画属性
    basicAnimation.duration=6.0;
    basicAnimation.autoreverses=true;//旋转后在旋转到原来的位置
    basicAnimation.repeatCount=HUGE_VALF;//设置无限循环
    basicAnimation.removedOnCompletion=NO;
//    basicAnimation.delegate=self;

    
    //4.添加动画到图层,注意key相当于给动画进行命名,以后获得该动画时可以使用此名称获取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Rotation"];
}

#pragma mark 点击事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    CGPoint location= [touch locationInView:self.view];
    //判断是否已经常见过动画,如果已经创建则不再创建动画
    CAAnimation *animation= [_layer animationForKey:@"KCBasicAnimation_Translation"];
    if(animation){
        if (_layer.speed==0) {
            [self animationResume];
        }else{
            [self animationPause];
        }
    }else{
        //创建并开始动画
        [self translatonAnimation:location];
        
        [self rotationAnimation];
    }
}

#pragma mark 动画暂停
-(void)animationPause{
    //取得指定图层动画的媒体时间,后面参数用于指定子图层,这里不需要
    CFTimeInterval interval=[_layer convertTime:CACurrentMediaTime() fromLayer:nil];
    //设置时间偏移量,保证暂停时停留在旋转的位置
    [_layer setTimeOffset:interval];
    //速度设置为0,暂停动画
    _layer.speed=0;
}

#pragma mark 动画恢复
-(void)animationResume{
    //获得暂停的时间
    CFTimeInterval beginTime= CACurrentMediaTime()- _layer.timeOffset;
    //设置偏移量
    _layer.timeOffset=0;
    //设置开始时间
    _layer.beginTime=beginTime;
    //设置动画速度,开始运动
    _layer.speed=1.0;
}

#pragma mark - 动画代理方法
#pragma mark 动画开始
-(void)animationDidStart:(CAAnimation *)anim{
    NSLog(@"animation(%@) start.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    NSLog(@"%@",[_layer animationForKey:@"KCBasicAnimation_Translation"]);//通过前面的设置的key获得动画
}

#pragma mark 动画结束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"animation(%@) stop.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    
    //开启事务
    [CATransaction begin];
    //禁用隐式动画
    [CATransaction setDisableActions:YES];
    
    _layer.position=[[anim valueForKey:@"KCBasicAnimationLocation"] CGPointValue];
    
    //提交事务
    [CATransaction commit];
    
    //暂停动画
    [self animationPause];

}

@end

运行效果:

iOS开发之让你的应用“动”起来(swift)_第12张图片

注意:

  • 动画暂停针对的是图层而不是图层中的某个动画。
  • 要做无限循环的动画,动画的removedOnCompletion属性必须设置为NO,否则运行一次动画就会销毁。

关键帧动画

熟悉flash开发的朋友对于关键帧动画应该不陌生,这种动画方式在flash开发中经常用到。关键帧动画就是在动画控制过程中开发者指定主要的动画状态,至于各个状态间动画如何进行则由系统自动运算补充(每两个关键帧之间系统形成的动画称为“补间动画”),这种动画的好处就是开发者不用逐个控制每个动画帧,而只要关心几个关键帧的状态即可。

关键帧动画开发分为两种形式:一种是通过设置不同的属性值进行关键帧控制,另一种是通过绘制路径进行关键帧控制。后者优先级高于前者,如果设置了路径则属性值就不再起作用。

对于前面的落花动画效果而言其实落花的过程并不自然,很显然实际生活中它不可能沿着直线下落,这里我们不妨通过关键帧动画的values属性控制它在下落过程中的属性。假设下落过程如图:

iOS开发之让你的应用“动”起来(swift)_第13张图片

在这里需要设置四个关键帧(如图中四个关键点),具体代码如下(动画创建过程同基本动画基本完全一致):

//
//  通过values设置关键帧动画
//  Animation
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置背景(注意这个图片其实在根图层)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];

    //自定义一个图层
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
    
    //创建动画
    [self translationAnimation];
}

#pragma mark 关键帧动画
-(void)translationAnimation{
    //1.创建关键帧动画并设置动画属性
    CAKeyframeAnimation *keyframeAnimation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
    
    //2.设置关键帧,这里有四个关键帧
    NSValue *key1=[NSValue valueWithCGPoint:_layer.position];//对于关键帧动画初始值不能省略
    NSValue *key2=[NSValue valueWithCGPoint:CGPointMake(80, 220)];
    NSValue *key3=[NSValue valueWithCGPoint:CGPointMake(45, 300)];
    NSValue *key4=[NSValue valueWithCGPoint:CGPointMake(55, 400)];
    NSArray *values=@[key1,key2,key3,key4];
    keyframeAnimation.values=values;
    //设置其他属性
    keyframeAnimation.duration=8.0;
    keyframeAnimation.beginTime=CACurrentMediaTime()+2;//设置延迟2秒执行
    
    
    //3.添加动画到图层,添加动画后就会执行动画
    [_layer addAnimation:keyframeAnimation forKey:@"KCKeyframeAnimation_Position"];
}

@end

运行效果(注意运行结束没有设置图层位置为动画运动结束位置):

iOS开发之让你的应用“动”起来(swift)_第14张图片

上面的方式固然比前面使用基础动画效果要好一些,但其实还是存在问题,那就是落花飞落的路径是直线的,当然这个直线是根据程序中设置的四个关键帧自动形成的,那么如何让它沿着曲线飘落呢?这就是第二种类型的关键帧动画,通过描绘路径进行关键帧动画控制。假设让落花沿着下面的曲线路径飘落:

iOS开发之让你的应用“动”起来(swift)_第15张图片

当然,这是一条贝塞尔曲线,学习了前篇文章之后相信对于这类曲线应该并不陌生,下面是具体实现代码:

//
//  通过path设置关键帧动画
//  Animation
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置背景(注意这个图片其实在根图层)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定义一个图层
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
    
    //创建动画
    [self translationAnimation];
}

#pragma mark 关键帧动画
-(void)translationAnimation{
    //1.创建关键帧动画并设置动画属性
    CAKeyframeAnimation *keyframeAnimation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
    
    //2.设置路径
    //绘制贝塞尔曲线
    CGPathRef path=CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, _layer.position.x, _layer.position.y);//移动到起始点
    CGPathAddCurveToPoint(path, NULL, 160, 280, -30, 300, 55, 400);//绘制二次贝塞尔曲线

    keyframeAnimation.path=path;//设置path属性
    CGPathRelease(path);//释放路径对象
    //设置其他属性
    keyframeAnimation.duration=8.0;
    keyframeAnimation.beginTime=CACurrentMediaTime()+5;//设置延迟2秒执行
    
    
    //3.添加动画到图层,添加动画后就会执行动画
    [_layer addAnimation:keyframeAnimation forKey:@"KCKeyframeAnimation_Position"];
}

@end

运行效果(注意运行结束没有设置图层位置为动画运动结束位置):

iOS开发之让你的应用“动”起来(swift)_第16张图片

看起来动画不会那么生硬了,但是这里需要注意,对于路径类型的关键帧动画系统是从描绘路径的位置开始路径,直到路径结束。如果上面的路径不是贝塞尔曲线而是矩形路径那么它会从矩形的左上角开始运行,顺时针一周回到左上角;如果指定的路径是一个椭圆,那么动画运行的路径是从椭圆右侧开始(0度)顺时针一周回到右侧。

补充--其他属性

在关键帧动画中还有一些动画属性初学者往往比较容易混淆,这里专门针对这些属性做一下介绍。

keyTimes:各个关键帧的时间控制。前面使用values设置了四个关键帧,默认情况下每两帧之间的间隔为:8/(4-1)秒。如果想要控制动画从第一帧到第二针占用时间4秒,从第二帧到第三帧时间为2秒,而从第三帧到第四帧时间2秒的话,就可以通过keyTimes进行设置。keyTimes中存储的是时间占用比例点,此时可以设置keyTimes的值为0.0,0.5,0.75,1.0(当然必须转换为NSNumber),也就是说1到2帧运行到总时间的50%,2到3帧运行到总时间的75%,3到4帧运行到8秒结束。

caculationMode:动画计算模式。还拿上面keyValues动画举例,之所以1到2帧能形成连贯性动画而不是直接从第1帧经过8/3秒到第2帧是因为动画模式是连续的(值为kCAAnimationLinear,这是计算模式的默认值);而如果指定了动画模式为kCAAnimationDiscrete离散的那么你会看到动画从第1帧经过8/3秒直接到第2帧,中间没有任何过渡。其他动画模式还有:kCAAnimationPaced(均匀执行,会忽略keyTimes)、kCAAnimationCubic(平滑执行,对于位置变动关键帧动画运行轨迹更平滑)、kCAAnimationCubicPaced(平滑均匀执行)。

下图描绘出了几种动画模式的关系(横坐标是运行时间,纵坐标是动画属性[例如位置、透明度等]):

iOS开发之让你的应用“动”起来(swift)_第17张图片

 

动画组

实际开发中一个物体的运动往往是复合运动,单一属性的运动情况比较少,但恰恰属性动画每次进行动画设置时一次只能设置一个属性进行动画控制(不管是基础动画还是关键帧动画都是如此),这样一来要做一个复合运动的动画就必须创建多个属性动画进行组合。对于一两种动画的组合或许处理起来还比较容易,但是对于更多动画的组合控制往往会变得很麻烦,动画组的产生就是基于这样一种情况而产生的。动画组是一系列动画的组合,凡是添加到动画组中的动画都受控于动画组,这样一来各类动画公共的行为就可以统一进行控制而不必单独设置,而且放到动画组中的各个动画可以并发执行,共同构建出复杂的动画效果。

动画组使用起来并不复杂,首先单独创建单个动画(可以是基础动画也可以是关键帧动画),然后将基础动画添加到动画组,最后将动画组添加到图层即可。

前面关键帧动画部分,路径动画看起来效果虽然很流畅,但是落花本身的旋转运动没有了,这里不妨将基础动画部分的旋转动画和路径关键帧动画进行组合使得整个动画看起来更加的和谐、顺畅。

//
//  动画组
//  Animation
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置背景(注意这个图片其实在根图层)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定义一个图层
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
    
    //创建动画
    [self groupAnimation];
}

#pragma mark 基础旋转动画
-(CABasicAnimation *)rotationAnimation{

    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];

    CGFloat toValue=M_PI_2*3;
    basicAnimation.toValue=[NSNumber numberWithFloat:M_PI_2*3];

//    basicAnimation.duration=6.0;
    basicAnimation.autoreverses=true;
    basicAnimation.repeatCount=HUGE_VALF;
    basicAnimation.removedOnCompletion=NO;
    
    [basicAnimation setValue:[NSNumber numberWithFloat:toValue] forKey:@"KCBasicAnimationProperty_ToValue"];
    
    return basicAnimation;
}

#pragma mark 关键帧移动动画
-(CAKeyframeAnimation *)translationAnimation{
    CAKeyframeAnimation *keyframeAnimation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
    
    CGPoint endPoint= CGPointMake(55, 400);
    CGPathRef path=CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, _layer.position.x, _layer.position.y);
    CGPathAddCurveToPoint(path, NULL, 160, 280, -30, 300, endPoint.x, endPoint.y);
    
    keyframeAnimation.path=path;
    CGPathRelease(path);

    [keyframeAnimation setValue:[NSValue valueWithCGPoint:endPoint] forKey:@"KCKeyframeAnimationProperty_EndPosition"];
    
    return keyframeAnimation;
}

#pragma mark 创建动画组
-(void)groupAnimation{
    //1.创建动画组
    CAAnimationGroup *animationGroup=[CAAnimationGroup animation];
    
    //2.设置组中的动画和其他属性
    CABasicAnimation *basicAnimation=[self rotationAnimation];
    CAKeyframeAnimation *keyframeAnimation=[self translationAnimation];
    animationGroup.animations=@[basicAnimation,keyframeAnimation];
    
    animationGroup.delegate=self;
    animationGroup.duration=10.0;//设置动画时间,如果动画组中动画已经设置过动画属性则不再生效
    animationGroup.beginTime=CACurrentMediaTime()+5;//延迟五秒执行
    
    //3.给图层添加动画
    [_layer addAnimation:animationGroup forKey:nil];
}

#pragma mark - 代理方法
#pragma mark 动画完成
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    CAAnimationGroup *animationGroup=(CAAnimationGroup *)anim;
    CABasicAnimation *basicAnimation=animationGroup.animations[0];
    CAKeyframeAnimation *keyframeAnimation=animationGroup.animations[1];
    CGFloat toValue=[[basicAnimation valueForKey:@"KCBasicAnimationProperty_ToValue"] floatValue];
    CGPoint endPoint=[[keyframeAnimation valueForKey:@"KCKeyframeAnimationProperty_EndPosition"] CGPointValue];
    
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    
    //设置动画最终状态
    _layer.position=endPoint;
    _layer.transform=CATransform3DMakeRotation(toValue, 0, 0, 1);
    
    [CATransaction commit];
}

@end

运行效果:

iOS开发之让你的应用“动”起来(swift)_第18张图片

转场动画

转场动画就是从一个场景以动画的形式过渡到另一个场景。转场动画的使用一般分为以下几个步骤:

1.创建转场动画

2.设置转场类型、子类型(可选)及其他属性

3.设置转场后的新视图并添加动画到图层

下表列出了常用的转场类型(注意私有API是苹果官方没有公开的动画类型,但是目前通过仍然可以使用):

动画类型 说明 对应常量 是否支持方向设置
公开API      
fade 淡出效果 kCATransitionFade
movein 新视图移动到旧视图上 kCATransitionMoveIn
push 新视图推出旧视图 kCATransitionPush
reveal 移开旧视图显示新视图 kCATransitionReveal
私有API   私有API只能通过字符串访问  
cube 立方体翻转效果
oglFlip 翻转效果
suckEffect 收缩效果
rippleEffect 水滴波纹效果
pageCurl 向上翻页效果
pageUnCurl 向下翻页效果
cameralIrisHollowOpen 摄像头打开效果
cameraIrisHollowClose 摄像头关闭效果

另外对于支持方向设置的动画类型还包含子类型:

动画子类型 说明
kCATransitionFromRight 从右侧转场
kCATransitionFromLeft 从左侧转场
kCATransitionFromTop 从顶部转场
kCATransitionFromBottom 从底部转场

在前面的文章“IOS开发系列--无限循环的图片浏览器”中为了使用UIScrollView做无限循环图片浏览器花费了不少时间在性能优化上面,这里使用转场动画利用一个UIImageView实现一个漂亮的无限循环图片浏览器。

//
//  KCMainViewController.m
//  TransitionAnimation
//
//  Created by Kenshin Cui on 14-3-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#define IMAGE_COUNT 5

@interface KCMainViewController (){
    UIImageView *_imageView;
    int _currentIndex;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //定义图片控件
    _imageView=[[UIImageView alloc]init];
    _imageView.frame=[UIScreen mainScreen].applicationFrame;
    _imageView.contentMode=UIViewContentModeScaleAspectFit;
    _imageView.image=[UIImage imageNamed:@"0.jpg"];//默认图片
    [self.view addSubview:_imageView];
    //添加手势
    UISwipeGestureRecognizer *leftSwipeGesture=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(leftSwipe:)];
    leftSwipeGesture.direction=UISwipeGestureRecognizerDirectionLeft;
    [self.view addGestureRecognizer:leftSwipeGesture];
    
    UISwipeGestureRecognizer *rightSwipeGesture=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(rightSwipe:)];
    rightSwipeGesture.direction=UISwipeGestureRecognizerDirectionRight;
    [self.view addGestureRecognizer:rightSwipeGesture];
}

#pragma mark 向左滑动浏览下一张图片
-(void)leftSwipe:(UISwipeGestureRecognizer *)gesture{
    [self transitionAnimation:YES];
}

#pragma mark 向右滑动浏览上一张图片
-(void)rightSwipe:(UISwipeGestureRecognizer *)gesture{
    [self transitionAnimation:NO];
}


#pragma mark 转场动画
-(void)transitionAnimation:(BOOL)isNext{
    //1.创建转场动画对象
    CATransition *transition=[[CATransition alloc]init];
    
    //2.设置动画类型,注意对于苹果官方没公开的动画类型只能使用字符串,并没有对应的常量定义
    transition.type=@"cube";
    
    //设置子类型
    if (isNext) {
        transition.subtype=kCATransitionFromRight;
    }else{
        transition.subtype=kCATransitionFromLeft;
    }
    //设置动画时常
    transition.duration=1.0f;
    
    //3.设置转场后的新视图添加转场动画
    _imageView.image=[self getImage:isNext];
    [_imageView.layer addAnimation:transition forKey:@"KCTransitionAnimation"];
}

#pragma mark 取得当前图片
-(UIImage *)getImage:(BOOL)isNext{
    if (isNext) {
        _currentIndex=(_currentIndex+1)%IMAGE_COUNT;
    }else{
        _currentIndex=(_currentIndex-1+IMAGE_COUNT)%IMAGE_COUNT;
    }
    NSString *imageName=[NSString stringWithFormat:@"%i.jpg",_currentIndex];
    return [UIImage imageNamed:imageName];
}
@end

运行效果:

代码十分简单,但是效果和性能却很惊人。当然演示代码有限,其他动画类型大家可以自己实现,效果都很绚丽。

逐帧动画

前面介绍了核心动画中大部分动画类型,但是做过动画处理的朋友都知道,在动画制作中还有一种动画类型“逐帧动画”。说到逐帧动画相信很多朋友第一个想到的就是UIImageView,通过设置UIImageView的animationImages属性,然后调用它的startAnimating方法去播放这组图片。当然这种方法在某些场景下是可以达到逐帧的动画效果,但是它也存在着很大的性能问题,并且这种方法一旦设置完图片中间的过程就无法控制了。当然,也许有朋友会想到利用iOS的定时器NSTimer定时更新图片来达到逐帧动画的效果。这种方式确实可以解决UIImageView一次性加载大量图片的问题,而且让播放过程可控,唯一的缺点就是定时器方法调用有时可能会因为当前系统执行某种比较占用时间的任务造成动画连续性出现问题。

虽然在核心动画没有直接提供逐帧动画类型,但是却提供了用于完成逐帧动画的相关对象CADisplayLink。CADisplayLink是一个计时器,但是同NSTimer不同的是,CADisplayLink的刷新周期同屏幕完全一致。例如在iOS中屏幕刷新周期是60次/秒,CADisplayLink刷新周期同屏幕刷新一致也是60次/秒,这样一来使用它完成的逐帧动画(又称为“时钟动画”)完全感觉不到动画的停滞情况。

在iOS开篇“IOS开发系列--IOS程序开发概览”中就曾说过:iOS程序在运行后就进入一个消息循环中(这个消息循环称为“主运行循环”),整个程序相当于进入一个死循环中,始终等待用户输入。将CADisplayLink加入到主运行循环队列后,它的时钟周期就和主运行循环保持一致,而主运行循环周期就是屏幕刷新周期。在CADisplayLink加入到主运行循环队列后就会循环调用目标方法,在这个方法中更新视图内容就可以完成逐帧动画。

当然这里不得不强调的是逐帧动画性能势必较低,但是对于一些事物的运动又不得不选择使用逐帧动画,例如人的运动,这是一个高度复杂的运动,基本动画、关键帧动画是不可能解决的。所大家一定要注意在循环方法中尽可能的降低算法复杂度,同时保证循环过程中内存峰值尽可能低。下面以一个鱼的运动为例为大家演示一下逐帧动画。

//
//  KCMainViewController.m
//  DisplayLink
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#define IMAGE_COUNT 10

@interface KCMainViewController (){
    CALayer *_layer;
    int _index;
    NSMutableArray *_images;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置背景
    self.view.layer.contents=(id)[UIImage imageNamed:@"bg.png"].CGImage;
    
    //创建图像显示图层
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 87, 32);
    _layer.position=CGPointMake(160, 284);
    [self.view.layer addSublayer:_layer];
    
    //由于鱼的图片在循环中会不断创建,而10张鱼的照片相对都很小
    //与其在循环中不断创建UIImage不如直接将10张图片缓存起来
    _images=[NSMutableArray array];
    for (int i=0; i<10; ++i) {
        NSString *imageName=[NSString stringWithFormat:@"fish%i.png",i];
        UIImage *image=[UIImage imageNamed:imageName];
        [_images addObject:image];
    }

    
    //定义时钟对象
    CADisplayLink *displayLink=[CADisplayLink displayLinkWithTarget:self selector:@selector(step)];
    //添加时钟对象到主运行循环
    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

#pragma mark 每次屏幕刷新就会执行一次此方法(每秒接近60次)
-(void)step{
    //定义一个变量记录执行次数
    static int s=0;
    //每秒执行6次
    if (++s%10==0) {
        UIImage *image=_images[_index];
        _layer.contents=(id)image.CGImage;//更新图片
        _index=(_index+1)%IMAGE_COUNT;
    }
}
@end

运行效果:

iOS开发之让你的应用“动”起来(swift)_第19张图片

注意:上面仅仅演示了逐帧动画的过程,事实上结合其他动画类型可以让整条鱼游动起来,这里不再赘述。

UIView动画封装

有了前面核心动画的知识,相信大家开发出一般的动画效果应该不在话下。在核心动画开篇也给大家说过,其实UIView本身对于基本动画和关键帧动画、转场动画都有相应的封装,在对动画细节没有特殊要求的情况下使用起来也要简单的多。可以说在日常开发中90%以上的情况使用UIView的动画封装方法都可以搞定,因此在熟悉了核心动画的原理之后还是有必要给大家简单介绍一下UIView中各类动画使用方法的。由于前面核心动画内容已经进行过详细介绍,学习UIView的封装方法根本是小菜一碟,这里对于一些细节就不再赘述了。

基础动画

基础动画部分和前面的基础动画演示相对应,演示点击屏幕落叶飘落到鼠标点击位置的过程。注意根据前面介绍的隐式动画知识其实非根图层直接设置终点位置不需要使用UIView的动画方法也可以实现动画效果,因此这里落花不再放到图层中而是放到了一个UIImageView中。

下面的代码演示了通过block和静态方法实现动画控制的过程:

//
//  UIView实现基础动画
//  Animation
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    UIImageView *_imageView;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置背景
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];

    //创建图像显示控件
    _imageView=[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"petal.png"]];
    _imageView.center=CGPointMake(50, 150);
    [self.view addSubview:_imageView];
}

#pragma mark 点击事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    CGPoint location= [touch locationInView:self.view];
    //方法1:block方式
    /*开始动画,UIView的动画方法执行完后动画会停留在重点位置,而不需要进行任何特殊处理
     duration:执行时间
     delay:延迟时间
     options:动画设置,例如自动恢复、匀速运动等
     completion:动画完成回调方法
     */
//    [UIView animateWithDuration:5.0 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
//        _imageView.center=location;
//    } completion:^(BOOL finished) {
//        NSLog(@"Animation end.");
//    }];
    
    //方法2:静态方法
    //开始动画
    [UIView beginAnimations:@"KCBasicAnimation" context:nil];
    [UIView setAnimationDuration:5.0];
    //[UIView setAnimationDelay:1.0];//设置延迟
    //[UIView setAnimationRepeatAutoreverses:NO];//是否回复
    //[UIView setAnimationRepeatCount:10];//重复次数
    //[UIView setAnimationStartDate:(NSDate *)];//设置动画开始运行的时间
    //[UIView setAnimationDelegate:self];//设置代理
    //[UIView setAnimationWillStartSelector:(SEL)];//设置动画开始运动的执行方法
    //[UIView setAnimationDidStopSelector:(SEL)];//设置动画运行结束后的执行方法
    
    _imageView.center=location;
    
    
    //开始动画
    [UIView commitAnimations];
}
@end

补充--弹簧动画效果

由于在iOS开发中弹性动画使用很普遍,所以在iOS7苹果官方直接提供了一个方法用于弹性动画开发,下面简单的演示一下:

//
//  UIView实现基础动画--弹性动画
//  Animation
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    UIImageView *_imageView;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //创建图像显示控件
    _imageView=[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"ball.png"]];
    _imageView.center=CGPointMake(160, 50);
    [self.view addSubview:_imageView];
}

#pragma mark 点击事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    CGPoint location= [touch locationInView:self.view];
    /*创建弹性动画
     damping:阻尼,范围0-1,阻尼越接近于0,弹性效果越明显
     velocity:弹性复位的速度
    */
    [UIView animateWithDuration:5.0 delay:0 usingSpringWithDamping:0.1 initialSpringVelocity:1.0 options:UIViewAnimationOptionCurveLinear animations:^{
        _imageView.center=location; //CGPointMake(160, 284);
    } completion:nil];
}
@end

运行效果:

iOS开发之让你的应用“动”起来(swift)_第20张图片

补充--动画设置参数

在动画方法中有一个option参数,UIViewAnimationOptions类型,它是一个枚举类型,动画参数分为三类,可以组合使用:

1.常规动画属性设置(可以同时选择多个进行设置)

UIViewAnimationOptionLayoutSubviews:动画过程中保证子视图跟随运动。

UIViewAnimationOptionAllowUserInteraction:动画过程中允许用户交互。

UIViewAnimationOptionBeginFromCurrentState:所有视图从当前状态开始运行。

UIViewAnimationOptionRepeat:重复运行动画。

UIViewAnimationOptionAutoreverse :动画运行到结束点后仍然以动画方式回到初始点。

UIViewAnimationOptionOverrideInheritedDuration:忽略嵌套动画时间设置。

UIViewAnimationOptionOverrideInheritedCurve:忽略嵌套动画速度设置。

UIViewAnimationOptionAllowAnimatedContent:动画过程中重绘视图(注意仅仅适用于转场动画)。 

UIViewAnimationOptionShowHideTransitionViews:视图切换时直接隐藏旧视图、显示新视图,而不是将旧视图从父视图移除(仅仅适用于转场动画)
UIViewAnimationOptionOverrideInheritedOptions :不继承父动画设置或动画类型。

2.动画速度控制(可从其中选择一个设置)

UIViewAnimationOptionCurveEaseInOut:动画先缓慢,然后逐渐加速。

UIViewAnimationOptionCurveEaseIn :动画逐渐变慢。

UIViewAnimationOptionCurveEaseOut:动画逐渐加速。

UIViewAnimationOptionCurveLinear :动画匀速执行,默认值。

3.转场类型(仅适用于转场动画设置,可以从中选择一个进行设置,基本动画、关键帧动画不需要设置)

UIViewAnimationOptionTransitionNone:没有转场动画效果。

UIViewAnimationOptionTransitionFlipFromLeft :从左侧翻转效果。

UIViewAnimationOptionTransitionFlipFromRight:从右侧翻转效果。

UIViewAnimationOptionTransitionCurlUp:向后翻页的动画过渡效果。   

UIViewAnimationOptionTransitionCurlDown :向前翻页的动画过渡效果。   

UIViewAnimationOptionTransitionCrossDissolve:旧视图溶解消失显示下一个新视图的效果。   

UIViewAnimationOptionTransitionFlipFromTop :从上方翻转效果。   

UIViewAnimationOptionTransitionFlipFromBottom:从底部翻转效果。

关键帧动画

从iOS7开始UIView动画中封装了关键帧动画,下面就来看一下如何使用UIView封装方法进行关键帧动画控制,这里实现前面关键帧动画部分对于落花的控制。

//
//  UIView关键帧动画
//  UIViewAnimation
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    UIImageView *_imageView;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置背景
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];

    //创建图像显示控件
    _imageView=[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"petal.png"]];
    _imageView.center=CGPointMake(50, 150);
    [self.view addSubview:_imageView];
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    //UITouch *touch=touches.anyObject;
    //CGPoint location= [touch locationInView:self.view];
    
    /*关键帧动画
     options:
     */
    [UIView animateKeyframesWithDuration:5.0 delay:0 options: UIViewAnimationOptionCurveLinear| UIViewAnimationOptionCurveLinear animations:^{
        //第二个关键帧(准确的说第一个关键帧是开始位置):从0秒开始持续50%的时间,也就是5.0*0.5=2.5秒
        [UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
            _imageView.center=CGPointMake(80.0, 220.0);
        }];
        //第三个关键帧,从0.5*5.0秒开始,持续5.0*0.25=1.25秒
        [UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.25 animations:^{
            _imageView.center=CGPointMake(45.0, 300.0);
        }];
        //第四个关键帧:从0.75*5.0秒开始,持所需5.0*0.25=1.25秒
        [UIView addKeyframeWithRelativeStartTime:0.75 relativeDuration:0.25 animations:^{
            _imageView.center=CGPointMake(55.0, 400.0);
        }];
        
    } completion:^(BOOL finished) {
        NSLog(@"Animation end.");
    }];
}
@end

补充--动画设置参数

对于关键帧动画也有一些动画参数设置options,UIViewKeyframeAnimationOptions类型,和上面基本动画参数设置有些差别,关键帧动画设置参数分为两类,可以组合使用:

1.常规动画属性设置(可以同时选择多个进行设置)

UIViewAnimationOptionLayoutSubviews:动画过程中保证子视图跟随运动。

UIViewAnimationOptionAllowUserInteraction:动画过程中允许用户交互。

UIViewAnimationOptionBeginFromCurrentState:所有视图从当前状态开始运行。

UIViewAnimationOptionRepeat:重复运行动画。

UIViewAnimationOptionAutoreverse :动画运行到结束点后仍然以动画方式回到初始点。

UIViewAnimationOptionOverrideInheritedDuration:忽略嵌套动画时间设置。
UIViewAnimationOptionOverrideInheritedOptions :不继承父动画设置或动画类型。

2.动画模式设置(同前面关键帧动画动画模式一一对应,可以从其中选择一个进行设置)

UIViewKeyframeAnimationOptionCalculationModeLinear:连续运算模式。

UIViewKeyframeAnimationOptionCalculationModeDiscrete :离散运算模式。

UIViewKeyframeAnimationOptionCalculationModePaced:均匀执行运算模式。

UIViewKeyframeAnimationOptionCalculationModeCubic:平滑运算模式。

UIViewKeyframeAnimationOptionCalculationModeCubicPaced:平滑均匀运算模式。

注意:前面说过关键帧动画有两种形式,上面演示的是属性值关键帧动画,路径关键帧动画目前UIView还不支持。

转场动画

从iOS4.0开始,UIView直接封装了转场动画,使用起来同样很简单。

//
//  UIView转场动画
//  TransitionAnimation
//
//  Created by Kenshin Cui on 14-3-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#define IMAGE_COUNT 5

@interface KCMainViewController (){
    UIImageView *_imageView;
    int _currentIndex;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //定义图片控件
    _imageView=[[UIImageView alloc]init];
    _imageView.frame=[UIScreen mainScreen].applicationFrame;
    _imageView.contentMode=UIViewContentModeScaleAspectFit;
    _imageView.image=[UIImage imageNamed:@"0.jpg"];//默认图片
    [self.view addSubview:_imageView];
    //添加手势
    UISwipeGestureRecognizer *leftSwipeGesture=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(leftSwipe:)];
    leftSwipeGesture.direction=UISwipeGestureRecognizerDirectionLeft;
    [self.view addGestureRecognizer:leftSwipeGesture];
    
    UISwipeGestureRecognizer *rightSwipeGesture=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(rightSwipe:)];
    rightSwipeGesture.direction=UISwipeGestureRecognizerDirectionRight;
    [self.view addGestureRecognizer:rightSwipeGesture];
}

#pragma mark 向左滑动浏览下一张图片
-(void)leftSwipe:(UISwipeGestureRecognizer *)gesture{
    [self transitionAnimation:YES];
}

#pragma mark 向右滑动浏览上一张图片
-(void)rightSwipe:(UISwipeGestureRecognizer *)gesture{
    [self transitionAnimation:NO];
}


#pragma mark 转场动画
-(void)transitionAnimation:(BOOL)isNext{
    UIViewAnimationOptions option;
    if (isNext) {
        option=UIViewAnimationOptionCurveLinear|UIViewAnimationOptionTransitionFlipFromRight;
    }else{
        option=UIViewAnimationOptionCurveLinear|UIViewAnimationOptionTransitionFlipFromLeft;
    }
    
    [UIView transitionWithView:_imageView duration:1.0 options:option animations:^{
        _imageView.image=[self getImage:isNext];
    } completion:nil];
}

#pragma mark 取得当前图片
-(UIImage *)getImage:(BOOL)isNext{
    if (isNext) {
        _currentIndex=(_currentIndex+1)%IMAGE_COUNT;
    }else{
        _currentIndex=(_currentIndex-1+IMAGE_COUNT)%IMAGE_COUNT;
    }
    NSString *imageName=[NSString stringWithFormat:@"%i.jpg",_currentIndex];
    return [UIImage imageNamed:imageName];
}
@end

上面的转场动画演示中,其实仅仅有一个视图UIImageView做转场动画,每次转场通过切换UIImageView的内容而已。如果有两个完全不同的视图,并且每个视图布局都很复杂,此时要在这两个视图之间进行转场可以使用+ (void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^)(BOOL finished))completion NS_AVAILABLE_IOS(4_0)方法进行两个视图间的转场,需要注意的是默认情况下转出的视图会从父视图移除,转入后重新添加,可以通过UIViewAnimationOptionShowHideTransitionViews参数设置,设置此参数后转出的视图会隐藏(不会移除)转入后再显示。

注意:转场动画设置参数完全同基本动画参数设置;同直接使用转场动画不同的是使用UIView的装饰方法进行转场动画其动画效果较少,因为这里无法直接使用私有API。

 

UITouch类

触摸屏幕是iOS设备接受用户输入的主要方式,包括单击、双击、拨动以及多点触摸等,这些操作都会产生触摸事件。

UITouch类中包含5个属性:

             window:触摸产生时所处的窗口。由于窗口可能发生变化,当前所在的窗口不一定是最开始的窗口。

             view:触摸产生时所处的视图。由于视图可能发生变化,当前视图也不一定时最初的视图。

             tapCount:轻击(Tap)操作和鼠标的单击操作类似,tapCount表示短时间内轻击屏幕的次数。因此可以根据tapCount判断单击、双击或更多的轻击。

             timestamp:时间戳记录了触摸事件产生或变化时的时间。单位是秒。

             phase:触摸事件在屏幕上有一个周期,即触摸开始、触摸点移动、触摸结束,还有中途取消。而通过phase可以查看当前触摸事件在一个周期中所处的状态。phase是UITouchPhase类型的,这是一个枚举配型,包含了

·      UITouchPhaseBegan(触摸开始)

·      UITouchPhaseMoved(接触点移动)

·      UITouchPhaseStationary(接触点无移动)

·      UITouchPhaseEnded(触摸结束)

·      UITouchPhaseCancelled(触摸取消)

 

UITouch类中包含如下成员函数:

func locationInView(view:UIView) ->CGPoint  获取一个坐标点,表示触摸在view这个视图上的位置,这里返回的位置是针对view的坐标系的。调用时传入的view参数为空的话,返回的时触摸点在整个窗口的位置。

func previousLocationInView(view:UIView) ->CGPoint 获取前一个坐标值,函数返回也是一个CGPoint类型的值, 表示触摸在view这个视图上的位置,这里返回的位置是针对view的坐标系的。调用时传入的view参数为空的话,返回的时触摸点在整个窗口的位置。

————

当手指接触到屏幕,不管是单点触摸还是多点触摸,事件都会开始,直到用户所有的手指都离开屏幕。期间所有的UITouch对象都被包含在UIEvent事件对象中,由程序分发给处理者。事件记录了这个周期中所有触摸对象状态的变化 

只要屏幕被触摸,系统就会报若干个触摸的信息封装到UIEvent对象中发送给程序,由管理程序UIApplication对象将事件分发。一般来说,事件将被发给主窗口,然后传给第一响应者对象(FirstResponder)处理。

关于响应者的概念,通过以下几点说明:

           响应者对象(Response object)

响应者对象就是可以响应事件并对事件作出处理。在iOS中,存在UIResponder类,它定义了响应者对象的所有方法。UIApplication、UIView等类都继承了UIResponder类,UIWindow和UIKit中的控件因为继承了UIView,所以也间接继承了UIResponder类,这些类的实例都可以当作响应者。

          第一响应者(First responder)

当前接受触摸的响应者对象被称为第一响应者,即表示当前该对象正在与用户交互,它是响应者链的开端。

           响应者链(Responder chain)

响应者链表示一系列的响应者对象。事件被交由第一响应者对象处理,如果第一响应者不处理,事件被沿着响应者链向上传递,交给下一个响应者(next responder)。一般来说,第一响应者是个视图对象或者其子类对象,当其被触摸后事件被交由它处理,如果它不处理,事件就会被传递给它的视图控制器对象(如果存在),然后是它的父视图(superview)对象(如果存在),以此类推,直到顶层视图。接下来会沿着顶层视图(top view)到窗口(UIWindow对象)再到程序(UIApplication对象)。如果整个过程都没有响应这个事件,该事件就被丢弃。一般情况下,在响应者链中只要由对象处理事件,事件就停止传递。但有时候可以在视图的响应方法中根据一些条件判断来决定是否需要继续传递事件。

          管理事件分发

视图对触摸事件是否需要作处回应可以通过设置视图的userInteractionEnabled属性,默认状态为YES。除此之外,当视图被隐藏(setHidden:YES)或者透明(alpha值为0)也不会收事件。不过这个属性只对视图有效,如果想要整个程序都步响应事件,可以调用UIApplication的beginIngnoringInteractionEvents方法来完全停止事件接收和分发。通过endIngnoringInteractionEvents方法来恢复让程序接收和分发事件。

 

如果要让视图接收多点触摸,需要设置它的multipleTouchEnabled属性为true,默认状态下这个属性值为false。

 

首先触摸的对象是视图,而视图的类UIView继承了UIRespnder类,但是要对事件作出处理,还需要重写UIResponder类中定义的事件处理函数。根据不通的触摸状态,程序会调用相应的处理函数,这些函数包括以下几个:

      func touchesBegan (touches:NSSet, withEvent event:UIEvent) 

  func touchesMoved (touches:NSSet, withEvent event:UIEvent) 

  func touchesEnded (touches:NSSet, withEvent event:UIEvent) 

  func touchesCancelled (touches:NSSet, withEvent event:UIEvent) 

            当手指接触屏幕时,就会调用touchesBegan:withEvent方法;

            当手指在屏幕上移时,动就会调用touchesMoved:withEvent方法;

            当手指离开屏幕时,就会调用touchesEnded:withEvent方法;

            当触摸被取消(比如触摸过程中被来电打断),就会调用touchesCancelled:withEvent方法。这几个方法被调用时,正好对应了UITouch类中phase属性的4个枚举值。

             上面的四个事件方法,在开发过程中并不要求全部实现,可以根据需要重写特定的方法。对于这4个方法,都有两个相同的参数:NSSet类型的touches和UIEvent类型的event。其中touches表示触摸产生的所有UITouch对象,而event表示特定的事件。因为UIEvent包含了整个触摸过程中所有的触摸对象,因此可以调用allTouches方法获取该事件内所有的触摸对象,也可以调用touchesForVIew:或者touchesForWindows:取出特定视图或者窗口上的触摸对象。在这几个事件中,都可以拿到触摸对象,然后根据其位置,状态,时间属性做逻辑处理。

 

            例如:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event  

{  

    UITouch *touch =  [touches anyObject];  

if(touch.tapCount == 2)  

    {  

        self.view.backgroundColor = [UIColor redColor];  

    }  

}  

 

            上面的例子说明在触摸手指离开后,根据tapCount点击的次数来设置当前视图的背景色。不管时一个手指还是多个手指,轻击操作都会使每个触摸对象的tapCount加1,由于上面的例子不需要知道具体触摸对象的位置或时间等,因此可以直接调用touches的anyObject方法来获取任意一个触摸对象然后判断其tapCount的值即可。

            检测tapCount可以放在touchesBegan也可以touchesEnded,不过一般后者跟准确,因为touchesEnded可以保证所有的手指都已经离开屏幕,这样就不会把轻击动作和按下拖动等动作混淆。

            轻击操作很容易引起歧义,比如当用户点了一次之后,并不知道用户是想单击还是只是双击的一部分,或者点了两次之后并不知道用户是想双击还是继续点击。为了解决这个问题,一般可以使用“延迟调用”函数。

 

            例如:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event  

{  

    UITouch *touch =  [touches anyObject];  

if(touch.tapCount == 1)  

    {  

        [self performSelector:@selector(setBackground:) withObject:[UIColor blueColor] afterDelay:2];  

        self.view.backgroundColor = [UIColor redColor];  

    }  

}  

 

            上面代码表示在第一次轻击之后,没有直接更改视图的背景属性,而是通过performSelector:withObject:afterDelay:方法设置2秒中后更改。

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event  

{  

    UITouch *touch =  [touches anyObject];  

if(touch.tapCount == 2)  

    {  

        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(setBackground:) object:[UIColor redColor]];  

        self.view.backgroundColor = [UIColor redColor];  

    }  

}  

        双击就是两次单击的组合,因此在第一次点击的时候,设置背景色的方法已经启动,在检测到双击的时候先要把先前对应的方法取消掉,可以通过调用NSObject类的cancelPreviousPerformRequestWithTarget:selector:object方法取消指定对象的方法调用,然后调用双击对应的方法设置背景色为红色。

 

            下面举个例子创建可以拖动的视图,这个主要通过触摸对象的位置坐标来实现。因此调用触摸对象的locationInView:方法即可。

            例如:

CGPoint originalLocation;  

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event  

{  

    UITouch *touch = [touches anyObject];  

    originalLocation = [touch locationInView:self.view];  

}  

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event  

{  

    UITouch *touch = [touches anyObject];  

    CGPoint currentLocation = [touch locationInView:self.view];  

    CGRect frame = self.view.frame;  

    frame.origin.x += currentLocation.x-originalLocation.x;  

    frame.origin.y += currentLocation.y-originalLocation.y;     

    self.view.frame = frame;  

}

 

            这里先在touchesBegan中通过[touch locationInView:self.view]获取手指触摸在当前视图上的位置,用CGPoint变量记录,然后在手指移动事件touchesMoved方法中获取触摸对象当前位置,并通过于与原始位置的差值计算出移动偏移量,再设置当前视图的位置。

实例: 

//对画面进行单次点击时所触发的函式  

 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {  

//宣告一个UITouch的指标来存放事件触发时所撷取到的状态  
 UITouch *touch = [[event allTouches] anyObject];  
//将XY轴的座标资讯正规化后输出  
 touchX.text = [NSString stringWithFormat:@"%0.0f", [touch locationInView:touch.view].x];  
 touchY.text = [NSString stringWithFormat:@"%0.0f", [touch locationInView:touch.view].y];  
 }  
//对画面进行拖曳动做时所触发的函式  

 -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {  

//宣告一个UITouch的指标来存放事件触发时所撷取到的状态  
 UITouch *touch = [[event allTouches] anyObject];  
//将XY轴的座标资讯正规化后输出  
 moveX.text = [NSString stringWithFormat:@"%0.0f", [touch locationInView:touch.view].x];  
 moveY.text = [NSString stringWithFormat:@"%0.0f", [touch locationInView:touch.view].y];  
 }  
//手指离开画面(结束操作)时所触发的函式  

 -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {  

//宣告一个UITouch的指标来存放事件触发时所撷取到的状态  
 UITouch *touch = [[event allTouches] anyObject];  
//取得并输出连点资讯,tapCount可保留一定时间内的连点次数  
 tapCountLabel.text = [NSString stringWithFormat:@"%d", [touch tapCount]];  
 }  

 

CGContextTranslateCTM 用法

CoreGraphics.h
一些常用旋转常量
#define M_E 2.71828182845904523536028747135266250 e

#define M_LOG2E 1.44269504088896340735992468100189214 log 2e

#define M_LOG10E 0.434294481903251827651128918916605082 log 10e

#define M_LN2 0.693147180559945309417232121458176568 log e2

#define M_LN10 2.30258509299404568401799145468436421 log e10

#define M_PI 3.14159265358979323846264338327950288 pi

#define M_PI_2 1.57079632679489661923132169163975144 pi/2

#define M_PI_4 0.785398163397448309615660845819875721 pi/4

#define M_1_PI 0.318309886183790671537767526745028724 1/pi

#define M_2_PI 0.636619772367581343075535053490057448 2/pi

#define M_2_SQRTPI 1.12837916709551257389615890312154517 2/sqrt(pi)
#define M_SQRT2 1.41421356237309504880168872420969808 sqrt(2)

#define M_SQRT1_2 0.707106781186547524400844362104849039 1/sqrt(2)

CGAffineTransformMakeTranslation(width, 0.0);是改变位置的,

 
CGAffineTransformRotate(transform, M_PI);是旋转的。

 
CGAffineTransformMakeRotation(-M_PI);也是旋转的

 
transform = CGAffineTransformScale(transform, -1.0, 1.0);是缩放的。
 

view.transform = CGAffineTransformIdentity;线性代数里面讲的矩阵变换,这个是恒等变换


当你改变过一个view.transform属性或者view.layer.transform的时候需要恢复默认状态的话,记得先把他们重置可以使用view.transform = CGAffineTransformIdentity,或者view.layer.transform = CATransform3DIdentity。
 
假设你一直不断的改变一个view.transform的属性,而每次改变之前没有重置的话,你会发现后来的改变和你想要的发生变化了,不是你真正想要的结果



Quartz转换实现的原理:Quartz把绘图分成两个部分,
用户空间,即和设备无关,
设备空间,
用户空间和设备空间中间存在一个转换矩阵 : CTM
本章实质是讲解CTM

Quartz提供的3大功能
移动,旋转,缩放

演示如下,首先加载一张图片
void CGContextDrawImage (
CGContextRef c,
CGRect rect,
CGImageRef image
);

移动函数
CGContextTranslateCTM (myContext, 100, 50);

旋转函数
include
static inline double radians (double degrees) {return degrees * M_PI/180;}
CGContextRotateCTM (myContext, radians(–45.));

缩放
CGContextScaleCTM (myContext, .5, .75);

翻转, 两种转换合成后的效果,先把图片移动到右上角,然后旋转180度
CGContextTranslateCTM (myContext, w,h);
CGContextRotateCTM (myContext, radians(-180.));

 

 

Quartz 2D编程指南(5)

Quartz 2D 绘制模型定义了两种独立的坐标空间:用户空间(用于表现文档页)和设备空间(用于表现设备的本地分辨率)。用户坐标空间用浮点数表示坐标,与设备空间的像素分辨率没有关系。当我们需要一个点或者显示文档时, Quartz会将用户空间坐标系统映射到设备空间坐标系统。因此,我们不需要重写应用程序或添加额外的代码来调整应用程序的输出以适应不同的设备。
我们可以通过操作CTM(current transformation matrix)来修改默认的用户空间。在创建图形上下文后,CTM是单位矩阵,我们可以使用 Quartz的变换函数来修改CTM,从而修改用户空间中的绘制操作。
本章内容包括:
  • 变换操作函数概览
  • 如何修改CTM
  • 如何创建一个仿射变换
  • 如何选择两个相同的变换
  • 如何获取user-to-device-space变换
Quartz变换函数
我们可能使用Quartz内置的变换函数方便的平移、旋转和缩放我们的绘图。只需要短短几行代码,我们便可以按顺序应用变换或结合使用变换。图5-1显示了缩放和旋转一幅图片的效果。我们使用的每个变换操作都更新了CTM。CTM总是用于表示用户空间和设备空间的当前映射关系。这种映射确保了应用程序的输出在任何显示器或打印机上看上去都很棒。

iOS开发之让你的应用“动”起来(swift)_第21张图片 

Quartz 2D API提供了5个函数,以允许我们获取和修改CTM。我们可以旋转、平移、缩放CTM。我们还可以联结一个仿射变换矩阵。
有时我们可以不想操作用户空间,直到我们决定将变换应用到CTM时,Quartz为此允许我们创建应用于此的仿射矩阵。我们可以使用另外一组函数来创建仿射变换,这些变换可以与CTM联结在一起。
我们可以不需要了解矩阵的数学含义而使用这些函数。
修改CTM
我们在绘制图像前操作CTM来旋转、缩放或平移page,从而变换我们将要绘制的对象。以变换CTM之前,我们需要保存图形状态,以便绘制后能恢复。我们同样能用仿射矩阵来联结CTM。在本节中,我们将介绍与CTM函数相关的四种操作--平移、旋转、缩放和联结。
假设我们提供了一个可用的图形上下文、一个指向可绘制图像的矩形的指针和一个可用的CGImage对象,则下面一行代码绘制了一个图像。该行代码可以绘制如图5-2所示的图片。在阅读了本节余下的部分后,我们将看到如何将变换应用于图像。
 
复制代码
  1. CGContextDrawImage (myContext, rect, myImage);


iOS开发之让你的应用“动”起来(swift)_第22张图片 

平移变换根据我们指定的x, y轴的值移动坐标系统的原点。我们通过调用CGContextTranslateCTM函数来修改每个点的x, y坐标值。如图5-3显示了一幅图片沿x轴移动了100个单位,沿y轴移动了50个单位。具体代码如下:
 
复制代码
  1. CGContextTranslateCTM (myContext, 100, 50);


iOS开发之让你的应用“动”起来(swift)_第23张图片 

旋转变换根据指定的角度来移动坐标空间。我们调用CGContextRotateCTM函数来指定旋转角度(以弧度为单位)。图5-4显示了图片以原点(左下角)为中心旋转45度,代码所下所示:
 
复制代码
  1. CGContextRotateCTM (myContext, radians(–45.));


iOS开发之让你的应用“动”起来(swift)_第24张图片 

由于旋转操作使图片的部分区域置于上下文之外,所以区域外的部分被裁减。我们用弧度来指定旋转角度。如果需要进行旋转操作,下面的代码将会很有用
 
复制代码
  1. #include
  2. static inline double radians (double degrees) {return degrees * M_PI/180;}
缩放操作根据指定的x, y因子来改变坐标空间的大小,从而放大或缩小图像。x, y因子的大小决定了新的坐标空间是否比原始坐标空间大或者小。另外,通过指定x因子为负数,可以倒转x轴,同样可以指定y因子为负数来倒转y轴。通过调用CGContextScaleCTM函数来指定x, y缩放因子。图5-5显示了指定x因子为0.5,y因子为0.75后的缩放效果。代码如下:
 
复制代码
  1. CGContextScaleCTM (myContext, .5, .75);


iOS开发之让你的应用“动”起来(swift)_第25张图片 

联合变换将两个矩阵相乘来联接现价变换操作。我们可以联接多个矩阵来得到一个包含所有矩阵累积效果矩阵。通过调用CGContextConcatCTM来联接CTM和仿射矩阵。
另外一种得到累积效果的方式是执行两个或多个变换操作而不恢复图形状态。图5-6显示了先平移后旋转一幅图片的效果,代码如下:

 
复制代码
  1. CGContextTranslateCTM (myContext, w,h);
  2. CGContextRotateCTM (myContext, radians(-180.));


iOS开发之让你的应用“动”起来(swift)_第26张图片 

图5-7显示了平移、缩放和旋转一幅图片,代码如下:

 
复制代码
  1. CGContextTranslateCTM (myContext, w/4, 0);
  2. CGContextScaleCTM (myContext, .25, .5);
  3. CGContextRotateCTM (myContext, radians ( 22.));


iOS开发之让你的应用“动”起来(swift)_第27张图片 

变换操作的顺序会影响到最终的效果。如果调换顺序,将得到不同的结果。调换上面代码的顺序将得到如图5-8所示的效果,代码如下:

 
复制代码
  1. CGContextRotateCTM (myContext, radians ( 22.));
  2. CGContextScaleCTM (myContext, .25, .5);
  3. CGContextTranslateCTM (myContext, w/4, 0);


iOS开发之让你的应用“动”起来(swift)_第28张图片 

创建仿射变换
仿射变换操作在矩阵上,而不是在CTM上。我们可以使用这些函数来构造一个之后用于CTM(调用函数CGContextConcatCTM)的矩阵。仿射变换函数使用或者返回一个CGAffineTransform数据对象。我们可以构建简单或复杂的仿射变换。
仿射变换函数能实现与CTM函数相同的操作--平移、旋转、缩放、联合。表5-1列出了仿射变换函数及其用途。注意每种变换都有两个函数。
表5-1 仿射变换函数
函数
用途
CGAffineTransformMakeTranslation
通过指定x, y值来创建一个平移矩阵
CGAffineTransformTranslate
在已存在的矩阵中使用平移
CGAffineTransformMakeRotation
通过指定角度来创建一个旋转矩阵
CGAffineTransformRotate
在已存在的矩阵中使用旋转
CGAffineTransformMakeScale
通过指定x, y缩放因子来创建一个缩放矩阵
CGAffineTransformScale
在已存在的矩阵中使用缩放
Quartz同样提供了一个仿射变换函数(CGAffineTransformInvert)来倒置矩阵。倒置操作通常用于在变换对象中提供点的倒置变换。当我们需要恢复一个被矩阵变换的值时,可以使用倒置操作。将值与倒置矩阵相乘,就可得到原先的值。我们通常不需要倒置操作,因为我们可以通过保存和恢复图形状态来倒置CTM的效果。
在一些情况下,我们可能不需要变换整修空间,而只是一个点或一个大小。我们通过调用CGPointApplyAffineTransform在CGPoint结构上执行变换操作。调用CGSizeApplyAffineTransform在CGSize结构上执行变换操作。调用CGRectApplyAffineTransform在CGRect结构上执行变换操作。CGRectApplyAffineTransform返回一个最小的矩形,该矩形包含了被传递给CGRectApplyAffineTransform的矩形对象的角点。如果矩形上的仿射变换操作只有缩放和平移操作,则返回的矩形与四个变换后的角组成的矩形是一致的。
可以通过调用函数CGAffineTransformMake来创建一个新的仿射变换,但与其它函数不同的是,它需要提供一个矩阵实体。


评价仿射变换
我们可以通过调用CGAffineTransformEqualToTransform函数来决定一个仿射变换是否与另一个相同。如果两个变换相同,则返回true;否则返回false。
函数CGAffineTransformIsIdentity用于确认一个变换是否是单位变换。单位变换没有平移、缩放和旋转操作。Quartz常量CGAffineTransformIdentity表示一个单位变换。
获取用户空间到设备空间的变换
当使用Quartz 2D时,我们只是在用户空间下工作。Quartz为我们处理用户空间和设备空间的转换。如果我们的应用程序需要获取Quartz转换用户空间和设备空间的仿射变换,我们可以调用函数CGContextGetUserSpaceToDeviceSpaceTransform。
Quartz提供了一系列的函数来转换用户空间和设备空间的几何体。我们会发现这些函数使用赶来比使用CGContextGetUserSpaceToDeviceSpaceTransform函数返回的仿射变换更好用。
  • 点:函数CGContextConvertPointToDeviceSpace和CGContextConvertPointToUserSpace将一个CGPoint数据结构从一个空间变换到另一个空间。
  • 大小:函数CGContextConvertSizeToDeviceSpace和CGContextConvertSizeToUserSpace将一个CGSize数据结构从一个空间变换到另一个空间。
  • 矩形:函数CGContextConvertRectToDeviceSpace和CGContextConvertRectToUserSpace将一个CGPoint数据结构从一个空间变换到另一个空间。
 
 
CALayer简单教程

首先要说的是CALayers 是屏幕上的一个具有可见内容的矩形区域,每个UIView都有一个根CALayer,其所有的绘制(视觉效果)都是在这个layer上进行的。(译者注:为验证这点,我写下了如下代码:

1
2
3
4
5
6
7
8
9
10
UILabel* lable = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, 100, 30)];
lable.text = @"test";
[self.view addSubview: lable];
lable.backgroundColor = [UIColor clearColor];
[lable release];

// 设定CALayer
self.view.layer.backgroundColor =[UIColor orangeColor].CGColor;
self.view.layer.cornerRadius =20.0;
self.view.layer.frame = CGRectInset(self.view.layer.frame, 20, 20);

请注意,我创建的UILable始终随着UIView的根CALayer的缩放而改变位置。)

其次,CALayer的可以影响其外观的特性有:

  • 层的大小尺寸
  • 背景色
  • 内容(比如图像或是使用Core Graphics绘制的内容)
  • 是否使用圆角
  • 是否使用阴影
  • 等等

需要说明的是CALayer的大部分属性都可以用来实现动画效果。

另外,你可以直接使用CALayer,也可以使用其子类,如CAGradientLayer,CATextLayer, CAShapeLayer等等。

示例

首先在Xcode中创建一个View-based App,CALayer是属于QuartzCore framework的,所以需要引入QuartzCore framework,另外在程序中包括QuartzCore.h

第一个例子是创建一个带圆角的层,在你的ViewController中的ViewDidLoad中加入下面代码:

1
2
3
4
5
6
7
// Import QuartzCore.h at the top of the file
#import

// Uncomment viewDidLoad and add the following lines
self.view.layer.backgroundColor =[UIColor orangeColor].CGColor;
self.view.layer.cornerRadius =20.0;
self.view.layer.frame = CGRectInset(self.view.layer.frame, 20, 20);

 

 

然后添加一个带阴影效果的子层,加入下列代码:

1
2
3
4
5
6
7
8
CALayer *sublayer = [CALayer layer];
sublayer.backgroundColor = [UIColor blueColor].CGColor;
sublayer.shadowOffset = CGSizeMake(0, 3);
sublayer.shadowRadius = 5.0;
sublayer.shadowColor = [UIColor blackColor].CGColor;
sublayer.shadowOpacity = 0.8;
sublayer.frame = CGRectMake(30, 30, 128, 192);
[self.view.layer addSublayer:sublayer];


 

为子层增加内容(图片),你还可以设置层的边框,代码如下:

1
2
3
sublayer.contents =(id)[UIImage imageNamed:@"BattleMapSplashScreen.png"].CGImage;
sublayer.borderColor =[UIColor blackColor].CGColor;
sublayer.borderWidth =2.0;


 

如果你希望子层也是圆角怎么办?你可能说很容易设置cornerRadius属性就行。实际上你即算是设置了cornerRadius属性,图片仍然不会显示圆角。你还需要设置masksToBounds为YES。但是这样做还是不够的,因为如果是这样,这个层的阴影显示就没有了。简单的实现方法如下(通过两个层来实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CALayer *sublayer =[CALayer layer];
sublayer.backgroundColor =[UIColor blueColor].CGColor;
sublayer.shadowOffset = CGSizeMake(0, 3);
sublayer.shadowRadius =5.0;
sublayer.shadowColor =[UIColor blackColor].CGColor;
sublayer.shadowOpacity =0.8;
sublayer.frame = CGRectMake(30, 30, 128, 192);
sublayer.borderColor =[UIColor blackColor].CGColor;
sublayer.borderWidth =2.0;
sublayer.cornerRadius =10.0;
[self.view.layer addSublayer:sublayer];

CALayer *imageLayer =[CALayer layer];
imageLayer.frame = sublayer.bounds;
imageLayer.cornerRadius =10.0;
imageLayer.contents =(id)[UIImage imageNamed:@"BattleMapSplashScreen.png"].CGImage;
imageLayer.masksToBounds =YES;
[sublayer addSublayer:imageLayer];

 

最后,还介绍一下自绘图型的实现,其要点是要设置所绘制层的delegate。比如在我们的例子中使用ViewController作为delegate,那么就需要在ViewController中实现drawLayer:inContext方法,对层进行绘制工作。另外,还需要调用setNeedsDisplay,来通知层需要进行绘制了,于是层才会通过对delegate的drawLayer:inContext方法进行调用。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void MyDrawColoredPattern (void*info, CGContextRef context){

CGColorRef dotColor =[UIColor colorWithHue:0 saturation:0 brightness:0.07 alpha:1.0].CGColor;
CGColorRef shadowColor =[UIColor colorWithRed:1 green:1 blue:1 alpha:0.1].CGColor;

CGContextSetFillColorWithColor(context, dotColor);
CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 1, shadowColor);

CGContextAddArc(context, 3, 3, 4, 0, radians(360), 0);
CGContextFillPath(context);

CGContextAddArc(context, 16, 16, 4, 0, radians(360), 0);
CGContextFillPath(context);

}

-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context {

CGColorRef bgColor =[UIColor colorWithHue:0.6 saturation:1.0 brightness:1.0 alpha:1.0].CGColor;
CGContextSetFillColorWithColor(context, bgColor);
CGContextFillRect(context, layer.bounds);

staticconst CGPatternCallbacks callbacks ={0, &MyDrawColoredPattern, NULL};

CGContextSaveGState(context);
CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
CGContextSetFillColorSpace(context, patternSpace);
CGColorSpaceRelease(patternSpace);

CGPatternRef pattern = CGPatternCreate(NULL,
layer.bounds,
CGAffineTransformIdentity,
24,
24,
kCGPatternTilingConstantSpacing,
true,
&callbacks);
CGFloat alpha =1.0;
CGContextSetFillPattern(context, pattern, &alpha);
CGPatternRelease(pattern);
CGContextFillRect(context, layer.bounds);
CGContextRestoreGState(context);
}

还需要注意,radians是一个自定义函数:

1
static inline double radians (double degrees) { return degrees * M_PI/180}

 CGContext小记

0  CGContextRef context = UIGraphicsGetCurrentContext(); 设置上下文

1 CGContextMoveToPoint 开始画线

2 CGContextAddLineToPoint 画直线

4 CGContextAddEllipseInRect 画一椭圆

4 CGContextSetLineCap 设置线条终点形状

4 CGContextSetLineDash 画虚线

4 CGContextAddRect 画一方框

4 CGContextStrokeRect 指定矩形

4 CGContextStrokeRectWithWidth 指定矩形线宽度

4 CGContextStrokeLineSegments 一些直线

5 CGContextAddArc 画已曲线 前俩店为中心 中间俩店为起始弧度 最后一数据为0则顺时针画 1则逆时针

5 CGContextAddArcToPoint(context,0,0, 2, 9, 40);//先画俩条线从point 到 弟1点 , 从弟1点到弟2点的线  切割里面的圆

6 CGContextSetShadowWithColor 设置阴影

7 CGContextSetRGBFillColor 这只填充颜色

7 CGContextSetRGBStrokeColor 画笔颜色设置

7 CGContextSetFillColorSpace 颜色空间填充

7 CGConextSetStrokeColorSpace 颜色空间画笔设置

8 CGContextFillRect 补充当前填充颜色的rect

8 CGContextSetAlaha 透明度

9 CGContextTranslateCTM 改变画布位置

10 CGContextSetLineWidth 设置线的宽度

11 CGContextAddRects 画多个线

12 CGContextAddQuadCurveToPoint 画曲线

13  CGContextStrokePath 开始绘制图片

13 CGContextDrawPath 设置绘制模式

14 CGContextClosePath 封闭当前线路

15 CGContextTranslateCTM(context, 0, rect.size.height);    CGContextScaleCTM(context, 1.0, -1.0);反转画布

16 CGContextSetInterpolationQuality 背景内置颜色质量等级

16 CGImageCreateWithImageInRect 从原图片中取小图

17 字符串的 写入可用  nsstring本身的画图方法 - (CGSize)drawInRect:(CGRect)rect withFont:(UIFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment;来写进去即可

18对图片放大缩小的功能就是慢了点 

    UIGraphicsBeginImageContext(newSize);

    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();

  UIGraphicsEndImageContext();

 

19 CGColorGetComponents() 返回颜色的各个直 以及透明度 可用只读const float 来接收  是个数组

20 画图片 CGImageRef image=CGImageRetain(img.CGImage);

     CGContextDrawImage(context, CGRectMake(10.0, height -              

     100.0, 90.0, 90.0), image);

 

21 实现逐变颜色填充方法 CGContextClip(context);

    CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();

    CGFloat colors[] =

    {

        204.0 / 255.0, 224.0 / 255.0, 244.0 / 255.0, 1.00,

        29.0 / 255.0, 156.0 / 255.0, 215.0 / 255.0, 1.00,

        0.0 / 255.0,  50.0 / 255.0, 126.0 / 255.0, 1.00,

    };

    CGGradientRef gradient = CGGradientCreateWithColorComponents       

   (rgb, colors, NULL, sizeof(colors)/(sizeof(colors[0])*4));

    CGColorSpaceRelease(rgb);    

    CGContextDrawLinearGradient(context, gradient,CGPointMake    

   (0.0,0.0) ,CGPointMake(0.0,self.frame.size.height),                    

     kCGGradientDrawsBeforeStartLocation);

    

22 注:  画完图后,必须 

    先用CGContextStrokePath来描线,即形状 

    后用CGContextFillPath来填充形状内的颜色. 

填充一个路径的时候,路径里面的子路径都是独立填充的。

假如是重叠的路径,决定一个点是否被填充,有两种规则

1,nonzero winding number rule:非零绕数规则,假如一个点被从左到右跨过,计数器+1,从右到左跨过,计数器-1,最后,如果结果是0,那么不填充,如果是非零,那么填充。

2,even-odd rule: 奇偶规则,假如一个点被跨过,那么+1,最后是奇数,那么要被填充,偶数则不填充,和方向没有关系。

 Function

Description 

 CGContextEOFillPath

 使用奇偶规则填充当前路径

 CGContextFillPath

 使用非零绕数规则填充当前路径

 CGContextFillRect

 填充指定的矩形

 CGContextFillRects

 填充指定的一些矩形

 CGContextFillEllipseInRect

 填充指定矩形中的椭圆

 CGContextDrawPath

 两个参数决定填充规则,kCGPathFill表示用非零绕数规则,kCGPathEOFill表示用奇偶规则,kCGPathFillStroke表示填充,kCGPathEOFillStroke表示描线,不是填充

 

设置当一个颜色覆盖上另外一个颜色,两个颜色怎么混合

默认方式是

result = (alpha * foreground) + (1 - alpha) * background

CGContextSetBlendMode :设置blend mode.

CGContextSaveGState :保存blend mode.

CGContextRestoreGState:在没有保存之前,用这个函数还原blend mode.

CGContextSetBlendMode 混合俩种颜色

 

 

IOS开发系列--IOS程序开发概览

IOS开发系列--无限循环的图片浏览器

iOS开发系列--UITableView全面解析

iOS开发系列--视图切换

iOS开发系列--触摸事件、手势识别、摇晃事件、耳机线控

iOS开发系列--打造自己的“美图秀秀”

 

作品采用知识共享署名 2.5 中国大陆许可协议进行许可,欢迎转载,演绎或用于商业目的。但转载请注明来自崔江涛(KenshinCui),并包含相关链接。

你可能感兴趣的:(swift)