Recent projects use a lot of animation effects, if you want to use native implementation, will undoubtedly greatly increase the difficulty of the developer, even if the final implementation, may still not reach the UI to the effect! Searched for a number of related technologies and found one that could amicably address the needs of the project – Lottie – github.com/airbnb/lott…

Lottie profile

Lottie is an open source animation library of Airbnb. UI designs animations through AE, and uses the BodyMovin plug-in provided by Lottie to export the designed animations into JSON format, which can be directly used on various platforms without additional operations. Lottie currently supports iOS, macOS, Android and React Native. IOS currently supports Swift 4.2 as well as CocoaPods and Carthage import. For import and use, you can refer to github link above, where there are corresponding steps, which will not be described here. Lottie provides a complete set of cross-platform animation implementation workflows below:

Lottie file structure

The json file UI gives you looks like this:

JSON file structure

The first layer

The second layer assets

The second floor the layers

Lottie application

Because so many Lottie animations were used in this project, it was selected and encapsulated in Assitant module as follows:

Application of a

Hccengine. json (hccengine. json) To do this, let’s take a look at the HCCEngineAnimationView code

import Lottiepublic class HCCEngineAnimationView: UIView { lazy var animateView: AnimationView = {let view = AnimationView() //json file location, Named ("hccEngine", bundle: bundle (for: HCCEngineAnimationView. Self)) view. Animation = animation way / / / fill the contentMode =. ScaleAspectFit / / / perform at a time LoopMode =.playonce /// Pause the animation and restart it when it reaches the foreground, 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() {/// animation fit addSubview(animateView) animateView.snp.makeConstraints { (make) in make.edges.equalToSuperview() } } public func startAnimation(completion: Play {(_) in completion?()}} public func remove() {/// The animation stops and the screen is removed self.animateView.stop() self.removeFromSuperview() } }Copy the code

Then start initializing the HCCEngineAnimationView animation View where the animation is called

And then adapt to the screen

Call the start animation when appropriate

Remove the animation when appropriate

The running results are as follows:

Application of the two

This is a simple page that uses a Lottie. Json file. If Lottie loads are related to state, then enumeration types might be present! If red and blue PK, PK results may be red wins animation, blue wins animation and red and blue draw animation three states, if we write three packages, obviously not appropriate, so the emergence of enumeration to solve the problem! Remember that the original value of the enumeration is the same as the filename of Lottie’s animated JSON (save a lot of trouble)

public enum PKWinSideEnum: String {case red = "red" // case blue = "blue" // case draw = "draw" // both parties draw} public class HCCBrokerPKSuccessAnimation: UIView { public var type: PKWinSideEnum? 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() } }Copy the code

Specific use reference above can!

Lottie loading principle

Code organization

code

let animation = Animation.named("hccEngine", bundle: Bundle(for: HCCEngineAnimationView.self))
Copy the code

Then click to view the implementation code of name as follows:

static func named(_ name: String, bundle: Bundle = Bundle.main, subdirectory: String? = nil, animationCache: AnimationCacheProvider? = nil) -> Animation? {/// create a cacheKey let cacheKey = bundlePath + (subdirectory?? AnimationCache = animationCache, let animation = animationCache.animation(forKey: CacheKey) {/// If found, return animation} /// Determine the provided path file 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 } }Copy the code

In addition

view.animation = animation
Copy the code

It does:

public var animation: Animation? {didSet {makeAnimationLayer()}} makeAnimationLayer fileprivate func makeAnimationLayer() {/// remove the current animation removeCurrentAnimation() if let oldAnimation = self.animationLayer { oldAnimation.removeFromSuperlayer() } invalidateIntrinsicContentSize() guard let animation = animation else { return } 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) }Copy the code

Then look at the core class AnimationContainer initialization method

final class AnimationContainer: CALayer { init(animation: Animation, imageProvider: AnimationImageProvider, textProvider: AnimationTextProvider, fontProvider: Self. layerImageProvider = layerImageProvider (imageProvider: imageProvider, assets: animation.assetLibrary? Self. layerTextProvider = layerTextProvider (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 // Merge the 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() } }Copy the code

Then we take the image layer processing LayerImageProvider(imageProvider: imageProvider, Assets: animation.assetLibrary? ImageAssets) method

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()
  }
Copy the code

Then look at the entities of the assets and assetLibrary classes

If you look closely, you’ll see that the Lottie JSON file structure corresponds to defining this entity!

And finally, what’s going on inside the play and the execution of the animation?

public func startAnimation(completion: (()-> Void)?) {/// Start playing the animation self.animateView.play {(_) in completion? ()}}Copy the code

Click inside to see.play

public func play(completion: LottieCompletionBlock? {guard let animation = animation else {return} // create a context for the animation let Context = AnimationContext(playFrom: CGFloat(animation.startFrame), playTo: CGFloat(animation.endFrame), closure: Completion) / / / remove the current animation removeCurrentAnimation () / / / add a new animation addNewAnimationForContext (context)}Copy the code

Then look at how to add a new animation: 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 // 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() }Copy the code

Lottie implementation is essentially through Layer to achieve animation, is a very good animation framework, we quickly move up