最近項目大量使用動畫效果,若是想用原生實現的話,無疑會大大增長研發人員的難度,即便最終實現了,可能仍是達不到UI要的效果!搜索了不少相關技術,找到了能夠友好解決項目需求的技術-lottie - github.com/airbnb/lott…ios
lottie 是Airbnb開源的動畫庫,UI經過AE設計出動畫,使用Lottie提供的BodyMovin插件將設計好的動畫導出成JSON格式,就能夠直接在各個平臺上運用,無需其餘額外的操做。lottie 目前已經支持了iOS,macOS,以及Android和React Native。 對於iOS目前支持了Swift 4.2 ,也支持了CocoaPods 和 Carthage方式導入,對於導入和使用能夠參考上面github連接,裏面有相應的步驟,在這就不作講述。下面是lottie提供了一套完整的跨平臺動畫實現工做流:git
UI給你們的.json文件大概以下:github
JSON文件結構json
因爲本項目運用到了特別多的lottie動畫,特意抽取封裝到Assitant模塊中,以下:markdown
下面就以hccEngine.json爲主,看下如何使用的?前提要倒入Lottie第三方庫,咱們查看一下HCCEngineAnimationView代碼app
import Lottiepublic class HCCEngineAnimationView: UIView {
lazy var animateView: AnimationView = {
let view = AnimationView()
//json文件放入的位置,經過bundle取出
let animation = Animation.named("hccEngine", bundle: Bundle(for: HCCEngineAnimationView.self))
view.animation = animation
///填充方式
view.contentMode = .scaleAspectFit
///執行一次
view.loopMode = .playOnce
/// 暫停動畫並在應到前臺時從新啓動它,在動畫完成時調用回調
view.backgroundBehavior = .pauseAndRestore
return view
}()
public override init(frame: CGRect) {
super.init(frame: frame)
self.setupSubviews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupSubviews() {
///動畫適配
addSubview(animateView)
animateView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
}
public func startAnimation(completion: (()-> Void)?) {
///開始播放動畫
self.animateView.play { (_) in
completion?()
}
}
public func remove() {
///動畫中止並移除屏幕
self.animateView.stop()
self.removeFromSuperview()
}
}
複製代碼
而後在調用動畫的地方,開始初始化HCCEngineAnimationView動畫View框架
而後再適配屏幕ide
在合適的時機調用開始動畫oop
在合適的時機移除動畫字體
運行結果以下:
上面只是簡單的頁面用到了一處lottie.json文件,假如lottie加載和狀態有關係呢,那麼可能有枚舉類型的出現! 假如紅藍雙方PK,可能出現的PK結果爲紅方勝出的動畫,藍方勝出的動畫以及紅藍平局的動畫三種狀態,若是咱們寫三個封裝,顯然不合適,因此枚舉的出現解決了該問題!記住枚舉的原始值和lottie的動畫json的文件名同樣(能夠省去很多的麻煩)
public enum PKWinSideEnum: String {
case red = "red" //紅方勝出
case blue = "blue" //藍方勝出
case draw = "draw" //雙方平局
}
public class HCCBrokerPKSuccessAnimation: UIView {
public var type: PKWinSideEnum? {
didSet {
///根據pk狀態,顯示不一樣Lottie動畫效果
guard let side = type else { return }
let animation = Animation.named(side.rawValue, bundle: Bundle(for: HCCBrokerPKSuccessAnimation.self))
animateView.animation = animation
}
}
lazy var animateView: AnimationView = {
let view = AnimationView()
view.contentMode = .scaleAspectFit
view.loopMode = .loop
view.backgroundBehavior = .pauseAndRestore
return view
}()
public override init(frame: CGRect) {
super.init(frame: frame)
self.setupSubviews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupSubviews() {
addSubview(animateView)
animateView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
}
public func startAnimation(completion: (()-> Void)?) {
self.animateView.play { (_) in
completion?()
}
}
public func remove() {
self.animateView.stop()
self.removeFromSuperview()
}
}
複製代碼
具體使用參考上面便可!
let animation = Animation.named("hccEngine", bundle: Bundle(for: HCCEngineAnimationView.self))
複製代碼
緊接着點進去查看name的實現代碼以下:
static func named(_ name: String,
bundle: Bundle = Bundle.main,
subdirectory: String? = nil,
animationCache: AnimationCacheProvider? = nil) -> Animation? {
/// 建立一個cacheKey
let cacheKey = bundle.bundlePath + (subdirectory ?? "") + "/" + name
/// 檢查動畫key
if let animationCache = animationCache,
let animation = animationCache.animation(forKey: cacheKey) {
///若是找到了,就直接返回動畫
return animation
}
/// 肯定提供的路徑文件
guard let url = bundle.url(forResource: name, withExtension: "json", subdirectory: subdirectory) else {
return nil
}
do {
let json = try Data(contentsOf: url)
let animation = try JSONDecoder().decode(Animation.self, from: json)
animationCache?.setAnimation(animation, forKey: cacheKey)
return animation
} catch {
print(error)
return nil
}
}
複製代碼
另外
view.animation = animation
複製代碼
裏面作了:
public var animation: Animation? {
didSet {
makeAnimationLayer()
}
}
//緊接着看makeAnimationLayer
fileprivate func makeAnimationLayer() {
///移除當前動畫
removeCurrentAnimation()
if let oldAnimation = self.animationLayer {
oldAnimation.removeFromSuperlayer()
}
invalidateIntrinsicContentSize()
guard let animation = animation else {
return
}
///經過AnimationContainer來構建animation和imageProvider等
let animationLayer = AnimationContainer(animation: animation, imageProvider: imageProvider, textProvider: textProvider, fontProvider: fontProvider)
animationLayer.renderScale = self.screenScale
viewLayer?.addSublayer(animationLayer)
self.animationLayer = animationLayer
reloadImages()
animationLayer.setNeedsDisplay()
setNeedsLayout()
currentFrame = CGFloat(animation.startFrame)
}
複製代碼
而後查看核心類AnimationContainer初始化方法
final class AnimationContainer: CALayer {
init(animation: Animation, imageProvider: AnimationImageProvider, textProvider: AnimationTextProvider, fontProvider: AnimationFontProvider) {
/// 圖片layer的處理
self.layerImageProvider = LayerImageProvider(imageProvider: imageProvider, assets: animation.assetLibrary?.imageAssets)
/// 文字的處理
self.layerTextProvider = LayerTextProvider(textProvider: textProvider)
/// 字體的處理
self.layerFontProvider = LayerFontProvider(fontProvider: fontProvider)
self.animationLayers = []
super.init()
bounds = animation.bounds
let layers = animation.layers.initializeCompositionLayers(assetLibrary: animation.assetLibrary, layerImageProvider: layerImageProvider, textProvider: textProvider, fontProvider: fontProvider, frameRate: CGFloat(animation.framerate))
var imageLayers = [ImageCompositionLayer]()
var textLayers = [TextCompositionLayer]()
var mattedLayer: CompositionLayer? = nil
//對layer圖層進行整合
for layer in layers.reversed() {
layer.bounds = bounds
animationLayers.append(layer)
if let imageLayer = layer as? ImageCompositionLayer {
imageLayers.append(imageLayer)
}
if let textLayer = layer as? TextCompositionLayer {
textLayers.append(textLayer)
}
if let matte = mattedLayer {
/// The previous layer requires this layer to be its matte
matte.matteLayer = layer
mattedLayer = nil
continue
}
if let matte = layer.matteType,
(matte == .add || matte == .invert) {
/// We have a layer that requires a matte.
mattedLayer = layer
}
addSublayer(layer)
}
layerImageProvider.addImageLayers(imageLayers)
layerImageProvider.reloadImages()
layerTextProvider.addTextLayers(textLayers)
layerTextProvider.reloadTexts()
layerFontProvider.addTextLayers(textLayers)
layerFontProvider.reloadTexts()
setNeedsDisplay()
}
}
複製代碼
而後咱們拿圖片layer的處理LayerImageProvider(imageProvider: imageProvider, assets: animation.assetLibrary?.imageAssets)方法
fileprivate(set) var imageLayers: [ImageCompositionLayer]
let imageAssets: [String : ImageAsset]
init(imageProvider: AnimationImageProvider, assets: [String : ImageAsset]?) {
self.imageProvider = imageProvider
self.imageLayers = [ImageCompositionLayer]()
if let assets = assets {
self.imageAssets = assets
} else {
self.imageAssets = [:]
}
reloadImages()
}
複製代碼
而後查看一下assets和assetLibrary所在類的實體
若是認真查看會發現Lottie JSON文件結構與定義此實體相對應!
最後看下動畫的執行play過程以及內部作了什麼?
public func startAnimation(completion: (()-> Void)?) {
///開始播放動畫
self.animateView.play { (_) in
completion?()
}
}
複製代碼
點擊進去查看.play裏面
public func play(completion: LottieCompletionBlock? = nil) {
guard let animation = animation else {
return
}
///爲動畫建立一個上下文
let context = AnimationContext(playFrom: CGFloat(animation.startFrame),
playTo: CGFloat(animation.endFrame),
closure: completion)
///首先移除當前的動畫
removeCurrentAnimation()
///添加新的動畫
addNewAnimationForContext(context)
}
複製代碼
而後查看如何添加新的動畫:addNewAnimationForContext(context)
/// Adds animation to animation layer and sets the delegate. If animation layer or animation are nil, exits.
fileprivate func addNewAnimationForContext(_ animationContext: AnimationContext) {
guard let animationlayer = animationLayer, let animation = animation else {
return
}
self.animationContext = animationContext
guard self.window != nil else { waitingToPlayAimation = true; return }
animationID = animationID + 1
activeAnimationName = AnimationView.animationName + String(animationID)
let framerate = animation.framerate
let playFrom = animationContext.playFrom.clamp(animation.startFrame, animation.endFrame)
let playTo = animationContext.playTo.clamp(animation.startFrame, animation.endFrame)
let duration = ((max(playFrom, playTo) - min(playFrom, playTo)) / CGFloat(framerate))
let playingForward: Bool =
((animationSpeed > 0 && playFrom < playTo) ||
(animationSpeed < 0 && playTo < playFrom))
var startFrame = currentFrame.clamp(min(playFrom, playTo), max(playFrom, playTo))
if startFrame == playTo {
startFrame = playFrom
}
let timeOffset: TimeInterval = playingForward ?
Double(startFrame - min(playFrom, playTo)) / framerate :
Double(max(playFrom, playTo) - startFrame) / framerate
///使用CABasicAnimation實現動畫
let layerAnimation = CABasicAnimation(keyPath: "currentFrame")
layerAnimation.fromValue = playFrom
layerAnimation.toValue = playTo
layerAnimation.speed = Float(animationSpeed)
layerAnimation.duration = TimeInterval(duration)
layerAnimation.fillMode = CAMediaTimingFillMode.both
switch loopMode {
case .playOnce:
layerAnimation.repeatCount = 1
case .loop:
layerAnimation.repeatCount = HUGE
case .autoReverse:
layerAnimation.repeatCount = HUGE
layerAnimation.autoreverses = true
case let .repeat(amount):
layerAnimation.repeatCount = amount
case let .repeatBackwards(amount):
layerAnimation.repeatCount = amount
layerAnimation.autoreverses = true
}
layerAnimation.isRemovedOnCompletion = false
if timeOffset != 0 {
let currentLayerTime = viewLayer?.convertTime(CACurrentMediaTime(), from: nil) ?? 0
layerAnimation.beginTime = currentLayerTime - (timeOffset * 1 / Double(animationSpeed))
}
layerAnimation.delegate = animationContext.closure
animationContext.closure.animationLayer = animationlayer
animationContext.closure.animationKey = activeAnimationName
animationlayer.add(layerAnimation, forKey: activeAnimationName)
updateRasterizationState()
}
複製代碼