Lottie源代码解析

news2024/11/28 14:39:29

Lottie-iOS

Lottie动画的原理:

  1. 一个完整动画View,是由很多个子Layer 组成,而每个子Layer主要通过shapes(形状),masks(蒙版),transform三大部分进行动画。
  2. Lottie框架通过读取JSON文件,获取到每个子Layer 的shapes,masks,以及出现时间,消失时间以及Transform各个属性的关键帧数组。
  3. 动画则是通过给CompositionLayer (所有的子layer都添加在这个Layer 上)的 currentFrame属性添加一个CABaseAnimation 来实现。
  4. 所有的子Layer根据currentFrame属性的变化,根据JSON中的关键帧数组计算出自己的当前状态并进行显示。

Lottie的创建方法:

/// 默认从main bundle中加载json文件和图片,animationName实际上就是json文件的名字
-(nonnull instancetype)animationNamed:(nonnull NSString *)animationName NS_SWIFT_NAME(init(name:));
/// 从指定的bundle中加载json文件和图片
-(nonnull instancetype)animationNamed:(nonnull NSString )animationName inBundle:(nonnull NSBundle )bundle NS_SWIFT_NAME(init(name:bundle:));
///直接从给定的json文件中加载动画,默认从main bundle加载
-(nonnull instancetype)animationFromJSON:(nonnull NSDictionary *)animationJSON NS_SWIFT_NAME(init(json:));
/// 从一个文件URL加载动画,但是不能使用web url作为参数
-(nonnull instancetype)animationWithFilePath:(nonnull NSString *)filePath NS_SWIFT_NAME(init(filePath:));
/// 给定一个反序列化的json文件数据和一个特定的bundle来初始化加载动画
-(nonnull instancetype)animationFromJSON:(nullable NSDictionary )animationJSON inBundle:(nullable NSBundle )bundle NS_SWIFT_NAME(init(json:bundle:));
/// 直接使用LOTComposition来创建动画, 图片从指定的bundle中加载
-(nonnull instancetype)initWithModel:(nullable LOTComposition )model inBundle:(nullable NSBundle )bundle;
/// 异步的从指定的URL中加载动画,这个url为webUrl
-(nonnull instancetype)initWithContentsOfURL:(nonnull NSURL *)url;

Lottie的使用:

/* 
 * 将动画从其当前位置播放到特定进度。
 * 动画将从其当前位置开始。
 * 如果循环动画,则动画将从开始位置无限循环到进度。
 * 如果 loopAnimation 为 NO,则动画将停止并调用完成块。
 */
- (void)playToProgress:(CGFloat)toProgress
        withCompletion:(nullable LOTAnimationCompletionBlock)completion;
/*
 * 播放从特定进度到特定进度的动画
 * 动画将从其当前位置开始。
 * 如果循环动画,则动画将从开始无限期地循环到结束进度
 * 如果 loopAnimation 为 NO,则动画将停止并调用完成块。
 */
- (void)playFromProgress:(CGFloat)fromStartProgress
              toProgress:(CGFloat)toEndProgress
          withCompletion:(nullable LOTAnimationCompletionBlock)completion;
/*
 * Plays the animation from its current position to a specific frame.
 * The animation will start from its current position.
 * If loopAnimation is YES the animation will loop from beginning to toFrame indefinitely.
 * If loopAnimation is NO the animation will stop and the completion block will be called.
 */
- (void)playToFrame:(nonnull NSNumber *)toFrame
     withCompletion:(nullable LOTAnimationCompletionBlock)completion;
/*
 * 将动画从特定帧播放到特定帧。
 * 动画将从其当前位置开始。
 * 如果循环动画,则动画将无限期地循环开始帧到结束帧。
 * 如果 loopAnimation 为 NO,则动画将停止并调用完成块。
 */
- (void)playFromFrame:(nonnull NSNumber *)fromStartFrame
              toFrame:(nonnull NSNumber *)toEndFrame
       withCompletion:(nullable LOTAnimationCompletionBlock)completion;
/**
 * 播放动画从当前位置到动画结束。
 * 动画将从其当前位置开始。
 * 如果循环动画,则动画将从头到尾无限循环。
 * 如果 loopAnimation 为 NO,则动画将停止并调用完成块。
 **/
- (void)playWithCompletion:(nullable LOTAnimationCompletionBlock)completion;

/// 播放
- (void)play;

/// 暂停到当前帧,调用完成块。
- (void)pause;

/// 停止,回到开头,调用完成块。
- (void)stop;

/// 将动画进度设置为特定帧。如果动画正在播放,它将停止并调用完成块。
- (void)setProgressWithFrame:(nonnull NSNumber *)currentFrame;

/// 将动画进度设置为特定帧。如果动画正在播放,它将停止,如果调用完成为是,则将调用完成块。
- (void)setProgressWithFrame:(nonnull NSNumber *)currentFrame callCompletionIfNecessary:(BOOL)callCompletion;

Lottie部分源码解析:

在这里插入图片描述
LOTAnimationView.m中初始化方法最后会调用下面这个方法:

- (instancetype)initWithModel:(LOTComposition *)model inBundle:(NSBundle *)bundle {
  self = [self initWithFrame:model.compBounds];
  if (self) {
    _bundle = bundle;
    [self _initializeAnimationContainer];
    [self _setupWithSceneModel:model];
  }
  return self;
}

LOTComposition对象包含一个LOTAssetGroup对象,该对象用于管理动画中使用的所有资源。LOTAssetGroup对象包含一个或多个LOTAsset对象,每个LOTAsset对象代表一个资源文件。例如,当动画中使用了一个图片资源时,LOTAssetGroup会创建一个LOTImageAsset对象来管理该图片资源。
LOTComposition还包含一个LOTLayerGroup,该对象用于管理所有图层和子图层。LOTLayerGroup对象包含一个或多个LOTLayer,每个LOTLayer代表一个图层。LOTLayer包含了图层的各种属性和元素,例如位置、大小、旋转、透明度等等。
每个LOTLayer都可以包含一个或多个子图层,每个子图层都可以包含一个或多个子视图。这种嵌套关系使得CALayer可以创建复杂的视图层级结构,并实现高效的渲染过程。
在Lottie中,LOTComposition被转换为一个名为LOTCompositionContainer的模型对象。LOTCompositionContainer包含了动画的各种属性和元素,例如图层、路径、形状、颜色等等。此外,LOTCompositionContainer还包含了一个CALayer用于渲染动画。

LOTComposition

可以在LOTComposition的代码里面找到Lottie解析json文件的过程:

#pragma mark - Initializer 基础初始化方法,json和bundle
- (instancetype _Nonnull)initWithJSON:(NSDictionary * _Nullable)jsonDictionary
                      withAssetBundle:(NSBundle * _Nullable)bundle {
  self = [super init];
  if (self) {
    if (jsonDictionary) {
      [self _mapFromJSON:jsonDictionary withAssetBundle:bundle];
    }
  }
  return self;
}

#pragma mark - Internal Methods 内部方法,用来解析json文件

- (void)_mapFromJSON:(NSDictionary *)jsonDictionary
     withAssetBundle:(NSBundle *)bundle {
  NSNumber *width = jsonDictionary[@"w"]; // 宽度 
  NSNumber *height = jsonDictionary[@"h"]; // 高度 
  if (width && height) {
    CGRect bounds = CGRectMake(0, 0, width.floatValue, height.floatValue);
    _compBounds = bounds;
  }
  // 动画开始frame
  _startFrame = [jsonDictionary[@"ip"] copy];
  // 动画结束frame
  _endFrame = [jsonDictionary[@"op"] copy];
  // 动画变化率
  _framerate = [jsonDictionary[@"fr"] copy];
  
  if (_startFrame && _endFrame && _framerate) {
    NSInteger frameDuration = (_endFrame.integerValue - _startFrame.integerValue) - 1;
    NSTimeInterval timeDuration = frameDuration / _framerate.floatValue;
    // 计算动画duration
    _timeDuration = timeDuration;
  }
  
  NSArray *assetArray = jsonDictionary[@"assets"];
  if (assetArray.count) {
    _assetGroup = [[LOTAssetGroup alloc] initWithJSON:assetArray withAssetBundle:bundle withFramerate:_framerate];
  }
  
  NSArray *layersJSON = jsonDictionary[@"layers"];
  if (layersJSON) {
    _layerGroup = [[LOTLayerGroup alloc] initWithLayerJSON:layersJSON
                                            withAssetGroup:_assetGroup
                                             withFramerate:_framerate];
  }
  
  [_assetGroup finalizeInitializationWithFramerate:_framerate];
}

在这个方法中,它调用了LOTAssetGroup的图片数组的初始化方法和LOTLayerGroup的layer层数组的初始化方法,生成了_assetGroup并使用其初始化了_layerGroup。具体来看一下。

LOTAssetGroup

LOTAssetGroup的初始化:

- (instancetype _Nonnull)initWithJSON:(NSArray * _Nonnull)jsonArray
                      withAssetBundle:(NSBundle * _Nullable)bundle
                        withFramerate:(NSNumber * _Nonnull)framerate {
  self = [super init];
  if (self) {
    _assetBundle = bundle;
    _assetMap = [NSMutableDictionary dictionary];
    NSMutableDictionary *assetJSONMap = [NSMutableDictionary dictionary];
    for (NSDictionary<NSString *, NSString *> *assetDictionary in jsonArray) {
      NSString *referenceID = assetDictionary[@"id"];
      if (referenceID) {
        assetJSONMap[referenceID] = assetDictionary;
      }
    }
    _assetJSONMap = assetJSONMap;
  }
  return self;
}

assetJSONMap存放的数据,里面是图片的各种信息:

{
    "image_0" : {"id":"image_0","w":180,"h":180,"u":"images/","p":"img_0.png","e":0},
    "image_1" : {"id":"image_1","w":600,"h":600,"u":"images/","p":"img_1.png","e":0},
    ...
}

我们最终在LOTAsset文件中找到了解析json数据的方法:

- (void)_mapFromJSON:(NSDictionary *)jsonDictionary
      withAssetGroup:(LOTAssetGroup * _Nullable)assetGroup
       withFramerate:(NSNumber *)framerate {
  _referenceID = [jsonDictionary[@"id"] copy];
  
  if (jsonDictionary[@"w"]) { // 宽度
    _assetWidth = [jsonDictionary[@"w"] copy];
  }
  
  if (jsonDictionary[@"h"]) { // 高度
    _assetHeight = [jsonDictionary[@"h"] copy];
  }
  
  if (jsonDictionary[@"u"]) { // 图片路径(图片文件夹)
    _imageDirectory = [jsonDictionary[@"u"] copy];
  }
  
  if (jsonDictionary[@"p"]) { // 图片名
    _imageName = [jsonDictionary[@"p"] copy];
  }

  NSArray *layersJSON = jsonDictionary[@"layers"];
  if (layersJSON) {
    _layerGroup = [[LOTLayerGroup alloc] initWithLayerJSON:layersJSON
                                            withAssetGroup:assetGroup
                                             withFramerate:framerate];
  }
}

LOTALayerGroup

LOTALayerGroup对json文件的解析:

- (void)_mapFromJSON:(NSArray *)layersJSON
      withAssetGroup:(LOTAssetGroup * _Nullable)assetGroup
       withFramerate:(NSNumber *)framerate {
  
  NSMutableArray *layers = [NSMutableArray array];
  NSMutableDictionary *modelMap = [NSMutableDictionary dictionary];
  NSMutableDictionary *referenceMap = [NSMutableDictionary dictionary];
  
  for (NSDictionary *layerJSON in layersJSON) {
    LOTLayer *layer = [[LOTLayer alloc] initWithJSON:layerJSON
                                      withAssetGroup:assetGroup
                                       withFramerate:framerate];
    [layers addObject:layer];
    if (layer.layerID) {
      modelMap[layer.layerID] = layer;
    }
    if (layer.referenceID) {
      referenceMap[layer.referenceID] = layer;
    }
  }
  
  _referenceIDMap = referenceMap;
  _modelMap = modelMap;
  _layers = layers;
}

里面调用了LOTALayer的初始化方法:

- (void)_mapFromJSON:(NSDictionary *)jsonDictionary
      withAssetGroup:(LOTAssetGroup *)assetGroup
       withFramerate:(NSNumber *)framerate {

  _layerName = [jsonDictionary[@"nm"] copy]; // layer名
  _layerID = [jsonDictionary[@"ind"] copy]; // layer的id,表示这是第几个layer
  
  NSNumber *layerType = jsonDictionary[@"ty"]; // 表示layer的类型,这个变量是一个枚举类型
  _layerType = layerType.integerValue;
 // LOTLayerTypePrecomp, 
 // LOTLayerTypeSolid, 
 // LOTLayerTypeImage, 
 // LOTLayerTypeNull, 
 // LOTLayerTypeShape, 
 // LOTLayerTypeUnknown
 
  if (jsonDictionary[@"refId"]) {
  // 这里的refId和图片文件的referenceID指向的是同一个标识符,
  // 表示这个layer动画会作用在 referenceID 指向的图片上
    _referenceID = [jsonDictionary[@"refId"] copy];
  }
  // 父layer
  _parentID = [jsonDictionary[@"parent"] copy];
  
  if (jsonDictionary[@"st"]) {
    // 开始的 frame
    _startFrame = [jsonDictionary[@"st"] copy];
  }
  // 开始的 frame,通常和 startFrame 值相同
  _inFrame = [jsonDictionary[@"ip"] copy];
  //最后一帧的frame
  _outFrame = [jsonDictionary[@"op"] copy];
  if (jsonDictionary[@"sr"]) {
    _timeStretch = [jsonDictionary[@"sr"] copy];
  } else {
    _timeStretch = @1;
  }

  if (_layerType == LOTLayerTypePrecomp) {
    _layerHeight = [jsonDictionary[@"h"] copy]; // 高度 
    _layerWidth = [jsonDictionary[@"w"] copy]; // 宽度 
    [assetGroup buildAssetNamed:_referenceID withFramerate:framerate];
  } else if (_layerType == LOTLayerTypeImage) {
    [assetGroup buildAssetNamed:_referenceID withFramerate:framerate];
    _imageAsset = [assetGroup assetModelForID:_referenceID];
    _layerWidth = [_imageAsset.assetWidth copy];
    _layerHeight = [_imageAsset.assetHeight copy];
  } else if (_layerType == LOTLayerTypeSolid) {
    _layerWidth = jsonDictionary[@"sw"];
    _layerHeight = jsonDictionary[@"sh"];
    NSString *solidColor = jsonDictionary[@"sc"];
    _solidColor = [UIColor LOT_colorWithHexString:solidColor];
  }
  
  _layerBounds = CGRectMake(0, 0, _layerWidth.floatValue, _layerHeight.floatValue);
  
  NSDictionary *ks = jsonDictionary[@"ks"];
  
  NSDictionary *opacity = ks[@"o"]; // 不透明度 
  if (opacity) {
    _opacity = [[LOTKeyframeGroup alloc] initWithData:opacity];
    [_opacity remapKeyframesWithBlock:^CGFloat(CGFloat inValue) {
      return LOT_RemapValue(inValue, 0, 100, 0, 1);
    }];
  }

  NSDictionary *timeRemap = jsonDictionary[@"tm"];
  if (timeRemap) {
    _timeRemapping = [[LOTKeyframeGroup alloc] initWithData:timeRemap];
    [_timeRemapping remapKeyframesWithBlock:^CGFloat(CGFloat inValue) {
      return inValue * framerate.doubleValue;
    }];
  }
  
  NSDictionary *rotation = ks[@"r"]; // 旋转 
  if (rotation == nil) {
    rotation = ks[@"rz"];
  }
  if (rotation) {
    _rotation = [[LOTKeyframeGroup alloc] initWithData:rotation];
    [_rotation remapKeyframesWithBlock:^CGFloat(CGFloat inValue) {
      return LOT_DegreesToRadians(inValue);
    }];
  }
  
  NSDictionary *position = ks[@"p"]; // 位置
  if ([position[@"s"] boolValue]) {
    // Separate dimensions
    _positionX = [[LOTKeyframeGroup alloc] initWithData:position[@"x"]];
    _positionY = [[LOTKeyframeGroup alloc] initWithData:position[@"y"]];
  } else {
    _position = [[LOTKeyframeGroup alloc] initWithData:position ];
  }
  
  NSDictionary *anchor = ks[@"a"]; // 锚点
  if (anchor) {
    _anchor = [[LOTKeyframeGroup alloc] initWithData:anchor];
  }
  
  NSDictionary *scale = ks[@"s"]; // 缩放比例
  if (scale) {
    _scale = [[LOTKeyframeGroup alloc] initWithData:scale];
    [_scale remapKeyframesWithBlock:^CGFloat(CGFloat inValue) {
      return LOT_RemapValue(inValue, -100, 100, -1, 1);
    }];
  }
  
  _matteType = [jsonDictionary[@"tt"] integerValue];
  
  
  NSMutableArray *masks = [NSMutableArray array];
  for (NSDictionary *maskJSON in jsonDictionary[@"masksProperties"]) {
    LOTMask *mask = [[LOTMask alloc] initWithJSON:maskJSON];
    [masks addObject:mask];
  }
  _masks = masks.count ? masks : nil;
  
  NSMutableArray *shapes = [NSMutableArray array];
  for (NSDictionary *shapeJSON in jsonDictionary[@"shapes"]) {
    id shapeItem = [LOTShapeGroup shapeItemWithJSON:shapeJSON];
    if (shapeItem) {
      [shapes addObject:shapeItem];
    }
  }
  _shapes = shapes;
  // 额外效果 
  NSArray *effects = jsonDictionary[@"ef"];
  if (effects.count > 0) {
    
    NSDictionary *effectNames = @{ @0: @"slider",
                                   @1: @"angle",
                                   @2: @"color",
                                   @3: @"point",
                                   @4: @"checkbox",
                                   @5: @"group",
                                   @6: @"noValue",
                                   @7: @"dropDown",
                                   @9: @"customValue",
                                   @10: @"layerIndex",
                                   @20: @"tint",
                                   @21: @"fill" };
                             
    for (NSDictionary *effect in effects) {
      NSNumber *typeNumber = effect[@"ty"];
      NSString *name = effect[@"nm"];
      NSString *internalName = effect[@"mn"];
      NSString *typeString = effectNames[typeNumber];
      if (typeString) {
        NSLog(@"%s: Warning: %@ effect not supported: %@ / %@", __PRETTY_FUNCTION__, typeString, internalName, name);
      }
    }
  }
}

LOTCompositionContainer

Lottie的整个动画,就是一个自定义属性的CAAnimation动画,自定义的属性就是currentFrame,这个动画是添加到LOTCompositionContainer上的。而LOTCompositionContainerLOTLayerContainer的子类,内部通过差值计算来得出对应帧在不同时间的值。

_compContainer = [[LOTCompositionContainer alloc] initWithModel:nil inLayerGroup:nil withLayerGroup:_sceneModel.layerGroup withAssestGroup:_sceneModel.assetGroup];
child = [[LOTLayerContainer alloc] initWithModel:layer inLayerGroup:childGroup];

不断的对子Layer预合成。
LOTLayerContainer初始化代码如下:

- (void)commonInitializeWith:(LOTLayer *)layer
                inLayerGroup:(LOTLayerGroup *)layerGroup {
  if (layer == nil) {
    return;
  }
  _layerName = layer.layerName;
  if (layer.layerType == LOTLayerTypeImage ||
      layer.layerType == LOTLayerTypeSolid ||
      layer.layerType == LOTLayerTypePrecomp) {
    _wrapperLayer.bounds = CGRectMake(0, 0, layer.layerWidth.floatValue, layer.layerHeight.floatValue);
    _wrapperLayer.anchorPoint = CGPointMake(0, 0);
    _wrapperLayer.masksToBounds = YES;
    DEBUG_Center.position = LOT_RectGetCenterPoint(self.bounds);
  }
  
  if (layer.layerType == LOTLayerTypeImage) {
    [self _setImageForAsset:layer.imageAsset];
  }
  
  _inFrame = [layer.inFrame copy];
  _outFrame = [layer.outFrame copy];

  _timeStretchFactor = [layer.timeStretch copy];
  _transformInterpolator = [LOTTransformInterpolator transformForLayer:layer];

  if (layer.parentID != nil) {
    NSNumber *parentID = layer.parentID;
    LOTTransformInterpolator *childInterpolator = _transformInterpolator;
    while (parentID != nil) {
      LOTLayer *parentModel = [layerGroup layerModelForID:parentID];
      LOTTransformInterpolator *interpolator = [LOTTransformInterpolator transformForLayer:parentModel];
      childInterpolator.inputNode = interpolator;
      childInterpolator = interpolator;
      parentID = parentModel.parentID;
    }
  }
  _opacityInterpolator = [[LOTNumberInterpolator alloc] initWithKeyframes:layer.opacity.keyframes];
  if (layer.layerType == LOTLayerTypeShape &&
      layer.shapes.count) {
    [self buildContents:layer.shapes];
  }
  if (layer.layerType == LOTLayerTypeSolid) {
    _wrapperLayer.backgroundColor = layer.solidColor.CGColor;
  }
  if (layer.masks.count) {
    _maskLayer = [[LOTMaskContainer alloc] initWithMasks:layer.masks];
    _wrapperLayer.mask = _maskLayer;
  }
  
  NSMutableDictionary *interpolators = [NSMutableDictionary dictionary];
  // 设置属性
  _valueInterpolators = interpolators;
}

有图动画设置图片方法:

- (void)_setImageForAsset:(LOTAsset *)asset {
  self.asyncLoadingResource = NO;
  
  if (asset.imageName) {
    UIImage *image;
    if ([asset.imageName hasPrefix:@"data:"]) {
      // Contents look like a data: URL. Ignore asset.imageDirectory and simply load the image directly.
      NSURL *imageUrl = [NSURL URLWithString:asset.imageName];
      NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
      image = [UIImage imageWithData:imageData];
    } else if (asset.rootDirectory.length > 0) {
    // 有rootDirectory
      NSString *rootDirectory  = asset.rootDirectory;
      if (asset.imageDirectory.length > 0) {
      // 拼接图片资源“p”字段,图片文件名
        rootDirectory = [rootDirectory stringByAppendingPathComponent:asset.imageDirectory];
      }
      NSString *imagePath = [rootDirectory stringByAppendingPathComponent:asset.imageName];
        
      id<LOTImageCache> imageCache = [LOTCacheProvider imageCache];
      // 存在imageCache
      if (imageCache) {
        image = [imageCache imageForKey:imagePath];
        if (!image) {
          image = [UIImage imageWithContentsOfFile:imagePath];
          [imageCache setImage:image forKey:imagePath];
        }
      } else {
        image = [UIImage imageWithContentsOfFile:imagePath];
      }
    } else if (!asset.ignoreBundleResource) {
        NSString *imagePath = [asset.assetBundle pathForResource:asset.imageName ofType:nil];
        image = [UIImage imageWithContentsOfFile:imagePath];
    }

    //try loading from asset catalogue instead if all else fails
    if (!image && !asset.ignoreBundleResource) {
      image = [UIImage imageNamed:asset.imageName inBundle:asset.assetBundle compatibleWithTraitCollection:nil];
    }
    
    if (image) {
      _wrapperLayer.contents = (__bridge id _Nullable)(image.CGImage);
    } else if (asset.baseURL) {
        // 通过url下载
    } else {
      NSLog(@"%s: Warn: image not found: %@", __PRETTY_FUNCTION__, asset.imageName);
    }
  }
}

播放:

  • layer首次加载时会调用 +(BOOL)needsDisplayForKey:(NSString *)key方法来判断当前指定的属性key改变是否需要重新绘制,默认返回NO
  • 当Core Animartion中的key或者keypath等于+(BOOL)needsDisplayForKey:(NSString *)key 方法中指定的key,便会自动调用setNeedsDisplay方法
  • 当指定key发生更改时,会触发actionForKey
  • runloop是一个循环处理事件和消息的方法,CATransaction begin和 CATransaction commit 进行修改和提交新事务。
  • 每个RunLoop周期中会自动开始一次新的事务,即使你不显式的使用[CATranscation begin]开始一次事务,任何在一次RunLoop运行时循环中属性的改变都会被集中起来,执行默认0.25秒的动画,在runloop快结束时,它会调用下一个事务display,也就是隐式动画
  • CALayer方法重绘响应链
    [layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layerDelegate displayLayer:]
    [layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layer drawInContext:] -> [layerDelegate drawLayer: inContext:]

LOTLayerContainer里面可以看到needsDisplayForKey指定了keycurrentFrame时触发重绘。actionForKey是接收指定key被修改时触发的行为操作,在下面代码中看到当keycurrentFrame时添加一个CABasicAnimation动画。

+ (BOOL)needsDisplayForKey:(NSString *)key {
  if ([key isEqualToString:@"currentFrame"]) {
    return YES;
  }
  return [super needsDisplayForKey:key];
}
- (id<CAAction>)actionForKey:(NSString *)event {
  if ([event isEqualToString:@"currentFrame"]) {
    CABasicAnimation *theAnimation = [CABasicAnimation
                                      animationWithKeyPath:event];
    theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    theAnimation.fromValue = [[self presentationLayer] valueForKey:event];
    return theAnimation;
  }
  return [super actionForKey:event];
}
 - (void)playFromFrame:(nonnull NSNumber *)fromStartFrame
              toFrame:(nonnull NSNumber *)toEndFrame
       withCompletion:(nullable LOTAnimationCompletionBlock)completion {
  if (_isAnimationPlaying) {
    return;
  }
  _playRangeStartFrame = fromStartFrame;
  _playRangeEndFrame = toEndFrame;
  if (completion) {
    self.completionBlock = completion;
  }
  if (!_sceneModel) {
    _isAnimationPlaying = YES;
    return;
  }

  BOOL playingForward = ((_animationSpeed > 0) && (toEndFrame.floatValue > fromStartFrame.floatValue))
    || ((_animationSpeed < 0) && (fromStartFrame.floatValue > toEndFrame.floatValue));

  CGFloat leftFrameValue = MIN(fromStartFrame.floatValue, toEndFrame.floatValue);
  CGFloat rightFrameValue = MAX(fromStartFrame.floatValue, toEndFrame.floatValue);

  NSNumber *currentFrame = [self _frameForProgress:_animationProgress];

  currentFrame = @(MAX(MIN(currentFrame.floatValue, rightFrameValue), leftFrameValue));

  if (currentFrame.floatValue == rightFrameValue && playingForward) {
    currentFrame = @(leftFrameValue);
  } else if (currentFrame.floatValue == leftFrameValue && !playingForward) {
    currentFrame = @(rightFrameValue);
  }
  _animationProgress = [self _progressForFrame:currentFrame];
  
  CGFloat currentProgress = _animationProgress * (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue);
  CGFloat skipProgress;
  if (playingForward) {
    skipProgress = currentProgress - leftFrameValue;
  } else {
    skipProgress = rightFrameValue - currentProgress;
  }
  NSTimeInterval offset = MAX(0, skipProgress) / _sceneModel.framerate.floatValue;
  if (!self.window) {
    _shouldRestoreStateWhenAttachedToWindow = YES;
    _completionBlockToRestoreWhenAttachedToWindow = self.completionBlock;
    self.completionBlock = nil;
  } else {
    float repeatCount = self.repeatCount == 0 ? HUGE_VALF : self.repeatCount;
    NSTimeInterval duration = (ABS(toEndFrame.floatValue - fromStartFrame.floatValue) / _sceneModel.framerate.floatValue);
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"currentFrame"];
    animation.speed = _animationSpeed;
    animation.fromValue = fromStartFrame;
    animation.toValue = toEndFrame;
    animation.duration = duration;
    animation.fillMode = kCAFillModeBoth;
    animation.repeatCount = _loopAnimation ? repeatCount : 1;
    animation.autoreverses = _autoReverseAnimation;
    animation.delegate = self;
    animation.removedOnCompletion = NO;
    if (offset != 0) {
      CFTimeInterval currentTime = CACurrentMediaTime();
      CFTimeInterval currentLayerTime = [self.layer convertTime:currentTime fromLayer:nil];
      animation.beginTime = currentLayerTime - (offset * 1 / _animationSpeed);
    }
    [_compContainer addAnimation:animation forKey:kCompContainerAnimationKey];
    _compContainer.shouldRasterize = NO;
  }
  _isAnimationPlaying = YES;
}

最底层的LOTLayerContainer继承自CALayer,添加了currentFrame属性,LOTCompositionContainer又是继承自LOTLayerContainer,为LOTCompositionContainer对象添加了一个CABaseAnimation动画,然后重写CALayerdisplay方法,在display方法中通过 CALayer中的presentationLayer获取在动画中变化的currentFrame数值 ,再通过遍历每一个子layer,将更新后的currentFrame传入,来实时更新每一个子layer的显示。核心代码在LOTLayerContainer中。

- (void)displayWithFrame:(NSNumber *)frame forceUpdate:(BOOL)forceUpdate {
  NSNumber *newFrame = @(frame.floatValue / self.timeStretchFactor.floatValue);
  if (ENABLE_DEBUG_LOGGING) NSLog(@"View %@ Displaying Frame %@, with local time %@", self, frame, newFrame);
  BOOL hidden = NO;
  if (_inFrame && _outFrame) {
    hidden = (frame.floatValue < _inFrame.floatValue ||
              frame.floatValue > _outFrame.floatValue);
  }
  self.hidden = hidden;
  if (hidden) {
    return;
  }
  if (_opacityInterpolator && [_opacityInterpolator hasUpdateForFrame:newFrame]) {
    self.opacity = [_opacityInterpolator floatValueForFrame:newFrame];
  }
  if (_transformInterpolator && [_transformInterpolator hasUpdateForFrame:newFrame]) {
    _wrapperLayer.transform = [_transformInterpolator transformForFrame:newFrame];
  }
  // 更新contentsGroup
  [_contentsGroup updateWithFrame:newFrame withModifierBlock:nil forceLocalUpdate:forceUpdate];
  _maskLayer.currentFrame = newFrame;
}

它实际上完成了以下几件事:

  1. 根据子Layer的起始帧和结束帧判断当前帧子Layer是否显示
  2. 更新子Layer当前帧的透明度
  3. 更新子Layer当前帧的transform
  4. 更新子Layer中路径和形状等内容的变化
    上面动画显示的2,3,4步都是通过XXInterpolator这种类,来从当前frame中计算出我们需要的值。下面看一个示例:
@interface LOTTransformInterpolator : NSObject
// 。。。
@property (nonatomic, readonly) LOTPointInterpolator *positionInterpolator;
@property (nonatomic, readonly) LOTPointInterpolator *anchorInterpolator;
@property (nonatomic, readonly) LOTSizeInterpolator *scaleInterpolator;
@property (nonatomic, readonly) LOTNumberInterpolator *rotationInterpolator;
@property (nonatomic, readonly) LOTNumberInterpolator *positionXInterpolator;
@property (nonatomic, readonly) LOTNumberInterpolator *positionYInterpolator;
@property (nonatomic, strong, nullable) NSString *parentKeyName;
// 。。。

针对transform变换需要很多的信息,LOTTransformInterpolator中提供了这些所需的信息。

- (CATransform3D)transformForFrame:(NSNumber *)frame {
  CATransform3D baseXform = CATransform3DIdentity;
  if (_inputNode) {
    baseXform = [_inputNode transformForFrame:frame];
  }
  CGPoint position = CGPointZero;
  if (_positionInterpolator) {
    position = [_positionInterpolator pointValueForFrame:frame];
  }
  if (_positionXInterpolator &&
      _positionYInterpolator) {
    position.x = [_positionXInterpolator floatValueForFrame:frame];
    position.y = [_positionYInterpolator floatValueForFrame:frame];
  }
  CGPoint anchor = [_anchorInterpolator pointValueForFrame:frame];
  CGSize scale = [_scaleInterpolator sizeValueForFrame:frame];
  CGFloat rotation = [_rotationInterpolator floatValueForFrame:frame];
  CATransform3D translateXform = CATransform3DTranslate(baseXform, position.x, position.y, 0);
  CATransform3D rotateXform = CATransform3DRotate(translateXform, rotation, 0, 0, 1);
  CATransform3D scaleXform = CATransform3DScale(rotateXform, scale.width, scale.height, 1);
  CATransform3D anchorXform = CATransform3DTranslate(scaleXform, -1 * anchor.x, -1 * anchor.y, 0);
  return anchorXform;
}

插值计算过程:

// 根据前一帧与后一帧进行计算
- (CGPoint)pointValueForFrame:(NSNumber *)frame {
  CGFloat progress = [self progressForFrame:frame];
  CGPoint returnPoint;
  if (progress == 0) {
    returnPoint = self.leadingKeyframe.pointValue;
  } else if (progress == 1) {
    returnPoint = self.trailingKeyframe.pointValue;
  } else if (!CGPointEqualToPoint(self.leadingKeyframe.spatialOutTangent, CGPointZero) ||
             !CGPointEqualToPoint(self.trailingKeyframe.spatialInTangent, CGPointZero)) {
    // Spatial Bezier path
    CGPoint outTan = LOT_PointAddedToPoint(self.leadingKeyframe.pointValue, self.leadingKeyframe.spatialOutTangent);
    CGPoint inTan = LOT_PointAddedToPoint(self.trailingKeyframe.pointValue, self.trailingKeyframe.spatialInTangent);
    returnPoint = LOT_PointInCubicCurve(self.leadingKeyframe.pointValue, outTan, inTan, self.trailingKeyframe.pointValue, progress);
  } else {
    returnPoint = LOT_PointInLine(self.leadingKeyframe.pointValue, self.trailingKeyframe.pointValue, progress);
  }
  if (self.hasDelegateOverride) {
    return [self.delegate pointForFrame:frame.floatValue
                          startKeyframe:self.leadingKeyframe.keyframeTime.floatValue
                            endKeyframe:self.trailingKeyframe.keyframeTime.floatValue
                   interpolatedProgress:progress
                             startPoint:self.leadingKeyframe.pointValue
                               endPoint:self.trailingKeyframe.pointValue
                           currentPoint:returnPoint];
  }
  return returnPoint;
}
CGPoint LOT_PointInLine(CGPoint A, CGPoint B, CGFloat T) {
  CGPoint C;
  C.x = A.x - ((A.x - B.x) * T);
  C.y = A.y - ((A.y - B.y) * T);
  return C;
}

当传入当前frame时,这些interpolator会返回不同的数值,从而组成当前的transform。这些不同的Interpolar会根据自己的算法返回当前所需要的值,但是他们大体的流程都是一样的:

  1. 在关键帧数组中找到当前frame的前一个关键帧(leadingKeyframe)和后一个关键帧(trailingKeyframe)
  2. 计算当前frame 在 leadingKeyframe 和 trailingKeyframe 的进度(progress)
  3. 根据这个progress以及 leadingKeyframe,trailingKeyframe算出当前frame下的值。(不同的Interpolator算法不同

小结:

设计师做设计,开发者写实现,各司其职。
Lottie内部帮我们做了json文件映射到不同类的不同属性中,通过一系列的计算,确定出每一帧的数据,然后完美的显示在屏幕上。开发者只需要通过创建方法创建、播放方法播放,简单几行代码,就可以实现炫酷的动画。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/763830.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

jdk9以上反射报错 , jib 镜像打包添加配置

错误信息&#xff1a; unable to make protected final java.lang.class java.lang.classloader.defineclass 在IDEA中添加&#xff1a; --add-opens java.base/java.langALL-UNNAMED 即可启动 如果用了jib-maven-plugin 发布镜像&#xff0c; 怎么配置这个参数进去呢&…

多表查询进阶

首先两表如下所示 两表结构如下 查询要求 1 所有有门派的人员信息 select *from t_emp right join t_dept on t_emp.deptIdt_dept.id; 2 列出所有用户&#xff0c;并显示其机构信息 select t_emp.name,t_dept.id,t_dept.deptName,t_dept.address,t_dept.CEO from t_emp l…

深蓝学院C++基础与深度解析笔记 第 12 章 类进阶

深蓝学院C基础与深度解析笔记 第 12 章 类进阶 1. 运算符重载 ● 使用 operator 关键字引入重载函数&#xff1a; – 重载不能发明新的运算&#xff0c;不能改变运算的优先级与结合性&#xff0c;通常不改变运算含义 – 函数参数个数与运算操作数个数相同&#xff0c;至少一…

C++模拟实现unordered_map和unordered_set(哈希)

目录 一、unordered系列关联式容器 1.1 unordered_map 1.1.1 unordered_map 1.1.2 unordered_map接口说明 1. unordered_map的容量 2. unordered_map的迭代器 3.unordered_map的元素访问 4. unordered_map的查询 5. unordered_map的修改操作 6. unordered_map的桶操作…

ros::catkin_create_pkg

用下面的命令即可 catkin_create_pkg first_pkg rospy roscpp std_msg -m ur-email-name

HBase(一)HBase v2.2 高可用多节点搭建

最近刚刚完成了HBase相关的一个项目,作为项目的技术负责人,完成了大部分的项目部署,特性调研工作,以此系列文章作为上一阶段工作的总结. 前言 其实目前就大多数做应用的情况来讲,我们并不需要去自己搭建一套HBase的集群,现有的很多云厂商提供的服务已经极大的方便日常的应用使…

接口测试工具——Postman使用详解

目录 Postman简介 Postman主界面 菜单栏 工具栏 请求管理区 环境管理区 请求设计区 发送请求 发送GET请求 Postman发送GET请求 发送表单格式POST请求 发送JSON格式POST请求 发送XML格式POST请求 发送文件上传类型的请求 响应 环境和变量 环境变量设置 环境变量…

【Ceph的介绍】

目录 1、存储基础1、单机存储设备2、单机存储的问题3、商业存储解决方案4、分布式存储&#xff08;软件定义的存储 SDS&#xff09;1、分布式存储的类型 2、Ceph 简介3、Ceph 优势4、Ceph 架构5、Ceph 核心组件1、Pool中数据保存方式支持两种类型2、Pool、PG 和 OSD 的关系 6、…

测试用例设计方法-场景法详解

01、定义 场景法是通过运用场景来对系统的功能点或业务流程的描述&#xff0c;从而提高测试效果的一种方法。 场景法一般包含基本流和备用流&#xff0c;从一个流程开始&#xff0c;通过描述经过的路径来确定的过程&#xff0c;经过遍历所有的基本流和备用流来完成整个场景。…

SOPC之NiosⅡ系统(四)

NIOS Ⅱ系统实例&#xff0c;参考自特权同学《勇敢的芯-伴你玩转NIOS Ⅱ》 一些基础操作就不再赘述 目录 1.创建Quartus项目 1.2 进入Platform Designer添加组件并设置 1.2.1 设置时钟频率50MHz&#xff1b; 1.2.2 添加Nios Ⅱ组件 1.2.3 添加RAM组件 1.2.4 设置Nios Ⅱ…

【每日随笔】摩托车安全驾驶 ① ( 摩托车骑行准备 | 买好保险 | 摩托车必要改装 - 护杠 + 行车记录仪 | 骑行护具 )

文章目录 一、摩托车骑行准备1、买好保险2、摩托车必要改装 - 护杠 行车记录仪3、骑行护具 德州考驾照归来 , 提了一辆 铃木 UY125 , 注意安全驾驶 , 以后上班就骑摩托车了 ; 由于居住证上的地址是海淀区 , 目前住在学院路 , 导致无法把车落户到自己名下 , 只能上公户了 ; 车…

G1垃圾收集器-JVM(十三)

上篇文章说了CMS垃圾收集器使用以及三色标记如何解决cms的一些问题。分别有初始标记&#xff0c;并发标记&#xff0c;重新标记&#xff0c;并发清理&#xff0c;并发重置。 CMS垃圾收集器&三色标记-JVM&#xff08;十二&#xff09; G1收集器&#xff08;Garbage-First&a…

浅析缓存一致性的解析方案

各位同学们平时开发的时候除了使用到数据库&#xff08;这里以mysql为例&#xff09;还会用到相关的缓存&#xff08;这里以redis为例&#xff09;操作。 举一个常用的场景当我们写的接口性能相对比较慢的时候&#xff08;高并发场景需要响应速度很快&#xff09;为了保证性能的…

LeetCode144. 二叉树的前序遍历

144. 二叉树的前序遍历 文章目录 [144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/)一、题目二、思路及代码&#xff08;1&#xff09;递归&#xff08;2&#xff09;迭代&#xff08;两种方法&#xff09; 一、题目 给你二叉树的根节点…

AlienSwap 首期 Launchpad — 偶像女团 NFT+RWA 的创新探索

NFT 是整个加密市场一致看好&#xff0c;并认为会继续爆发的领域。随着更多的 NFT 平台和 NFT 项目的推出&#xff0c;NFT 市场的格局也在不断变化。从开始的 OpenSea 占据绝对领先地位&#xff0c;到 Blur 的横空出世风头无两&#xff0c;在加密领域&#xff0c;局势更迭总是在…

【Java面试丨并发编程】线程中并发安全

一、Synchronized关键字的底层原理 1. Synchronized的作用 Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】&#xff0c;其他线程再想获取这个【对象锁】时就会阻塞住 2. Monitor Synchronized【对象锁】底层是由Monitor实现&#xff0c;…

泰裤辣!这是什么操作,自动埋点,还能传参?

目录 前言 参数放在注释中 准备入口文件 编写插件 运行代码 完整代码 参数放在局部作用域中 准备源代码 编写插件 运行代码 完整代码 总结 前言 在上篇文章讲了如何通过手写babel插件自动给函数埋点之后&#xff0c;就有同学问我&#xff0c;自动插入埋点的函数怎么…

基于IMX6ULL的AP3216C的QT动态数据曲线图显示

前言&#xff1a;本文为手把手教学 LinuxQT 的典型基础项目 AP3216C 的数据折线图显示&#xff0c;项目使用正点原子的 IMX6ULL 阿尔法( Cortex-A7 系列)开发板。项目需要实现 AP3216C 在 Linux 系统下的驱动&#xff0c;使用 QT 设计 AP3216C 的数据显示页面作为项目的应用层。…

消息中间件RabbitMQ简介

1.1消息队列中间件简介 消息队列中间件是分布式系统中重要的组件&#xff0c;主要解决应用耦合&#xff0c;异步消息&#xff0c;流量削锋等问题实现高性能&#xff0c;高可用&#xff0c;可伸缩和最终一致性[架构] 使用较多的消息队列有ActiveMQ&#xff0c;RabbitMQ&#xff…

人工智能安全风险:零信任的作用

人工智能&#xff08;AI&#xff09;和机器学习技术飞速发展&#xff0c;我们所处的时代正在经历前所未有的创新。但是&#xff0c;技术飞速发展的同时也带来了各种挑战。人工智能技术越来越复杂&#xff0c;与之相关的网络安全风险也越来越棘手&#xff0c;随之产生了一个新的…