iOS Lottie的原理解析

Lottie是Airbnb开源的一套动画框架,它可以帮助把开发人员从动画的制作上解放出来。设计师可以直接通过AE设计并导出动画,客户端无需做处理就可以直接使用。这确实是一个伟大的创新,强烈推荐大家使用。关于Lottie的安装及使用这里就不啰嗦了,属于Baidu+翻墙+Google可以解决的问题。(https://github.com/airbnb/lottie-ios)

正好最近一段时间得闲,所以就看了看Lottie-iOS的源码,在这里记录一点,有不正确的地方也希望大家可以不吝指教。

1. 动画

关于iOS动画的一些基本知识可以参见 https://www.gitbook.com/book/zsisme/ios-/details 很棒的一本书,内容丰富讲解细致,推荐大家读读。

2. 代码组织

lottie-ios.png

整套代码同时支持Mac和iOS为了方便我们只分析iOS的部分。

#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
#import <UIKit/UIKit.h>
@compatibility_alias LOTView UIView;
#else
#import <AppKit/AppKit.h>
@compatibility_alias LOTView NSView;
#endif

代码功能组织如下

AnimatableLayers: 动画View和Layer的相关定义和实现
AnimatableProperties:  属性动画的相关定义和实现
AnimationCache: 动画数据的LRU Cache
Extensions: 相关类的扩展
MacCompatability: 为支持Mac的一些移植文件
Models: 基础数据结构的定义
Private/PublicHeaders: Interface的定义

3. 代码详解

从PublicHeaders我们可以看到Lottie暴露出来的给我们的是两个类:

LOTAnimationView(动画的View)
LOTAnimationTransitionController(Controller了之间的专场动画)

本文着重分析LOTAnimationView

LOTAnimationView结构

大体上来讲LOTAnimationView的结构如下,负责动画显示的LOTCompositionLayer,记录动画状态的LOTAnimationState以及负责动画生命周期的CADisplayLink。


Lottie-iOS.png

LOTCompositionLayer通过动画数据LOTComposition来初始化。Lottie的动画数据为json文件/数据,针对json文件在性能上也实现了针对LOTComposition数据的LRU Cache也就是LOTAnimationCache,这点我们可以从头文件定义和内部实现上可以看到。

+ (instancetype)animationNamed:(NSString *)animationName inBundle:(NSBundle *)bundle {
  NSArray *components = [animationName componentsSeparatedByString:@"."];
  animationName = components.firstObject;
  
  LOTComposition *comp = [[LOTAnimationCache sharedCache] animationForKey:animationName];
  if (comp) {
    return [[LOTAnimationView alloc] initWithModel:comp];
  }
  
  NSError *error;
  NSString *filePath = [bundle pathForResource:animationName ofType:@"json"];
  NSData *jsonData = [[NSData alloc] initWithContentsOfFile:filePath];
  NSDictionary  *JSONObject = jsonData ? [NSJSONSerialization JSONObjectWithData:jsonData
                                                                         options:0 error:&error] : nil;
  if (JSONObject && !error) {
    LOTComposition *laScene = [[LOTComposition alloc] initWithJSON:JSONObject];
    [[LOTAnimationCache sharedCache] addAnimation:laScene forKey:animationName];
    return [[LOTAnimationView alloc] initWithModel:laScene];
  }
  
  NSException* resourceNotFoundException = [NSException exceptionWithName:@"ResourceNotFoundException"
                                                                   reason:[error localizedDescription]
                                                                 userInfo:nil];
  @throw resourceNotFoundException;
}
#import <Foundation/Foundation.h>

@class LOTComposition;

@interface LOTAnimationCache : NSObject

+ (instancetype)sharedCache;

- (void)addAnimation:(LOTComposition *)animation forKey:(NSString *)key;
- (LOTComposition *)animationForKey:(NSString *)key;

@end

LOTAnimationCache的实现

LOTAnimationCache是一个LRU的Cache,如果不了解LRU Cache可以去https://en.wikipedia.org/wiki/Cache_replacement_policies 科普下

@implementation LOTAnimationCache {
  NSMutableDictionary *animationsCache_; // cache数据
  NSMutableArray *lruOrderArray_; // 通过key数组来保证lru顺序,最近使用的在数组最尾部,该淘汰的在数组最顶部,
}

- (void)addAnimation:(LOTComposition *)animation forKey:(NSString *)key {
  if (lruOrderArray_.count >= kLOTCacheSize) {
    NSString *oldKey = lruOrderArray_[0];
    [animationsCache_ removeObjectForKey:oldKey];
    [lruOrderArray_ removeObject:oldKey];
  }
  [lruOrderArray_ removeObject:key];
  [lruOrderArray_ addObject:key];
  [animationsCache_ setObject:animation forKey:key];
}

- (LOTComposition *)animationForKey:(NSString *)key {
  LOTComposition *animation = [animationsCache_ objectForKey:key];
  [lruOrderArray_ removeObject:key];
  [lruOrderArray_ addObject:key];
  return animation;
}

LOTComposition (构图数据)

@property (nonatomic, readonly) CGRect compBounds;
@property (nonatomic, readonly) NSNumber *startFrame;
@property (nonatomic, readonly) NSNumber *endFrame;
@property (nonatomic, readonly) NSNumber *framerate;
@property (nonatomic, readonly) NSTimeInterval timeDuration;
@property (nonatomic, readonly) LOTLayerGroup *layerGroup;
@property (nonatomic, readonly) LOTAssetGroup *assetGroup;

看到上面的属性值是不是一头雾水,但是也可以稍微做一些推测:动画包含了整个显示大小,时长,开始和结束帧,图层资源和图片资源,如果希望进一步理解则需要从底层数据结构往上进行分析。

所以我们看看AnimatableProperties,Models,是不是第一眼看上去觉得和一些我们已知的概念相似?animatable properties,是不是联想到了CALayer的属性动画?我们知道CALayer的属性值修改时会有隐式动画,我们可以温习一下 https://zsisme.gitbooks.io/ios-/content/chapter7/transactions.html

当你改变一个属性,Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。

事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。

事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。

任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。

Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。

在这里,这些属性值也是有异曲同工之妙的,我们可以看看定义

@interface LOTAnimatableColorValue ()

@property (nonatomic, readonly) NSArray *colorKeyframes;
@property (nonatomic, readonly) NSArray<NSNumber *> *keyTimes;
@property (nonatomic, readonly) NSArray<CAMediaTimingFunction *> *timingFunctions;
@property (nonatomic, readonly) NSTimeInterval delay;
@property (nonatomic, readonly) NSTimeInterval duration;

@property (nonatomic, readonly) NSNumber *startFrame;
@property (nonatomic, readonly) NSNumber *durationFrames;
@property (nonatomic, readonly) NSNumber *frameRate;

@end

所有的Animatable Properties都包含如上相似的结构,timingFunctions.count + 1 = colorKeyframes.count
包含了属性的值数组,以及开始结束帧,持续时长,帧与帧之间的时间函数等。而这些就是构成整个动画的基础,也可以简单的理解为一组更加灵活的动画属性。值得说一点的是,Lottie的动画核心基础是CAKeyframeAnimation 关键帧动画,是贯穿整个动画最底层的原理。

@protocol LOTAnimatableValue <NSObject>

- (CAKeyframeAnimation *)animationForKeyPath:(NSString *)keypath;
- (BOOL)hasAnimation;

@end

- (nullable CAKeyframeAnimation *)animationForKeyPath:(nonnull NSString *)keypath {
  if (self.hasAnimation == NO) {
    return nil;
  }
  CAKeyframeAnimation *keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:keypath];
  keyframeAnimation.keyTimes = self.keyTimes;
  keyframeAnimation.values = self.boundsKeyframes;
  keyframeAnimation.timingFunctions = self.timingFunctions;
  keyframeAnimation.duration = self.duration;
  keyframeAnimation.beginTime = self.delay;
  keyframeAnimation.fillMode = kCAFillModeForwards;
  return keyframeAnimation;
}

引用一段关键帧动画的描述

关键帧动画,是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,各个关键帧的时间是平分的

那么有了这么些属性值,再往上我们可以猜测应该到了构建layer的时候了。
在Models里我们可以看到有如下的定义

#import "LOTComposition.h"
#import "LOTLayer.h"
#import "LOTMask.h"
#import "LOTShapeCircle.h"
#import "LOTShapeFill.h"
#import "LOTShapeGroup.h"
#import "LOTShapePath.h"
#import "LOTShapeRectangle.h"
#import "LOTShapeStroke.h"
#import "LOTShapeTransform.h"
#import "LOTShapeTrimPath.h"
#import "LOTLayerGroup.h"
#import "LOTAsset.h"

这些类定义了Lottie的基础数据结构,注意的是LOTLayer等并不是图层,而是图层的数据,有兴趣的同学可以对比着CALayer的属性看一看,结构上有很多相通的地方。

再往上就是通过数据构成的AnimatableLayers
值得一提的是,在构建Layer上涉及到很多数学知识,有兴趣的可以研究下两个扩展文件

CGGeometry+LOTAdditions
CGRect LOT_RectIntegral(CGRect rect);

// Centering

// Returns a rectangle of the given size, centered at a point
CGRect LOT_RectCenteredAtPoint(CGPoint center, CGSize size, BOOL integral);

// Returns the center point of a CGRect
CGPoint LOT_RectGetCenterPoint(CGRect rect);

// Insetting

// Inset the rectangle on a single edge
CGRect LOT_RectInsetLeft(CGRect rect, CGFloat inset);
CGRect LOT_RectInsetRight(CGRect rect, CGFloat inset);
CGRect LOT_RectInsetTop(CGRect rect, CGFloat inset);
CGRect LOT_RectInsetBottom(CGRect rect, CGFloat inset);

// Inset the rectangle on two edges
CGRect LOT_RectInsetHorizontal(CGRect rect, CGFloat leftInset, CGFloat rightInset);
CGRect LOT_RectInsetVertical(CGRect rect, CGFloat topInset, CGFloat bottomInset);

// Inset the rectangle on all edges
CGRect LOT_RectInsetAll(CGRect rect, CGFloat leftInset, CGFloat rightInset, CGFloat topInset, CGFloat bottomInset);

// Framing

// Returns a rectangle of size framed in the center of the given rectangle
CGRect LOT_RectFramedCenteredInRect(CGRect rect, CGSize size, BOOL integral);

// Returns a rectangle of size framed in the given rectangle and inset
CGRect LOT_RectFramedLeftInRect(CGRect rect, CGSize size, CGFloat inset, BOOL integral);
CGRect LOT_RectFramedRightInRect(CGRect rect, CGSize size, CGFloat inset, BOOL integral);
CGRect LOT_RectFramedTopInRect(CGRect rect, CGSize size, CGFloat inset, BOOL integral);
CGRect LOT_RectFramedBottomInRect(CGRect rect, CGSize size, CGFloat inset, BOOL integral);

CGRect LOT_RectFramedTopLeftInRect(CGRect rect, CGSize size, CGFloat insetWidth, CGFloat insetHeight, BOOL integral);
CGRect LOT_RectFramedTopRightInRect(CGRect rect, CGSize size, CGFloat insetWidth, CGFloat insetHeight, BOOL integral);
CGRect LOT_RectFramedBottomLeftInRect(CGRect rect, CGSize size, CGFloat insetWidth, CGFloat insetHeight, BOOL integral);
CGRect LOT_RectFramedBottomRightInRect(CGRect rect, CGSize size, CGFloat insetWidth, CGFloat insetHeight, BOOL integral);

// Divides a rect into sections and returns the section at specified index

CGRect LOT_RectDividedSection(CGRect rect, NSInteger sections, NSInteger index, CGRectEdge fromEdge);

// Returns a rectangle of size attached to the given rectangle
CGRect LOT_RectAttachedLeftToRect(CGRect rect, CGSize size, CGFloat margin, BOOL integral);
CGRect LOT_RectAttachedRightToRect(CGRect rect, CGSize size, CGFloat margin, BOOL integral);
CGRect LOT_RectAttachedTopToRect(CGRect rect, CGSize size, CGFloat margin, BOOL integral);
CGRect LOT_RectAttachedBottomToRect(CGRect rect, CGSize size, CGFloat margin, BOOL integral);

CGRect LOT_RectAttachedBottomLeftToRect(CGRect rect, CGSize size, CGFloat marginWidth, CGFloat marginHeight, BOOL integral);
CGRect LOT_RectAttachedBottomRightToRect(CGRect rect, CGSize size, CGFloat marginWidth, CGFloat marginHeight, BOOL integral);
CGRect LOT_RectAttachedTopRightToRect(CGRect rect, CGSize size, CGFloat marginWidth, CGFloat marginHeight, BOOL integral);
CGRect LOT_RectAttachedTopLeftToRect(CGRect rect, CGSize size, CGFloat marginWidth, CGFloat marginHeight, BOOL integral);

// Combining
// Adds all values of the 2nd rect to the first rect
CGRect LOT_RectAddRect(CGRect rect, CGRect other);
CGRect LOT_RectAddPoint(CGRect rect, CGPoint point);
CGRect LOT_RectAddSize(CGRect rect, CGSize size);
CGRect LOT_RectBounded(CGRect rect);

CGPoint LOT_PointAddedToPoint(CGPoint point1, CGPoint point2);

CGRect LOT_RectSetHeight(CGRect rect, CGFloat height);

CGFloat LOT_PointDistanceFromPoint(CGPoint point1, CGPoint point2);
CGFloat LOT_DegreesToRadians(CGFloat degrees);

GLKMatrix4 LOT_GLKMatrix4FromCATransform(CATransform3D xform);

CATransform3D LOT_CATransform3DFromGLKMatrix4(GLKMatrix4 xform);

CATransform3D LOT_CATransform3DSlerpToTransform(CATransform3D fromXorm, CATransform3D toXform, CGFloat amount );

CGFloat LOT_RemapValue(CGFloat value, CGFloat low1, CGFloat high1, CGFloat low2, CGFloat high2 );
CGPoint LOT_PointByLerpingPoints(CGPoint point1, CGPoint point2, CGFloat value);
UIColor+Expanded
- (NSString *)LOT_colorSpaceString;

- (NSArray *)LOT_arrayFromRGBAComponents;

- (BOOL)LOT_red:(CGFloat *)r green:(CGFloat *)g blue:(CGFloat *)b alpha:(CGFloat *)a;

- (UIColor *)LOT_colorByLuminanceMapping;

- (UIColor *)LOT_colorByMultiplyingByRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha;
- (UIColor *)       LOT_colorByAddingRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha;
- (UIColor *) LOT_colorByLighteningToRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha;
- (UIColor *)  LOT_colorByDarkeningToRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha;

- (UIColor *)LOT_colorByMultiplyingBy:(CGFloat)f;
- (UIColor *)       LOT_colorByAdding:(CGFloat)f;
- (UIColor *) LOT_colorByLighteningTo:(CGFloat)f;
- (UIColor *)  LOT_colorByDarkeningTo:(CGFloat)f;

- (UIColor *)LOT_colorByMultiplyingByColor:(UIColor *)color;
- (UIColor *)       LOT_colorByAddingColor:(UIColor *)color;
- (UIColor *) LOT_colorByLighteningToColor:(UIColor *)color;
- (UIColor *)  LOT_colorByDarkeningToColor:(UIColor *)color;

- (NSString *)LOT_stringFromColor;
- (NSString *)LOT_hexStringValue;

+ (UIColor *)LOT_randomColor;
+ (UIColor *)LOT_colorWithString:(NSString *)stringToConvert;
+ (UIColor *)LOT_colorWithRGBHex:(UInt32)hex;
+ (UIColor *)LOT_colorWithHexString:(NSString *)stringToConvert;

+ (UIColor *)LOT_colorWithName:(NSString *)cssColorName;

+ (UIColor *)LOT_colorByLerpingFromColor:(UIColor *)fromColor toColor:(UIColor *)toColor amount:(CGFloat)amount;

搞清楚里面各个函数的作用,起码会多学些数学知识。

先写到这吧,等有空了再增加点。

BTW Lottie确实是一个好工具

推荐阅读更多精彩内容

  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 6,629评论 4 25
  • 书写的很好,翻译的也棒!感谢译者,感谢感谢! iOS-Core-Animation-Advanced-Techni...
    钱嘘嘘阅读 1,564评论 0 6
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 3,840评论 5 10
  • 前言 本文只要描述了iOS中的Core Animation(核心动画:隐式动画、显示动画)、贝塞尔曲线、UIVie...
    GitHubPorter阅读 2,928评论 7 11
  • 图层树 在UIKit中所有的视图都是基于UIView派生而来,UIView支持触摸时间,可以支持基于CoreGra...
    maguns阅读 832评论 1 3