Swift: 深入理解Core Animation(一)


如果想在底層做一些改變,想實現一些特別的動畫,這時除了學習Core Animation之外,別無選擇。

 

最近在看《iOS Core Animation:Advanced Techniques》這本書籍,尚有所收獲,並將之記錄下來。

  1.  CALayer
    如果將UIView說成是視圖,那么CALayer就是圖層了。每一個 UIView 的身后對應一個 Core Animation 框架中的 CALayer;每一個 CALayer 都是 UIView 的代理。可以嘗試運行下面的代碼,會發現打印是一樣的內存地址:

    let testView = UIView()
    print("\(testView)")
    print("\(testView.layer.delegate!)")
    

     在iOS開發中,處理的一個又一個UIView,實際是在操作CALayer。那么為什么不直接對CALayer進行編程呢?那是因為CALayer繼承自NSObject,主要是用於圖層的處理以及動畫,而UIView繼承自UIResponder,可以處理交互事件。

    由此,可以認為UIView就是對CALayer的一個簡單封裝,圖像繪制、動畫都是CALayer做的。有過開發經驗的朋友都知道,蘋果在UIView里面封裝了一套動畫接口,但是利用這些接口,只是可以做一些簡單、不靈活的動畫。如果想在底層做一些改變,想實現一些特別的動畫,這是除了學習Core Animation之外,別無選擇。


    A. CALayer的簡單使用
        這一部分內容開發中經常用到,可以說是基礎的東西。代碼:
        

            let testView = UIView(frame: CGRectMake(100, 100, 100, 100))
            testView.backgroundColor = UIColor.blueColor()
            self.view.addSubview(testView)
            //設置圓角
            testView.layer.cornerRadius = testView.frame.width / 2.0
    //        testView.layer.masksToBounds = true
            
            //設置邊框
            testView.layer.borderWidth = 5
            testView.layer.borderColor = UIColor.redColor().CGColor
            
            //設置陰影,這里需要注意,如果masksToBounds為true,陰影會被裁剪掉(所以,一般如果你既想要圓形圖片,又想要陰影,那么是需要將圖片進行裁剪的)
            testView.layer.shadowOpacity = 0.5
            testView.layer.shadowOffset = CGSizeMake(0, 2)
            testView.layer.shadowRadius = 5
            
            //需要注意的是這個屬性: shadowPath   它是CGPathRef類型,可以用自定義陰影樣式,比如說,它默認是圓形的陰影,現在將之變成方形陰影
            let path = CGPathCreateMutable()
            CGPathAddRect(path, nil, testView.bounds)
            testView.layer.shadowPath = path
            
            //如果是oc的話,需要注意釋放path,因為它是core Graphics的東西,OC的ARC機制並不會對它進行內存管理,但是Swift對它自動進行了,所以在Swift中不需要寫這個代碼
    //        CGPathRelease(path)
    

     
    B. 自定義CALayer
        CALayer有一個contents屬性,它在OC中是id類型,在Swift中是AnyObject類型,這意味着它可以是任何類型對象。但在實踐中,如果給這個屬性賦值的不是CGImage類型,圖層會是一片空白。它的這個奇怪的現象是因為MAC OS原因造成的,在MAC OS系統上,給它賦值CGImage或者NSImage都是起作用的,但是在iOS系統,如果將UIImage賦值給它,圖層只會是一片空白。(注意,視圖指的是UIView,圖層指的是CALayer)

         在OC中,如果直接將這么做,會產生編譯錯誤:

        UIImage *image = [UIImage imageNamed:@"1"];
        self.view.layer.contents = image.CGImage;
    

         正確的寫法是:

        UIImage *image = [UIImage imageNamed:@"1"];
        self.view.layer.contents = (__bridge id _Nullable)(image.CGImage);
    

         

        在OC中,事實上真正要賦值的類型應該是CGImageRef,它是一個指向CGImage的指針,而CGImageRef並不是Cocoa對象,而是一個Core Foundation類型,所以需要進行橋接轉換。
        但是在Swift中沒必要這么麻煩,直接用CGImage賦值即可,我猜想這是蘋果在底層幫忙轉換了,代碼:
        

        func drawImageOnLayer() {
            let image = UIImage(named: "1")
            self.blueView.layer.contents = image?.CGImage
            /**
            *  相當於UIImageView的contentMode屬性,可以設置圖片顯示樣式
                kCAGravityCenter
                kCAGravityTop
                kCAGravityBottom
                kCAGravityLeft
                kCAGravityRight
                kCAGravityTopLeft
                kCAGravityTopRight
                kCAGravityBottomLeft
                kCAGravityBottomRight
                kCAGravityResize
                kCAGravityResizeAspect
                kCAGravityResizeAspectFill
            */
            //相當於UIImageView的contentMode屬性,可以設置圖片顯示樣式
            self.blueView.layer.contentsGravity = kCAGravityResizeAspect
            
            
            
            //contentsScale定義了CGImage的像素尺寸和視圖大小比例,默認情況下為1.0
            //如何理解這句話,我是這么理解的,屏幕有非retina屏幕(像素和尺寸是1:1)
            //還有retina屏幕,像素和尺寸比是2:1
            //現在還出現了@3x圖片,像素和尺寸比理論上是3:1,但實際上在顯示的時候,蘋果進行了調整(具體可以看ios9的新特性)
            //那么,像我下面這么寫,就是說像素點按照屏幕來調整,如果是非retaina,那么就是1:1,如果是retina,就是2:1
            self.blueView.layer.contentsScale = UIScreen.mainScreen().scale
            
            self.blueView.layer.masksToBounds = true
        }
    

     
        其實自定義CALayer也是很簡單的事情,代碼如下:

        

        func drawCircleOnLayer() {
            let layer = CALayer()
            layer.frame = CGRectMake(50.0, 50.0, 100.0, 100.0)
            layer.backgroundColor = UIColor.blueColor().CGColor
            
            layer.delegate = self
    //        layer.contentsScale = UIScreen.mainScreen().scale
            self.view.layer.addSublayer(layer)
            
            layer.display()
        }
        
        //MARK:- CALayerDelegate
        override func drawLayer(layer: CALayer, inContext ctx: CGContext) {
            CGContextSetLineWidth(ctx, 10)
            CGContextSetStrokeColorWithColor(ctx, UIColor.redColor().CGColor)
            CGContextAddEllipseInRect(ctx, layer.bounds)
            CGContextStrokePath(ctx)
        }
    

         上面的代碼通過使用Core Graphics來繪圖。CALayer有一個delegate屬性,實現了CALayerDelegate協議,但是這個協議是個非正式的協議,可以看到CALayer的這個屬性的聲明:

    weak public var delegate: AnyObject?
    

         然后在這個方法里面繪制想要的圖像,從而達到自定義CALayer的效果:

    override func drawLayer(layer: CALayer, inContext ctx: CGContext) {
            CGContextSetLineWidth(ctx, 10)
            CGContextSetStrokeColorWithColor(ctx, UIColor.redColor().CGColor)
            CGContextAddEllipseInRect(ctx, layer.bounds)
            CGContextStrokePath(ctx)
        }
    

        UIView有一個drawRect:方法,在這個方法里面拿到當前的CGContext,進行繪畫。它的底層還是通過調用CALayer的一系列方法繪制東西。

     C. CALayer的布局   

        UIView有frame、bounds、center這三個重要的布局屬性,對應的CALayer也有frame、bounds、position這三個布局屬性。layer的position與view的center代表的是同樣的值。
        如下圖的關系:
        

        anchorPoint(錨點)這個屬性必須要好好說說。anchorPoint決定了view和layer的顯示位置,它的取值是CGPoint(0~~1, 0~~1)。默認是為(0.5, 0.5),默認意味着它就在中點。那么,如果它不在中點呢?如下圖:



        注意到,當anchorPoint發生改變的時候,frame改變了,但是position並不會發生變化。也就是說,anchorPoint決定了view和layer的位置如何顯示。
        那么,在生命場合需要改變anchorPoint呢?
        
        加入現在需要做一個鍾表的應用,有三根針,時針、分針、秒針,他們需要轉動,轉動過程中需要動畫。如果anchorPoint為默認值,也就是中點的話,會如下面這樣旋轉:



        圍繞着三根針的重點旋轉,這明顯不是我們所想要。但如果將anchorPoint設置為(0.5, 0.9)就不一樣了,它會這么旋轉:




    代碼:

    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var hourView: UIImageView!
        
        @IBOutlet weak var minuteView: UIImageView!
        
        @IBOutlet weak var secondView: UIImageView!
        
        @IBOutlet weak var imageView: UIImageView!
        var timer: NSTimer?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
            self.view.insertSubview(imageView, atIndex: 0)
            self.hourView.layer.anchorPoint = CGPointMake(0.5, 0.9)
            self.minuteView.layer.anchorPoint = CGPointMake(0.5, 0.9)
            self.secondView.layer.anchorPoint = CGPointMake(0.5, 0.9)
            
            self.addTimer()
        }
        
        func addTimer() {
            self.updateWithAnimate(false)
            self.timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("tick"), userInfo: nil, repeats: true)
            NSRunLoop.mainRunLoop().addTimer(self.timer!, forMode: NSRunLoopCommonModes)
            
        }
        
        func removeTimer() {
            self.timer!.invalidate()
            self.timer = nil
        }
        
        func tick() {
            self.updateWithAnimate(true)
        }
        
        @objc func updateWithAnimate(animated: Bool) {
            let calendar = NSCalendar.currentCalendar()
            
            let unit: NSCalendarUnit = [NSCalendarUnit.Hour, NSCalendarUnit.Minute, NSCalendarUnit.Second]
            let nowComponent = calendar.components(unit, fromDate: NSDate())
            
            let hourAngle = CGFloat(nowComponent.hour) / 12.0 * CGFloat(M_PI) * 2.0
            let minuteAngle = CGFloat(nowComponent.minute) / 60.0 * CGFloat(M_PI) * 2.0
            let secondAngle = CGFloat(nowComponent.second) / 60.0 * CGFloat(M_PI) * 2.0
            
    //        print("\(hourAngle)   \(minuteAngle)   \(secondAngle)  \(M_PI)")
            
            self.setAngle(hourAngle, forView: hourView, animated: animated)
            self.setAngle(minuteAngle, forView: minuteView, animated: animated)
            self.setAngle(secondAngle, forView: secondView, animated: animated)
        }
        
        func setAngle(angle: CGFloat, forView view: UIView, animated: Bool) {
            var transform: CATransform3D = CATransform3DMakeRotation(angle, 0, 0, 1)
            if (animated) {
                let basicAnimate = CABasicAnimation(keyPath: "transform")
                basicAnimate.toValue = NSValue(CATransform3D: transform)
                
                self.updateWithAnimate(false)
                
                basicAnimate.duration = 0.5
                basicAnimate.delegate = self
                basicAnimate.fillMode = kCAFillModeForwards
    //            basicAnimate.removedOnCompletion = false
                basicAnimate.setValue(view, forKeyPath: "view")
                view.layer.addAnimation(basicAnimate, forKey: nil)
            }
            else {
                view.layer.transform = transform
            }
        }
        
        override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
    //        print("animationDidStop")
            let view = anim.valueForKeyPath("view") as! UIView
            let tempAnim = anim as? CABasicAnimation
            view.layer.transform = (tempAnim?.toValue?.CATransform3DValue)!
        }
        
        deinit {
            self.removeTimer()
        }
    }
    

         (它的github地址:https://github.com/wzpziyi1/Core-Animation-Test-One)

        這個Demo里面的所用到的動畫,在后面會詳細提及。在這里,主要是需要知道anchorPoint在做動畫時的巨大價值。

    D. CALayer處理事件
        與UIView類似,CALayer也提供了一系列的坐標轉換方法,使用這些方法,可以使得我們可以較為方便的處理layer層面的事件響應:
        

    public func convertPoint(p: CGPoint, fromLayer l: CALayer?) -> CGPoint
    public func convertPoint(p: CGPoint, toLayer l: CALayer?) -> CGPoint
    public func convertRect(r: CGRect, fromLayer l: CALayer?) -> CGRect
    public func convertRect(r: CGRect, toLayer l: CALayer?) -> CGRect
    

     需要着重提下hitTest:方法,它使得layer可以進行事件處理,這么說吧,正是因為它的存在,才可以判斷,一個點擊或者觸摸等事件是發生在哪個layer上面。示例代碼如下:

    import UIKit
    
    class ViewController: UIViewController, UIAlertViewDelegate {
    
        weak private var redLayer: CALayer!
        weak private var blueLayer: CALayer!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
            
            let redLayer = CALayer()
            redLayer.frame = CGRectMake(100, 50, 100, 100)
            redLayer.backgroundColor = UIColor.redColor().CGColor
            self.view.layer.addSublayer(redLayer)
            self.redLayer = redLayer
            
            let blueLayer = CALayer()
            blueLayer.frame = CGRectMake(100, 200, 100, 100)
            blueLayer.backgroundColor = UIColor.blueColor().CGColor
            self.view.layer.addSublayer(blueLayer)
            self.blueLayer = blueLayer
        }
    
        override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
            let touch = touches.first
            let point = touch?.locationInView(self.view)
            
            let layer = self.view.layer.hitTest(point!)
            
            if (layer == self.redLayer) {
                let alertView = UIAlertView(title: "點擊了redLayer", message: "", delegate: self, cancelButtonTitle: nil, otherButtonTitles: "確定")
                alertView.show()
            }
            else if (layer == self.blueLayer) {
                let alertView = UIAlertView(title: "點擊了blueLayer", message: "", delegate: self, cancelButtonTitle: nil, otherButtonTitles: "確定")
                alertView.show()
            }
        }
        
    }
    

     

  2. 動畫的基礎知識

    這里會說一些動畫軌跡的具體實現,補充UIBezierPath的使用。如果想要你的動畫沿着所寫的曲線運動,那么這些基礎知識是不可缺少的。

    A. UIBezierPath
        如果熟悉Core Graphics,那么就可以很快掌握它的用法,實際上它就是對Core Graphics的一個封裝。示例代碼:
    import UIKit
    
    class ZYView: UIView {
    
        override func drawRect(rect: CGRect) {
            
            let path = UIBezierPath()
            path.addArcWithCenter(CGPointMake(150, 150), radius: 50, startAngle: 0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
            path.lineWidth = 5
            UIColor.blueColor().setStroke()
            UIColor.redColor().setFill()
            path.stroke()
            path.fill()
        }
    
    }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
    //        redView.backgroundColor = UIColor.redColor()
    //        self.view.addSubview(redView)
            
            self.view.addSubview(ZYView(frame: CGRectMake(100, 100, 300, 300)))
        }
    

     可以得到下面的界面:

        那么,現在加入有一個需求,一張圖片想要繞着圓轉一圈的動畫,如何實現?其實只需要定義好運動的軌跡,然后告訴animate給我按照這個軌跡進行運動即可。代碼:

    import UIKit
    
    class ViewController: UIViewController {
    
        var redView: UIView = UIView(frame: CGRectMake(40, 200, 50, 50))
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
            redView.backgroundColor = UIColor.redColor()
            self.view.addSubview(redView)
        }
        
        override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
            self.animate1()
        }
        
        func animate1() {
            
    
            redView.layer.cornerRadius = redView.frame.width / 2
            redView.layer.anchorPoint = CGPointMake(0.5, 0.5)
            
            let keyFrameAnimate = CAKeyframeAnimation(keyPath: "position")
            
            let path = UIBezierPath()
            path.addArcWithCenter(CGPointMake(150, 150), radius: 100, startAngle: 0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
            keyFrameAnimate.path = path.CGPath
            
            keyFrameAnimate.duration = 3
            keyFrameAnimate.fillMode = kCAFillModeForwards
            keyFrameAnimate.delegate = self
            keyFrameAnimate.removedOnCompletion = false
            keyFrameAnimate.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
            
            self.redView.layer.addAnimation(keyFrameAnimate, forKey: "aaa")
        }
        
    }
    

        可以看到這樣的代碼:

            let path = UIBezierPath()
            path.addArcWithCenter(CGPointMake(150, 150), radius: 100, startAngle: 0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
            keyFrameAnimate.path = path.CGPath
    

         正是上面提到的,給animate提供了一條運動軌跡。
         類似的,沿着圓運動的軌跡已經可以定義了,那么沿着四邊形運動的軌跡也是相似的。那么,沿着曲線運動的軌跡如何定義呢?

    B. 貝塞爾曲線
        如果你想要定義曲線運動的軌跡,那么這個知識點是必須學習的。

        這是我看到的一份總結貝塞爾曲線很好的博客:http://blog.csdn.net/guo_hongjun1611/article/details/7842110

        下面的關於曲線的總結也是轉載自上面的博客。

    Bézier curve(貝塞爾曲線)是應用於二維圖形應用程序的數學曲線。 曲線定義:起始點、終止點(也稱錨點)、控制點。通過調整控制點,貝塞爾曲線的形狀會發生變化。 1962年,法國數學家Pierre Bézier第一個研究了這種矢量繪制曲線的方法,並給出了詳細的計算公式,因此按照這樣的公式繪制出來的曲線就用他的姓氏來命名,稱為貝塞爾曲線。

        以下公式中:B(t)為t時間下 點的坐標;

        P0為起點,Pn為終點,Pi為控制點
    一階貝塞爾曲線:

        意義:由 P0 至 P1 的連續點, 描述的一條線段


    二階貝塞爾曲線:



       原理:由 P0 至 P1 的連續點 Q0,描述一條線段。 
          由 P1 至 P2 的連續點 Q1,描述一條線段。 
          由 Q0 至 Q1 的連續點 B(t),描述一條二次貝塞爾曲線。
          經驗:P1-P0為曲線在P0處的切線。


        三階貝塞爾曲線:





  3. CGAffineTransform與CATransform3D
    已經知道了運動軌跡的定義,那么該如何讓layer沿着動畫軌跡運動呢,如果在運動過程中,還有能縮放、旋轉呢?

    A. CGAffineTransform
        從CG就可以看出它是屬於Core Graphics的東西,實際上UIView的transform屬性就是CGAffineTransform類型,用它可以做二維平面上的縮放、旋轉、平移。
        下面的這幾個函數都創建了一個CGAffineTransform實例:

            //平移,輸入需要平移的x,y值即可
            public func CGAffineTransformMakeTranslation(tx: CGFloat, _ ty: CGFloat) -> CGAffineTransform
            
            //縮放x方向縮放的倍數,y方向的縮放倍數,默認都為1
            public func CGAffineTransformMakeScale(sx: CGFloat, _ sy: CGFloat) -> CGAffineTransform
            
            //旋轉,angle放弧度
            public func CGAffineTransformMakeRotation(angle: CGFloat) -> CGAffineTransform
    

        當然還有一些其他的接口,並不都一一寫出,下面是一份簡單的實例代碼,一邊縮放、一邊平移:

    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var redView: UIView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
        }
        
        override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
            self.transformTest2D()
        }
        
        
        func transformTest2D() {
            UIView.animateWithDuration(2) { () -> Void in
                
                let transform = CGAffineTransformMakeScale(2, 2)
                
                
                //注意,本意只是想平移200的,但是由於是先縮放后平移的,如果不除以縮放系數的話,會達不到本意
                self.redView.transform = CGAffineTransformTranslate(transform, 200 / 2, 200 / 2)
            }
        }
    }
    
    B. CATransform3D
        它可以做到讓圖層在三維空間內平移、旋轉等。
        CATransform3D是一個4*4的矩陣:

        與CGAffineTransform類似,它也提供了一系列的接口讓我們得以實例化一個CATransform3D實例:
            public func CATransform3DMakeTranslation(tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat) -> CATransform3D
            
            public func CATransform3DMakeScale(sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat) -> CATransform3D
            
            public func CATransform3DMakeRotation(angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat) -> CATransform3D
    

        多了一個z值。同樣的,它也提一系列用來做混合動畫的接口:

            public func CATransform3DTranslate(t: CATransform3D, _ tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat) -> CATransform3D
            
            public func CATransform3DScale(t: CATransform3D, _ sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat) -> CATransform3D
            
            @available(iOS 2.0, *)
            public func CATransform3DRotate(t: CATransform3D, _ angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat) -> CATransform3D
    

         示例代碼:

    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var redView: UIView!
        
        @IBOutlet weak var imageView1: UIImageView!
    
        @IBOutlet weak var imageView2: UIImageView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
        }
        
        override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
            self.transformTest3D()
        }
        
        func transformTest3D() {
            
            UIView.animateWithDuration(2, animations: { () -> Void in
                //繞Y軸旋轉四分之三PI
                self.imageView1.layer.transform = CATransform3DMakeRotation(CGFloat(M_PI * 3.0 / 4.0), 0, 1, 0)
                //繞Y軸旋轉四分之一PI
                self.imageView2.layer.transform = CATransform3DMakeRotation(-CGFloat(M_PI_4), 0, 1, 0)
                }) { (finish: Bool) -> Void in
                    dispatch_after(dispatch_time_t(dispatch_time(DISPATCH_TIME_NOW, (Int64)(2 * NSEC_PER_SEC))), dispatch_get_main_queue(), { () -> Void in
                        
                        //旋轉完成2s之后,將他們各自的transform恢復原始值
                        //需要注意,CGAffineTransform也有這個CGAffineTransformIdentity,前面忘說了,效果是一樣的
                        self.imageView1.layer.transform = CATransform3DIdentity
                        self.imageView2.layer.transform = CATransform3DIdentity
                    })
            }
        }
    }
    
    CATransform3D還有一個很有意思的m34屬性,我們通過調整這個屬性可以做一些很nice的視覺。恩,就是通過設置它的值,可以做出一些很好的透視效果。m34的默認值是0,我們可以設置m34的值為(-1/d)來做相應的透視效果,d的值一般在500.0~~1000.0之間效果就很不錯了,用它寫了一個打開門的動畫,設置m34與不設置m34的區別簡直就是天差地別。

        代碼(代碼太少了,上傳到github實在浪費時間,我的建議是,百度一張門的圖片,然后嘗試注釋掉m34的設置與不注釋m34的視覺區別):
    #import "ViewController.h"
    
    @interface ViewController ()
    @property (nonatomic, strong) CALayer *doorLayer;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        self.doorLayer = [CALayer layer];
        self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
        self.doorLayer.position = CGPointMake(150 - 64, 300);
        self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
        self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
        [self.view.layer addSublayer:self.doorLayer];
        CATransform3D perspective = CATransform3DIdentity;

    //設置m34,可以嘗試注釋掉,看看與不注銷的視覺差距 perspective.m34 = -1.0 / 500.0;
    self.view.layer.sublayerTransform = perspective; UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init]; [pan addTarget:self action:@selector(pan:)]; [self.view addGestureRecognizer:pan]; self.doorLayer.speed = 0.0; CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform"; animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(-M_PI_2, 0, 1, 0)]; animation.duration = 1.0; [self.doorLayer addAnimation:animation forKey:nil]; } - (void)pan:(UIPanGestureRecognizer *)pan { CGFloat x = [pan translationInView:self.view].x; x /= 200.0f; CFTimeInterval timeOffset = self.doorLayer.timeOffset; timeOffset = MIN(0.999, MAX(0.0, timeOffset - x)); self.doorLayer.timeOffset = timeOffset; [pan setTranslation:CGPointZero inView:self.view]; } @end

        oc代碼,沒有改寫成Swift代碼,這是可以手勢控制門打開或者關閉動畫的代碼。
    圖片:








      動畫在學習動畫之前,有一副圖是必須提到的:

        CAAnimation的繼承結構。而我這次所深入學習的是CABasicAnimation、CAKeyFrameAnimation、CAAnimationGroup、CATransition。

 

        CAAnimation:

 

            所有動畫對象的父類,負責控制動畫的持續時間和速度,是個抽象類,不能直接使用,應該使用它具體的子類



            duration:動畫的持續時間
            repeatCount:動畫的重復次數
            repeatDuration:動畫的重復時間
            removedOnCompletion:默認為YES,代表動畫執行完畢后就從圖層上移除,圖形會恢復到動畫執行前的狀態。如果想讓圖層保持顯示動畫執行后的狀態,那就設置為NO,不過還要設置fillMode為kCAFillModeForwards
            fillMode:決定當前對象在非active時間段的行為.比如動畫開始之前,動畫結束之后
            beginTime:可以用來設置動畫延遲執行時間,若想延遲2s,就設置為CACurrentMediaTime()+2,CACurrentMediaTime()為圖層的當前時間  
            timingFunction:速度控制函數,控制動畫運行的節奏
            delegate:動畫代理
            keyPath: 通過指定CALayer的一個屬性名稱達到相應的動畫效果,比如說,指定"position"為keyPath,就修改CALayer的position屬性值,以達到平移的動畫效果



        A.  CABasicAnimation
CAPropertyAnimation的子類
屬性解析:
fromValue:keyPath相應屬性的初始值
toValue:keyPath相應屬性的結束值
隨着動畫的進行,在長度為duration的持續時間內,keyPath相應屬性的值從fromValue漸漸地變為toValue
代碼:
import UIKit

class ViewController: UIViewController {

    weak private var contentView: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        let view = UIView(frame: CGRectMake(100, 100, 100, 100))
        view.backgroundColor = UIColor.redColor()
        self.view.addSubview(view)
        self.contentView = view
        self.contentView.layer.anchorPoint = CGPointZero
    }
    
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.animateOfTransform3D()
    }
    
    //3D旋轉
    func animateOfTransform3D() {
        let animate = CABasicAnimation(keyPath: "transform")
        
        animate.toValue = NSValue(CATransform3D: CATransform3DMakeRotation(CGFloat(M_PI_4), 0, 1, 0))
        
        animate.duration = 2
        
        animate.removedOnCompletion = false
        
        animate.fillMode = kCAFillModeForwards
        
        animate.delegate = self
        
        self.contentView.layer.addAnimation(animate, forKey: "aaa")
    }
    
    //縮放動畫
    func animateOfScale() {
        let animate = CABasicAnimation(keyPath: "bounds")
        
        animate.toValue = NSValue(CGRect: CGRectMake(100, 100, 200, 200))
        
        animate.duration = 2
        
        animate.removedOnCompletion = false
        
        animate.fillMode = kCAFillModeForwards
        
        animate.delegate = self
        
        self.contentView.layer.addAnimation(animate, forKey: "bcd")
    }
    
    //平移動畫
    func animateOfTranslation() {
        //keyPath放需要執行怎么樣的動畫
        let animate = CABasicAnimation(keyPath: "position")
        
        //layer從哪里來
        animate.fromValue = NSValue(CGPoint: CGPointMake(100, 100))
        //到哪去
        animate.toValue = NSValue(CGPoint: CGPointMake(200, 200))
        
        //    在當前位置的基礎上增加多少
        //    animate.byValue = [NSValue valueWithCGPoint:CGPointMake(0, 300)];
        
        animate.duration = 1
        //設置動畫執行完畢之后不刪除動畫
        animate.removedOnCompletion = false
        
        //設置保存動畫的最新狀態
        animate.fillMode = kCAFillModeForwards
        
        animate.delegate = self
        
        self.contentView.layer.addAnimation(animate, forKey: "abc")
    }

    override func animationDidStart(anim: CAAnimation) {
        print("animationDidStart")
    }
    
    override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
        print("animationDidStop")
    }
}

 

B. CAKeyFrameAnimation

CApropertyAnimation的子類,跟CABasicAnimation的區別是:CABasicAnimation只能從一個數值(fromValue)變到另一個數值(toValue),而CAKeyframeAnimation會使用一個NSArray保存這些數值
屬性解析:
values:就是上述的NSArray對象。里面的元素稱為”關鍵幀”(keyframe)。動畫對象會在指定的時間(duration)內,依次顯示values數組中的每一個關鍵幀
path:可以設置一個CGPathRef\CGMutablePathRef,讓層跟着路徑移動。path只對CALayer的anchorPoint和position起作用。如果你設置了path,那么values將被忽略
keyTimes:可以為對應的關鍵幀指定對應的時間點,其取值范圍為0到1.0,keyTimes中的每一個時間值都對應values中的每一幀.當keyTimes沒有設置的時候,各個關鍵幀的時間是平分的
CABasicAnimation可看做是最多只有2個關鍵幀的CAKeyframeAnimation
 
代碼:
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var redView: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
//        self.redView.layer.anchorPoint = CGPointZero
    }
    
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.animateTest2()
    }
    
    @IBAction func clickBtn(sender: AnyObject) {
        self.redView.layer.removeAnimationForKey("aaa")
    }
    
    //無限搖擺
    func animateTest2() {
//        self.redView.layer.anchorPoint = CGPointMake(self.redView.frame.width / 2, self.redView.frame.height / 2)
        let keyAnimate = CAKeyframeAnimation(keyPath: "transform.rotation")
        
        keyAnimate.values = [ -2.14 / 20, 2.14 / 20, -2.14 / 20];
        
        keyAnimate.removedOnCompletion = false
        keyAnimate.fillMode = kCAFillModeForwards
        
        keyAnimate.duration = 0.05
        
        keyAnimate.delegate = self
        keyAnimate.repeatCount = Float(CGFloat.max)
        self.redView.layer.addAnimation(keyAnimate, forKey: "aaa")
    }
    
    //圍繞一個圓進行動畫
    func animateTest1() {
        let keyAnimate = CAKeyframeAnimation(keyPath: "position")
        
        var path = CGPathCreateMutable()
        CGPathAddEllipseInRect(path, nil, CGRectMake(0, 100, 200, 200))
        keyAnimate.path = path
        
        //設置動畫的進度快慢,可以先快后慢,先慢后快等
        keyAnimate.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        
        keyAnimate.removedOnCompletion = false
        keyAnimate.fillMode = kCAFillModeForwards
        
        keyAnimate.duration = 2
        
        keyAnimate.delegate = self
        
        self.redView.layer.addAnimation(keyAnimate, forKey: "aaa")
    }
    
    func animateTest() {
        let keyAnimate = CAKeyframeAnimation(keyPath: "position")
        let v1 = NSValue(CGPoint: CGPointMake(self.redView.frame.origin.x, self.redView.frame.origin.y))
        let v2 = NSValue(CGPoint: CGPointMake(100, 200))
        let v3 = NSValue(CGPoint: CGPointMake(0, 200))
        let v4 = NSValue(CGPoint: CGPointMake(0, 100))
        let v5 = NSValue(CGPoint: CGPointMake(self.redView.frame.origin.x, self.redView.frame.origin.y))
        keyAnimate.values = [v1, v2, v3, v4, v5]
        
        //設置動畫的進度快慢,可以先快后慢,先慢后快等
        keyAnimate.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        
        keyAnimate.removedOnCompletion = false
        keyAnimate.fillMode = kCAFillModeForwards
        
        keyAnimate.duration = 2
        
        keyAnimate.delegate = self
        
        self.redView.layer.addAnimation(keyAnimate, forKey: "aaa")
    }
    
    override func animationDidStart(anim: CAAnimation) {
        print("animationDidStart")
    }
    
    override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
        print("animationDidStop")
    }
}

 

前面提到了,沿着貝塞爾曲線的動畫:

    func animateTestThree() {
        
        let path = UIBezierPath()
        path.moveToPoint(CGPointMake(0, 150))
        
        //這是二次貝塞爾曲線的接口,利用此接口,可以定義一條貝塞爾曲線軌跡
        path.addCurveToPoint(CGPointMake(400, 150), controlPoint1: CGPointMake(75, 0), controlPoint2: CGPointMake(225, 300))
        
        let plane = UIImageView(frame: CGRectMake(0, 0, 85, 60))
        //這里可以放一張飛機圖片
        plane.image = UIImage(named: "F2005071315255000000")
        plane.center = CGPointMake(0, 150)
        plane.layer.anchorPoint = CGPointMake(0.5, 0.5)
        self.view.addSubview(plane)
        
        let animate = CAKeyframeAnimation(keyPath: "position")
        animate.duration = 4
        animate.path = path.CGPath
        animate.fillMode = kCAFillModeForwards
        animate.removedOnCompletion = false
        animate.delegate = self
        
        //設置此屬性,可以使得飛機在飛行時自動調整角度
        animate.rotationMode = kCAAnimationRotateAuto
        
//        animate.setValue(plane.layer, forKey: "plane.layer")
        plane.layer.addAnimation(animate, forKey: "animateTestThree")
    }

 圖片:

 
C. CAAnimationGroup
CAAnimation的子類,可以保存一組動畫對象,將CAAnimationGroup對象加入層后,組中所有動畫對象可以同時並發運行
屬性解析:
animations:用來保存一組動畫對象的NSArray
默認情況下,一組動畫對象是同時運行的,也可以通過設置動畫對象的beginTime屬性來更改動畫的開始時間
代碼:
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var redView: UIView!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

//    {
//    // 平移動畫
//    CABasicAnimation *a1 = [CABasicAnimation animation];
//    a1.keyPath = @"transform.translation.y";
//    a1.toValue = @(100);
//    // 縮放動畫
//    CABasicAnimation *a2 = [CABasicAnimation animation];
//    a2.keyPath = @"transform.scale";
//    a2.toValue = @(0.0);
//    // 旋轉動畫
//    CABasicAnimation *a3 = [CABasicAnimation animation];
//    a3.keyPath = @"transform.rotation";
//    a3.toValue = @(M_PI_2);
//    }
    
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let basicAnimate = CABasicAnimation(keyPath: "transform.scale")
        basicAnimate.toValue = 1.5
        
        let keyFrameAnimate = CAKeyframeAnimation(keyPath: "position")
        
        let path = CGPathCreateMutable()
        CGPathAddEllipseInRect(path, nil, CGRectMake(100, 200, 150, 150))
        keyFrameAnimate.path = path
        
        let animateGroup = CAAnimationGroup()
        animateGroup.animations = [basicAnimate, keyFrameAnimate]
        animateGroup.duration = 2
        animateGroup.fillMode = kCAFillModeForwards
        animateGroup.removedOnCompletion = false
        
        self.redView.layer.addAnimation(animateGroup, forKey: "aaa")
    }
}

 

 
D. CATransition
 
CAAnimation的子類,用於做轉場動畫,能夠為層提供移出屏幕和移入屏幕的動畫效果。iOS比Mac OS X的轉場動畫效果少一點
UINavigationController就是通過CATransition實現了將控制器的視圖推入屏幕的動畫效果
屬性解析:
type:動畫過渡類型
subtype:動畫過渡方向
startProgress:動畫起點(在整體動畫的百分比)
endProgress:動畫終點(在整體動畫的百分比)
      代碼:
import UIKit

class ViewController: UIViewController {

    
    @IBOutlet weak var imageView: UIImageView!
    
    var index: Int = 0
    
    private var _images: Array<UIImage>?
    var images: Array<UIImage>? {
        get{
            if (_images == nil) {
                _images = Array<UIImage>()
                
                for(var i = 1; i <= 7; i++) {
                    let image = UIImage(named: "\(i)")
                    _images?.append(image!)
                }
            }
            return _images
        }
        set{
            _images = newValue
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        self.imageView.image = self.images![0]
    }
    
    @IBAction func next(sender: AnyObject) {
        self.index++
        if (self.index == 7) {
            self.index = 0
        }
        self.imageView.image = self.images![self.index]
        
        let transition = CATransition()
        //動畫過渡類型
        transition.type = "cube"
        
        //動畫過渡類型方向
        transition.subtype = kCATransitionFromLeft
        
        transition.duration = 1
        
        self.imageView.layer.addAnimation(transition, forKey: nil)
        
    }
    
    @IBAction func previous(sender: AnyObject) {
        self.index--
        if (self.index < 0) {
            self.index = 6
        }
        
        self.imageView.image = self.images![self.index]
        
        let transition = CATransition()
        
        transition.type = "cube"
        
        transition.subtype = kCATransitionFromRight
        
        transition.duration = 1
        
        self.imageView.layer.addAnimation(transition, forKey: nil)
    }
}

第一篇就到這里結束吧,下一篇打算記錄自定義UITabBarController以及presented一個ViewController的轉場動畫,以及自定義動畫的緩沖函數、定時動畫、性能優化等方面的只是。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM