iOS 動畫進階

貓咪在上一篇文章中簡單的介紹了UIView動畫的入門和實現(xiàn),那這篇文章,就來和大家一起來看一看更復(fù)雜的動畫效果是怎么實現(xiàn)的呢?先來看一張圖:

iOS中的圖形層次

正如我們看到的那樣,越往上封裝度越高,動畫的實現(xiàn)越簡潔,但是自由度低。

核心動畫有以下幾類:
  • 圖層類(CALayer
  • 動畫和計時類 (CAAnimationCAMediaTiming
  • 布局和約束類(CAConstraint
  • 事務(wù)類 (CATransaction

一、各個類的概念介紹

1.圖層類 - CALayer

圖層類是核心動畫的基礎(chǔ),它是所有核心動畫圖層類的父類。和UIView一樣,同樣有自己的視圖集合,同樣有l(wèi)ayer、subLayer...也同樣擁有backgroundColor、frame等相似的屬性,我們可以將UIView看做是一個特殊的CALayer。
它們之間的區(qū)別就是UIView可以響應(yīng)事件,layer最常用的就是設(shè)置圓角、陰影、邊框等參數(shù)和實現(xiàn)動畫。因此對view做動畫實際上是對layer進行操作。

2.動畫類 - CAAnimation

核心動畫的動畫類使用基本的動畫和關(guān)鍵幀動畫把圖層的內(nèi)容和選取的屬性動 畫的顯示出來。所有核心動畫的動畫類都是從 CAAnimation 類繼承而來。CAAnimation 實現(xiàn)了 CAMediaTiming 協(xié)議,提供了動畫的持續(xù)時間,速度,和重復(fù)計數(shù)。 CAAnimation 也實現(xiàn)了 CAAction 協(xié)議。該協(xié)議為圖層觸發(fā)一個動畫動作提供了提供 標準化響應(yīng)。
推薦文章:iOS開發(fā)CoreAnimation解讀之一——初識CoreAnimation核心動畫編程

3.布局和約束類 - CAConstraint

核心動畫的 CAConstraint 類 是一個布局管理器,它可以指定子圖層類限制于你指定的約束集合。每個約束 (CAConstraint 類的實例封裝)描述層的幾何屬性(左,右,頂部或底部的邊緣或水 平或垂直中心)的關(guān)系,關(guān)系到其同級之一的幾何屬性層或 superlayer。

4.事務(wù)類 - CATransaction

CATransition 提供了一個圖層變化的過渡效果,它能影響圖層的整個內(nèi)容。 動畫進行的時候淡入淡出(fade)、推(push)、顯露(reveal)圖層的內(nèi)容。這些過渡效 果可以擴展到你自己定制的 Core Image 濾鏡。

二、核心動畫渲染框架

雖然核心動畫的圖層和 Cocoa 的視圖在很大程度上沒有一定的相似性,但是他們 兩者最大的區(qū)別是,圖層不會直接渲染到屏幕上。

在我們常用的模型-視圖-控制器(model-view-controller)概念里面 NSView 和 UIView 是典 型的視圖部分,但是在核心動畫里面圖層是模型部分。圖層封裝了幾何、時間、可視 化屬性,同時它提供了圖層現(xiàn)實的內(nèi)容,但是實際顯示的過程則不是由它來完成。

三、CoreGraphics框架

介紹CoreGraphics的資料很多,推薦給大家一篇我覺得比較好的: CoreGraphics入門

hamburger.gif
hamburger.gif
這是貓咪實現(xiàn)的兩個小動畫,上面hamburger的動畫原址:用 Swift 制作一個漂亮的漢堡按鈕過渡動畫,給大家簡單介紹一下下面播放按鈕的實現(xiàn)原理:
import CoreGraphics
import QuartzCore
import UIKit

class PlayButton: UIControl {

// MARK: - enum
// 按鈕的幾種動畫效果
enum PlayAnimation {
    case rotateAndGrad
    case grad
    case breathe
}

// 播放狀態(tài)
enum PlayStatus {
    case buffering  // 緩沖
    case play       // 播放
    case pause      // 暫停
    case stopped    // 停止
}

// MARK: - private property
// 左右兩條線
private var top: CAShapeLayer! = CAShapeLayer()
private var bottom: CAShapeLayer! = CAShapeLayer()
private var rotate: CAShapeLayer! = CAShapeLayer()
// 菜單的起點和終點
private let menuStrokeStart: CGFloat = 0.325
private let menuStrokeEnd: CGFloat = 0.9
// 中線的起點和重點
private let playStrokeStart: CGFloat = 0.028
private let playStrokeEnd: CGFloat = 0.111

// MARK: - life cycle
override init(frame: CGRect) {
    super.init(frame: frame)
    
    // 繪制左右兩條線
    self.top.path = leftStroke
    self.bottom.path = rightStroke
    self.rotate.path = outline
    
    // 設(shè)置layer的相關(guān)屬性
    for layer in [self.top, self.bottom, self.rotate] {
        // 填充顏色
        layer?.fillColor = nil
        // 線條的顏色
        layer?.strokeColor = UIColor.white.cgColor
        // 線條寬度
        layer?.lineWidth = 4
        // 兩條線段相交時銳角斜面長度
        layer?.miterLimit = 4
        // 線條首尾的外觀
        layer?.lineCap = kCALineCapRound

        // 設(shè)置layer的bounds
        let strokingPath = CGPath(__byStroking: (layer?.path!)!, transform: nil, lineWidth: 4, lineCap: .round, lineJoin: .miter, miterLimit: 4)
        layer?.bounds = (strokingPath?.boundingBoxOfPath)!
        // 設(shè)置行為
        layer?.actions = [
            "strokeStart": NSNull(),
            "strokeEnd": NSNull(),
            "transform": NSNull()
            
        ]
        
        self.layer.addSublayer(layer!)
    }
    
    // 設(shè)置左線的錨點和位置
    self.top.anchorPoint = CGPoint(x: 1, y: 1)
    self.top.position = CGPoint(x: 40, y: 19.25)
    // 設(shè)置右線的錨點和位置
    self.bottom.anchorPoint = CGPoint(x: 1, y: 0)
    self.bottom.position = CGPoint(x: 40, y: 18)
    // 設(shè)置中線的位置和起點和終點
    self.rotate.position = CGPoint(x: 29, y: 18)
    self.rotate.strokeStart = playStrokeStart
    self.rotate.strokeEnd = playStrokeEnd

}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

// MARK: - setter and getter

// 畫短直線 -> play
let leftStroke: CGPath = {
    let path = CGMutablePath()
    path.move(to: CGPoint(x: 2, y: 2))
    path.addLine(to: CGPoint(x: 2, y: 15))
    return path
}()
private let rightStroke: CGPath = {
    let path = CGMutablePath()
    path.move(to: CGPoint(x: 2, y: 12))
    path.addLine(to: CGPoint(x: 2, y: 25))
    return path
}()
// 外邊框圓 -> 中間
private let outline: CGPath = {
    let path = CGMutablePath()
    path.move(to: CGPoint(x: 29, y: 0))
    // 添加曲線
    path.addCurve(to: CGPoint(x: 27, y: 53), control1: CGPoint(x: 27, y: 31), control2: CGPoint(x: 27, y: 43.75))
    path.addCurve(to: CGPoint(x: 41.5, y: 3), control1: CGPoint(x: 75.92, y: 24.75), control2: CGPoint(x: 62.97, y: 3))
    path.addCurve(to: CGPoint(x: 16.5, y: 27), control1: CGPoint(x: 28.66, y: 3), control2: CGPoint(x: 16.5, y: 13.16))
    path.addCurve(to: CGPoint(x: 41.5, y: 52), control1: CGPoint(x: 16.5, y: 40.84), control2: CGPoint(x: 27.66, y: 52))
    path.addCurve(to: CGPoint(x: 66.5, y: 27), control1: CGPoint(x: 55.34, y: 52), control2: CGPoint(x: 66.5, y: 40.84))
    path.addCurve(to: CGPoint(x: 41.5, y: 3), control1: CGPoint(x: 66.5, y: 13.16), control2: CGPoint(x: 56.89, y: 3))
    path.addCurve(to: CGPoint(x: 16.5, y: 27), control1: CGPoint(x: 27.66, y: 3), control2: CGPoint(x: 16.5, y: 13.16))

    return path
}()

var playStatus: PlayStatus = .pause {
    didSet {
        let strokeStart = CABasicAnimation(keyPath: "strokeStart")
        let strokeEnd = CABasicAnimation(keyPath: "strokeEnd")
        
        // 動畫
        if self.playStatus == .play {
            strokeStart.toValue = menuStrokeStart
            strokeStart.duration = 0.2
            strokeStart.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, -0.4, 0.5, 1)
            
            strokeEnd.toValue = menuStrokeEnd
            strokeEnd.duration = 0.3
            strokeEnd.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, -0.4, 0.5, 1)
        } else {
            strokeStart.toValue = playStrokeStart
            strokeStart.duration = 0.2
            strokeStart.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0, 0.5, 1.2)
            strokeStart.beginTime = CACurrentMediaTime() + 0.1
            strokeStart.fillMode = kCAFillModeBackwards
            
            strokeEnd.toValue = playStrokeEnd
            strokeEnd.duration = 0.3
            strokeEnd.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0.3, 0.5, 0.9)
        }
        
        self.rotate.ocb_applyAnimation(strokeStart)
        self.rotate.ocb_applyAnimation(strokeEnd)
    
        // 設(shè)置豎線的變化
        let topTransform = CABasicAnimation(keyPath: "transform")
        topTransform.timingFunction = CAMediaTimingFunction(controlPoints: 0.5, -0.8, 0.5, 1.85)
        topTransform.duration = 0.2
        topTransform.fillMode = kCAFillModeBackwards
        
        let bottomTransform = topTransform.copy() as! CABasicAnimation
        
        if self.playStatus == .play {
            let translation = CATransform3DMakeTranslation(-2, 0, 0)
            
            topTransform.toValue = NSValue(caTransform3D: CATransform3DRotate(translation, -0.65, 0, 0, 1))
            topTransform.beginTime = CACurrentMediaTime() + 0.25
            
            bottomTransform.toValue = NSValue(caTransform3D: CATransform3DRotate(translation, 0.84, 0, 0, 1))
            bottomTransform.beginTime = CACurrentMediaTime() + 0.25
        } else {
            topTransform.toValue = NSValue(caTransform3D: CATransform3DIdentity)
            topTransform.beginTime = CACurrentMediaTime() + 0.05
            
            bottomTransform.toValue = NSValue(caTransform3D: CATransform3DIdentity)
            bottomTransform.beginTime = CACurrentMediaTime() + 0.05
        }
        
        self.top.ocb_applyAnimation(topTransform)
        self.bottom.ocb_applyAnimation(bottomTransform)
    }
    
}
}

對CALayer進行擴展

extension CALayer {
   func ocb_applyAnimation(_ animation: CABasicAnimation) {
      let copy = animation.copy() as! CABasicAnimation
    
      if copy.fromValue == nil {
          copy.fromValue = self.presentation()!.value(forKeyPath: copy.keyPath!)
      }
    
      self.add(copy, forKey: copy.keyPath)
      self.setValue(copy.toValue, forKeyPath:copy.keyPath!)
    }  
}

在控制器中,我們創(chuàng)建一個播放按鈕:

class ViewController: UIViewController {

   var button: PlayButton! = nil

   override func viewDidLoad() {
      super.viewDidLoad()
      self.view.backgroundColor = UIColor(red: 38.0 / 255, green: 151.0 / 255, blue: 68.0 / 255, alpha: 1)
    
      button = PlayButton(frame: CGRect(x: 133, y: 133, width: 100, height: 100))
//        button.transform = CGAffineTransform.init(scaleX: 0.5, y: 0.5)  作用于view
      button.layer.transform =  CATransform3DMakeScale(0.8, 0.8, 1)
      button.addTarget(self, action: #selector(ViewController.toggle(_:)), for:.touchUpInside)
    
      view.addSubview(button)
    
   }

   override var preferredStatusBarStyle : UIStatusBarStyle  {
    return .lightContent
   }

   func toggle(_ sender: AnyObject!) {
     if button.playStatus == .play {
         button.playStatus = .pause
     } else if button.playStatus == .pause {
         button.playStatus = .play
     }
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在iOS中隨處都可以看到絢麗的動畫效果,實現(xiàn)這些動畫的過程并不復(fù)雜,今天將帶大家一窺ios動畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,686評論 6 30
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現(xiàn)這些動畫的過程并不復(fù)雜,今天將帶大家一窺iOS動畫全貌。在這里你可以看...
    F麥子閱讀 5,260評論 5 13
  • 書寫的很好,翻譯的也棒!感謝譯者,感謝感謝! iOS-Core-Animation-Advanced-Techni...
    錢噓噓閱讀 2,427評論 0 6
  • 在iOS實際開發(fā)中常用的動畫無非是以下四種:UIView動畫,核心動畫,幀動畫,自定義轉(zhuǎn)場動畫。 1.UIView...
    請叫我周小帥閱讀 3,313評論 1 23
  • 來簡書幾個月了,堅持認真更文卻才三周,而且是在加入365挑戰(zhàn)營以后。 說實話,寫到今天都有些泄氣了。自己那水平也叫...
    蕓窗月影閱讀 808評論 6 9

友情鏈接更多精彩內(nèi)容