最近项目大量使用动画效果,如果想用原生实现的话,无疑会大大增加研发人员的难度,即使最终实现了,可能还是达不到UI要的效果!搜索了很多相关技术,找到了可以友好解决项目需求的技术-lottie - github.com/airbnb/lott…
Lottie简介
lottie 是Airbnb开源的动画库,UI通过AE设计出动画,使用Lottie提供的BodyMovin插件将设计好的动画导出成JSON格式,就可以直接在各个平台上运用,无需其他额外的操作。lottie 目前已经支持了iOS,macOS,以及Android和React Native。 对于iOS目前支持了Swift 4.2 ,也支持了CocoaPods 和 Carthage方式导入,对于导入和使用可以参考上面github链接,里面有相应的步骤,在这就不做讲述。下面是lottie提供了一套完整的跨平台动画实现工作流:
Lottie文件结构
UI给大家的.json文件大概如下:
JSON文件结构
第一层
第二层 assets
第二层 layers
Lottie应用
由于本项目运用到了特别多的lottie动画,特地抽取封装到Assitant模块中,如下:
应用一
下面就以hccEngine.json为主,看下如何使用的?前提要倒入Lottie第三方库,我们查看一下HCCEngineAnimationView代码
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
然后再适配屏幕
在合适的时机调用开始动画
在合适的时机移除动画
运行结果如下:
应用二
上面只是简单的页面用到了一处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()
}
}
具体使用参考上面即可!
Lottie加载原理
代码组织结构
代码过程
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()
}
Lottie实现本质是通过Layer来实现动画的,是一个非常好的动画框架,大家赶紧操动起来吧
机会❤️❤️❤️🌹🌹🌹
如果想和我一起共建抖音,成为一名bytedancer,Come on。期待你的加入!!!