|
@@ -0,0 +1,845 @@
|
|
|
+//
|
|
|
+// LOTAnimationView
|
|
|
+// LottieAnimator
|
|
|
+//
|
|
|
+// Created by Brandon Withrow on 12/14/15.
|
|
|
+// Copyright © 2015 Brandon Withrow. All rights reserved.
|
|
|
+//
|
|
|
+
|
|
|
+#import "LOTAnimationView.h"
|
|
|
+#import "LOTPlatformCompat.h"
|
|
|
+#import "LOTModels.h"
|
|
|
+#import "LOTHelpers.h"
|
|
|
+#import "LOTAnimationView_Internal.h"
|
|
|
+#import "LOTAnimationCache.h"
|
|
|
+#import "LOTCompositionContainer.h"
|
|
|
+
|
|
|
+static NSString * const kCompContainerAnimationKey = @"play";
|
|
|
+
|
|
|
+@implementation LOTAnimationView {
|
|
|
+ LOTCompositionContainer *_compContainer;
|
|
|
+ NSNumber *_playRangeStartFrame;
|
|
|
+ NSNumber *_playRangeEndFrame;
|
|
|
+ CGFloat _playRangeStartProgress;
|
|
|
+ CGFloat _playRangeEndProgress;
|
|
|
+ NSBundle *_bundle;
|
|
|
+ CGFloat _animationProgress;
|
|
|
+ // Properties for tracking automatic restoration of animation.
|
|
|
+ BOOL _shouldRestoreStateWhenAttachedToWindow;
|
|
|
+ LOTAnimationCompletionBlock _completionBlockToRestoreWhenAttachedToWindow;
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - Convenience Initializers
|
|
|
+
|
|
|
++ (nonnull instancetype)animationNamed:(nonnull NSString *)animationName {
|
|
|
+ return [self animationNamed:animationName inBundle:[NSBundle mainBundle]];
|
|
|
+}
|
|
|
+
|
|
|
++ (nonnull instancetype)animationNamed:(nonnull NSString *)animationName inBundle:(nonnull NSBundle *)bundle {
|
|
|
+ LOTComposition *comp = [LOTComposition animationNamed:animationName inBundle:bundle];
|
|
|
+ return [[self alloc] initWithModel:comp inBundle:bundle];
|
|
|
+}
|
|
|
+
|
|
|
++ (nonnull instancetype)animationFromJSON:(nonnull NSDictionary *)animationJSON {
|
|
|
+ return [self animationFromJSON:animationJSON inBundle:[NSBundle mainBundle]];
|
|
|
+}
|
|
|
+
|
|
|
++ (nonnull instancetype)animationFromJSON:(nullable NSDictionary *)animationJSON inBundle:(nullable NSBundle *)bundle {
|
|
|
+ LOTComposition *comp = [LOTComposition animationFromJSON:animationJSON inBundle:bundle];
|
|
|
+ return [[self alloc] initWithModel:comp inBundle:bundle];
|
|
|
+}
|
|
|
+
|
|
|
++ (nonnull instancetype)animationWithFilePath:(nonnull NSString *)filePath {
|
|
|
+ LOTComposition *comp = [LOTComposition animationWithFilePath:filePath];
|
|
|
+ return [[self alloc] initWithModel:comp inBundle:[NSBundle mainBundle]];
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - Initializers
|
|
|
+
|
|
|
+- (instancetype)initWithContentsOfURL:(NSURL *)url {
|
|
|
+ self = [self initWithFrame:CGRectZero];
|
|
|
+ if (self) {
|
|
|
+ LOTComposition *laScene = [[LOTAnimationCache sharedCache] animationForKey:url.absoluteString];
|
|
|
+ if (laScene) {
|
|
|
+ laScene.cacheKey = url.absoluteString;
|
|
|
+ [self _initializeAnimationContainer];
|
|
|
+ [self _setupWithSceneModel:laScene];
|
|
|
+ } else {
|
|
|
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
|
|
|
+ NSData *animationData = [NSData dataWithContentsOfURL:url];
|
|
|
+ if (!animationData) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ NSError *error;
|
|
|
+ NSDictionary *animationJSON = [NSJSONSerialization JSONObjectWithData:animationData
|
|
|
+ options:0 error:&error];
|
|
|
+ if (error || !animationJSON) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ LOTComposition *laScene = [[LOTComposition alloc] initWithJSON:animationJSON withAssetBundle:[NSBundle mainBundle]];
|
|
|
+ dispatch_async(dispatch_get_main_queue(), ^(void) {
|
|
|
+ [[LOTAnimationCache sharedCache] addAnimation:laScene forKey:url.absoluteString];
|
|
|
+ laScene.cacheKey = url.absoluteString;
|
|
|
+ [self _initializeAnimationContainer];
|
|
|
+ [self _setupWithSceneModel:laScene];
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+- (instancetype)initWithModel:(LOTComposition *)model inBundle:(NSBundle *)bundle {
|
|
|
+ self = [self initWithFrame:model.compBounds];
|
|
|
+ if (self) {
|
|
|
+ _bundle = bundle;
|
|
|
+ [self _initializeAnimationContainer];
|
|
|
+ [self _setupWithSceneModel:model];
|
|
|
+ }
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+- (instancetype)initWithFrame:(CGRect)frame {
|
|
|
+ self = [super initWithFrame:frame];
|
|
|
+ if (self) {
|
|
|
+ [self _commonInit];
|
|
|
+ }
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+- (instancetype)initWithCoder:(NSCoder *)coder {
|
|
|
+ self = [super initWithCoder:coder];
|
|
|
+ if (self) {
|
|
|
+ [self _commonInit];
|
|
|
+ }
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - Inspectables
|
|
|
+
|
|
|
+- (void)setAnimation:(NSString *)animationName {
|
|
|
+
|
|
|
+ _animation = animationName;
|
|
|
+
|
|
|
+ [self setAnimationNamed:animationName];
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - Internal Methods
|
|
|
+
|
|
|
+#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR
|
|
|
+
|
|
|
+- (void)_initializeAnimationContainer {
|
|
|
+ self.clipsToBounds = YES;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)_commonInit {
|
|
|
+ _animationSpeed = 1;
|
|
|
+ _animationProgress = 0;
|
|
|
+ _loopAnimation = NO;
|
|
|
+ _autoReverseAnimation = NO;
|
|
|
+ _playRangeEndFrame = nil;
|
|
|
+ _playRangeStartFrame = nil;
|
|
|
+ _playRangeEndProgress = 0;
|
|
|
+ _playRangeStartProgress = 0;
|
|
|
+ _shouldRasterizeWhenIdle = NO;
|
|
|
+ [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(_handleWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
|
|
|
+ [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(_handleWillEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
|
|
|
+}
|
|
|
+
|
|
|
+#else
|
|
|
+
|
|
|
+- (void)_initializeAnimationContainer {
|
|
|
+ self.wantsLayer = YES;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)_commonInit {
|
|
|
+ _animationSpeed = 1;
|
|
|
+ _animationProgress = 0;
|
|
|
+ _loopAnimation = NO;
|
|
|
+ _autoReverseAnimation = NO;
|
|
|
+ _playRangeEndFrame = nil;
|
|
|
+ _playRangeStartFrame = nil;
|
|
|
+ _playRangeEndProgress = 0;
|
|
|
+ _playRangeStartProgress = 0;
|
|
|
+ _shouldRasterizeWhenIdle = NO;
|
|
|
+}
|
|
|
+
|
|
|
+#endif
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+- (void)dealloc {
|
|
|
+ [NSNotificationCenter.defaultCenter removeObserver:self];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)_setupWithSceneModel:(LOTComposition *)model {
|
|
|
+ if (_sceneModel) {
|
|
|
+ [self _removeCurrentAnimationIfNecessary];
|
|
|
+ [self _callCompletionIfNecessary:NO];
|
|
|
+ [_compContainer removeFromSuperlayer];
|
|
|
+ _compContainer = nil;
|
|
|
+ _sceneModel = nil;
|
|
|
+ [self _commonInit];
|
|
|
+ }
|
|
|
+
|
|
|
+ _sceneModel = model;
|
|
|
+ _compContainer = [[LOTCompositionContainer alloc] initWithModel:nil inLayerGroup:nil withLayerGroup:_sceneModel.layerGroup withAssestGroup:_sceneModel.assetGroup];
|
|
|
+ [self.layer addSublayer:_compContainer];
|
|
|
+ [self _restoreState];
|
|
|
+ [self setNeedsLayout];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)_restoreState {
|
|
|
+ if (_isAnimationPlaying) {
|
|
|
+ _isAnimationPlaying = NO;
|
|
|
+ if (_playRangeStartFrame && _playRangeEndFrame) {
|
|
|
+ [self playFromFrame:_playRangeStartFrame toFrame:_playRangeEndFrame withCompletion:self.completionBlock];
|
|
|
+ } else if (_playRangeEndProgress != _playRangeStartProgress) {
|
|
|
+ [self playFromProgress:_playRangeStartProgress toProgress:_playRangeEndProgress withCompletion:self.completionBlock];
|
|
|
+ } else {
|
|
|
+ [self playWithCompletion:self.completionBlock];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ self.animationProgress = _animationProgress;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+- (void)_removeCurrentAnimationIfNecessary {
|
|
|
+ _isAnimationPlaying = NO;
|
|
|
+ [_compContainer removeAllAnimations];
|
|
|
+ _compContainer.shouldRasterize = _shouldRasterizeWhenIdle;
|
|
|
+}
|
|
|
+
|
|
|
+- (CGFloat)_progressForFrame:(NSNumber *)frame {
|
|
|
+ if (!_sceneModel) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ return ((frame.floatValue - _sceneModel.startFrame.floatValue) / (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue));
|
|
|
+}
|
|
|
+
|
|
|
+- (NSNumber *)_frameForProgress:(CGFloat)progress {
|
|
|
+ if (!_sceneModel) {
|
|
|
+ return @0;
|
|
|
+ }
|
|
|
+ return @(((_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue) * progress) + _sceneModel.startFrame.floatValue);
|
|
|
+}
|
|
|
+
|
|
|
+- (BOOL)_isSpeedNegative {
|
|
|
+ // If the animation speed is negative, then we're moving backwards.
|
|
|
+ return _animationSpeed >= 0;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)_handleWindowChanges:(BOOL)hasNewWindow
|
|
|
+{
|
|
|
+ // When this view or its superview is leaving the screen, e.g. a modal is presented or another
|
|
|
+ // screen is pushed, this method will get called with newWindow value set to nil - indicating that
|
|
|
+ // this view will be detached from the visible window.
|
|
|
+ // When a view is detached, animations will stop - but will not automatically resumed when it's
|
|
|
+ // re-attached back to window, e.g. when the presented modal is dismissed or another screen is
|
|
|
+ // pop.
|
|
|
+ if (hasNewWindow) {
|
|
|
+ // The view is being re-attached, resume animation if needed.
|
|
|
+ if (_shouldRestoreStateWhenAttachedToWindow) {
|
|
|
+ _shouldRestoreStateWhenAttachedToWindow = NO;
|
|
|
+
|
|
|
+ _isAnimationPlaying = YES;
|
|
|
+ _completionBlock = _completionBlockToRestoreWhenAttachedToWindow;
|
|
|
+ _completionBlockToRestoreWhenAttachedToWindow = nil;
|
|
|
+
|
|
|
+ [self performSelector:@selector(_restoreState) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // The view is being detached, capture information that need to be restored later.
|
|
|
+ if (_isAnimationPlaying) {
|
|
|
+ LOTAnimationCompletionBlock completion = _completionBlock;
|
|
|
+ [self pause];
|
|
|
+ _shouldRestoreStateWhenAttachedToWindow = YES;
|
|
|
+ _completionBlockToRestoreWhenAttachedToWindow = completion;
|
|
|
+ _completionBlock = nil;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+- (void)_handleWillEnterBackground {
|
|
|
+ [self _handleWindowChanges: false];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)_handleWillEnterForeground {
|
|
|
+ [self _handleWindowChanges: (self.window != nil)];
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - Completion Block
|
|
|
+
|
|
|
+- (void)_callCompletionIfNecessary:(BOOL)complete {
|
|
|
+ if (self.completionBlock) {
|
|
|
+ LOTAnimationCompletionBlock completion = self.completionBlock;
|
|
|
+ self.completionBlock = nil;
|
|
|
+ completion(complete);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - External Methods
|
|
|
+
|
|
|
+- (void)setAnimationNamed:(nonnull NSString *)animationName {
|
|
|
+ LOTComposition *comp = [LOTComposition animationNamed:animationName];
|
|
|
+
|
|
|
+ [self _initializeAnimationContainer];
|
|
|
+ [self _setupWithSceneModel:comp];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setAnimationNamed:(NSString *)animationName inBundle:(NSBundle *)bundle {
|
|
|
+ LOTComposition *comp = [LOTComposition animationNamed:animationName inBundle:bundle];
|
|
|
+
|
|
|
+ [self _initializeAnimationContainer];
|
|
|
+ [self _setupWithSceneModel:comp];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setAnimationFromJSON:(nonnull NSDictionary *)animationJSON {
|
|
|
+ LOTComposition *comp = [LOTComposition animationFromJSON:animationJSON];
|
|
|
+
|
|
|
+ [self _initializeAnimationContainer];
|
|
|
+ [self _setupWithSceneModel:comp];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setAnimationFromJSON:(NSDictionary *)animationJSON inBundle:(NSBundle *)bundle {
|
|
|
+ LOTComposition *comp = [LOTComposition animationFromJSON:animationJSON inBundle:bundle];
|
|
|
+
|
|
|
+ [self _initializeAnimationContainer];
|
|
|
+ [self _setupWithSceneModel:comp];
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - External Methods - Model
|
|
|
+
|
|
|
+- (void)setSceneModel:(LOTComposition *)sceneModel {
|
|
|
+ [self _setupWithSceneModel:sceneModel];
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - External Methods - Play Control
|
|
|
+
|
|
|
+- (void)play {
|
|
|
+ if (!_sceneModel) {
|
|
|
+ _isAnimationPlaying = YES;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ [self playFromFrame:_sceneModel.startFrame toFrame:_sceneModel.endFrame withCompletion:nil];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)playWithCompletion:(LOTAnimationCompletionBlock)completion {
|
|
|
+ if (!_sceneModel) {
|
|
|
+ _isAnimationPlaying = YES;
|
|
|
+ self.completionBlock = completion;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ [self playFromFrame:_sceneModel.startFrame toFrame:_sceneModel.endFrame withCompletion:completion];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)playToProgress:(CGFloat)progress withCompletion:(nullable LOTAnimationCompletionBlock)completion {
|
|
|
+ [self playFromProgress:0 toProgress:progress withCompletion:completion];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)playFromProgress:(CGFloat)fromStartProgress
|
|
|
+ toProgress:(CGFloat)toEndProgress
|
|
|
+ withCompletion:(nullable LOTAnimationCompletionBlock)completion {
|
|
|
+ if (!_sceneModel) {
|
|
|
+ _isAnimationPlaying = YES;
|
|
|
+ self.completionBlock = completion;
|
|
|
+ _playRangeStartProgress = fromStartProgress;
|
|
|
+ _playRangeEndProgress = toEndProgress;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ [self playFromFrame:[self _frameForProgress:fromStartProgress]
|
|
|
+ toFrame:[self _frameForProgress:toEndProgress]
|
|
|
+ withCompletion:completion];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)playToFrame:(nonnull NSNumber *)toFrame
|
|
|
+ withCompletion:(nullable LOTAnimationCompletionBlock)completion {
|
|
|
+ [self playFromFrame:_sceneModel.startFrame toFrame:toFrame withCompletion:completion];
|
|
|
+}
|
|
|
+
|
|
|
+- (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 {
|
|
|
+ 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 ? HUGE_VALF : 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;
|
|
|
+}
|
|
|
+
|
|
|
+#pragma mark - Other Time Controls
|
|
|
+
|
|
|
+- (void)stop {
|
|
|
+ _isAnimationPlaying = NO;
|
|
|
+ if (_sceneModel) {
|
|
|
+ [self setProgressWithFrame:_sceneModel.startFrame callCompletionIfNecessary:YES];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+- (void)pause {
|
|
|
+ if (!_sceneModel ||
|
|
|
+ !_isAnimationPlaying) {
|
|
|
+ _isAnimationPlaying = NO;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ NSNumber *frame = [_compContainer.presentationLayer.currentFrame copy];
|
|
|
+ [self setProgressWithFrame:frame callCompletionIfNecessary:YES];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setAnimationProgress:(CGFloat)animationProgress {
|
|
|
+ if (!_sceneModel) {
|
|
|
+ _animationProgress = animationProgress;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ [self setProgressWithFrame:[self _frameForProgress:animationProgress] callCompletionIfNecessary:YES];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setProgressWithFrame:(nonnull NSNumber *)currentFrame {
|
|
|
+ [self setProgressWithFrame:currentFrame callCompletionIfNecessary:YES];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setProgressWithFrame:(nonnull NSNumber *)currentFrame callCompletionIfNecessary:(BOOL)callCompletion {
|
|
|
+ [self _removeCurrentAnimationIfNecessary];
|
|
|
+
|
|
|
+ if (_shouldRestoreStateWhenAttachedToWindow) {
|
|
|
+ _shouldRestoreStateWhenAttachedToWindow = NO;
|
|
|
+
|
|
|
+ self.completionBlock = _completionBlockToRestoreWhenAttachedToWindow;
|
|
|
+ _completionBlockToRestoreWhenAttachedToWindow = nil;
|
|
|
+ }
|
|
|
+
|
|
|
+ _animationProgress = [self _progressForFrame:currentFrame];
|
|
|
+
|
|
|
+ [CATransaction begin];
|
|
|
+ [CATransaction setDisableActions:YES];
|
|
|
+ _compContainer.currentFrame = currentFrame;
|
|
|
+ [_compContainer setNeedsDisplay];
|
|
|
+ [CATransaction commit];
|
|
|
+ if (callCompletion) {
|
|
|
+ [self _callCompletionIfNecessary:NO];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setLoopAnimation:(BOOL)loopAnimation {
|
|
|
+ _loopAnimation = loopAnimation;
|
|
|
+ if (_isAnimationPlaying && _sceneModel) {
|
|
|
+ NSNumber *frame = [_compContainer.presentationLayer.currentFrame copy];
|
|
|
+ [self setProgressWithFrame:frame callCompletionIfNecessary:NO];
|
|
|
+ [self playFromFrame:_playRangeStartFrame toFrame:_playRangeEndFrame withCompletion:self.completionBlock];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setAnimationSpeed:(CGFloat)animationSpeed {
|
|
|
+ _animationSpeed = animationSpeed;
|
|
|
+ if (_isAnimationPlaying && _sceneModel) {
|
|
|
+ NSNumber *frame = [_compContainer.presentationLayer.currentFrame copy];
|
|
|
+ [self setProgressWithFrame:frame callCompletionIfNecessary:NO];
|
|
|
+ [self playFromFrame:_playRangeStartFrame toFrame:_playRangeEndFrame withCompletion:self.completionBlock];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+- (void)forceDrawingUpdate {
|
|
|
+ [self _layoutAndForceUpdate];
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - External Methods - Idle Rasterization
|
|
|
+
|
|
|
+- (void)setShouldRasterizeWhenIdle:(BOOL)shouldRasterize {
|
|
|
+ _shouldRasterizeWhenIdle = shouldRasterize;
|
|
|
+ if (!_isAnimationPlaying) {
|
|
|
+ _compContainer.shouldRasterize = _shouldRasterizeWhenIdle;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - External Methods - Cache
|
|
|
+
|
|
|
+- (void)setCacheEnable:(BOOL)cacheEnable {
|
|
|
+ _cacheEnable = cacheEnable;
|
|
|
+ if (!self.sceneModel.cacheKey) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (cacheEnable) {
|
|
|
+ [[LOTAnimationCache sharedCache] addAnimation:_sceneModel forKey:self.sceneModel.cacheKey];
|
|
|
+ } else {
|
|
|
+ [[LOTAnimationCache sharedCache] removeAnimationForKey:self.sceneModel.cacheKey];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - External Methods - Interactive Controls
|
|
|
+
|
|
|
+- (void)setValueDelegate:(id<LOTValueDelegate> _Nonnull)delegate
|
|
|
+ forKeypath:(LOTKeypath * _Nonnull)keypath {
|
|
|
+ [_compContainer setValueDelegate:delegate forKeypath:keypath];
|
|
|
+ [self _layoutAndForceUpdate];
|
|
|
+}
|
|
|
+
|
|
|
+- (nullable NSArray *)keysForKeyPath:(nonnull LOTKeypath *)keypath {
|
|
|
+ return [_compContainer keysForKeyPath:keypath];
|
|
|
+}
|
|
|
+
|
|
|
+- (CGPoint)convertPoint:(CGPoint)point
|
|
|
+ toKeypathLayer:(nonnull LOTKeypath *)keypath {
|
|
|
+ [self _layoutAndForceUpdate];
|
|
|
+ return [_compContainer convertPoint:point toKeypathLayer:keypath withParentLayer:self.layer];
|
|
|
+}
|
|
|
+
|
|
|
+- (CGRect)convertRect:(CGRect)rect
|
|
|
+ toKeypathLayer:(nonnull LOTKeypath *)keypath {
|
|
|
+ [self _layoutAndForceUpdate];
|
|
|
+ return [_compContainer convertRect:rect toKeypathLayer:keypath withParentLayer:self.layer];
|
|
|
+}
|
|
|
+
|
|
|
+- (CGPoint)convertPoint:(CGPoint)point
|
|
|
+ fromKeypathLayer:(nonnull LOTKeypath *)keypath {
|
|
|
+ [self _layoutAndForceUpdate];
|
|
|
+ return [_compContainer convertPoint:point fromKeypathLayer:keypath withParentLayer:self.layer];
|
|
|
+}
|
|
|
+
|
|
|
+- (CGRect)convertRect:(CGRect)rect
|
|
|
+ fromKeypathLayer:(nonnull LOTKeypath *)keypath {
|
|
|
+ [self _layoutAndForceUpdate];
|
|
|
+ return [_compContainer convertRect:rect fromKeypathLayer:keypath withParentLayer:self.layer];
|
|
|
+}
|
|
|
+
|
|
|
+#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR
|
|
|
+
|
|
|
+- (void)addSubview:(nonnull LOTView *)view
|
|
|
+ toKeypathLayer:(nonnull LOTKeypath *)keypath {
|
|
|
+ [self _layoutAndForceUpdate];
|
|
|
+ CGRect viewRect = view.frame;
|
|
|
+ LOTView *wrapperView = [[LOTView alloc] initWithFrame:viewRect];
|
|
|
+ view.frame = view.bounds;
|
|
|
+ view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
|
+ [wrapperView addSubview:view];
|
|
|
+ [self addSubview:wrapperView];
|
|
|
+ [_compContainer addSublayer:wrapperView.layer toKeypathLayer:keypath];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)maskSubview:(nonnull LOTView *)view
|
|
|
+ toKeypathLayer:(nonnull LOTKeypath *)keypath {
|
|
|
+ [self _layoutAndForceUpdate];
|
|
|
+ CGRect viewRect = view.frame;
|
|
|
+ LOTView *wrapperView = [[LOTView alloc] initWithFrame:viewRect];
|
|
|
+ view.frame = view.bounds;
|
|
|
+ view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
|
+ [wrapperView addSubview:view];
|
|
|
+ [self addSubview:wrapperView];
|
|
|
+ [_compContainer maskSublayer:wrapperView.layer toKeypathLayer:keypath];
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+#else
|
|
|
+
|
|
|
+- (void)addSubview:(nonnull LOTView *)view
|
|
|
+ toKeypathLayer:(nonnull LOTKeypath *)keypath {
|
|
|
+ [self _layout];
|
|
|
+ CGRect viewRect = view.frame;
|
|
|
+ LOTView *wrapperView = [[LOTView alloc] initWithFrame:viewRect];
|
|
|
+ view.frame = view.bounds;
|
|
|
+ view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
|
+ [wrapperView addSubview:view];
|
|
|
+ [self addSubview:wrapperView];
|
|
|
+ [_compContainer addSublayer:wrapperView.layer toKeypathLayer:keypath];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)maskSubview:(nonnull LOTView *)view
|
|
|
+ toKeypathLayer:(nonnull LOTKeypath *)keypath {
|
|
|
+ [self _layout];
|
|
|
+ CGRect viewRect = view.frame;
|
|
|
+ LOTView *wrapperView = [[LOTView alloc] initWithFrame:viewRect];
|
|
|
+ view.frame = view.bounds;
|
|
|
+ view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
|
+ [wrapperView addSubview:view];
|
|
|
+ [self addSubview:wrapperView];
|
|
|
+ [_compContainer maskSublayer:wrapperView.layer toKeypathLayer:keypath];
|
|
|
+}
|
|
|
+
|
|
|
+#endif
|
|
|
+
|
|
|
+# pragma mark - Semi-Private Methods
|
|
|
+
|
|
|
+- (CALayer * _Nullable)layerForKey:(NSString * _Nonnull)keyname {
|
|
|
+ return _compContainer.childMap[keyname];
|
|
|
+}
|
|
|
+
|
|
|
+- (NSArray * _Nonnull)compositionLayers {
|
|
|
+ return _compContainer.childLayers;
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - Getters and Setters
|
|
|
+
|
|
|
+- (CGFloat)animationDuration {
|
|
|
+ if (!_sceneModel) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ CAAnimation *play = [_compContainer animationForKey:kCompContainerAnimationKey];
|
|
|
+ if (play) {
|
|
|
+ return play.duration;
|
|
|
+ }
|
|
|
+ return (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue) / _sceneModel.framerate.floatValue;
|
|
|
+}
|
|
|
+
|
|
|
+- (CGFloat)animationProgress {
|
|
|
+ if (_isAnimationPlaying &&
|
|
|
+ _compContainer.presentationLayer) {
|
|
|
+ CGFloat activeProgress = [self _progressForFrame:[(LOTCompositionContainer *)_compContainer.presentationLayer currentFrame]];
|
|
|
+ return activeProgress;
|
|
|
+ }
|
|
|
+ return _animationProgress;
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - Overrides
|
|
|
+
|
|
|
+#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR
|
|
|
+
|
|
|
+#define LOTViewContentMode UIViewContentMode
|
|
|
+#define LOTViewContentModeScaleToFill UIViewContentModeScaleToFill
|
|
|
+#define LOTViewContentModeScaleAspectFit UIViewContentModeScaleAspectFit
|
|
|
+#define LOTViewContentModeScaleAspectFill UIViewContentModeScaleAspectFill
|
|
|
+#define LOTViewContentModeRedraw UIViewContentModeRedraw
|
|
|
+#define LOTViewContentModeCenter UIViewContentModeCenter
|
|
|
+#define LOTViewContentModeTop UIViewContentModeTop
|
|
|
+#define LOTViewContentModeBottom UIViewContentModeBottom
|
|
|
+#define LOTViewContentModeLeft UIViewContentModeLeft
|
|
|
+#define LOTViewContentModeRight UIViewContentModeRight
|
|
|
+#define LOTViewContentModeTopLeft UIViewContentModeTopLeft
|
|
|
+#define LOTViewContentModeTopRight UIViewContentModeTopRight
|
|
|
+#define LOTViewContentModeBottomLeft UIViewContentModeBottomLeft
|
|
|
+#define LOTViewContentModeBottomRight UIViewContentModeBottomRight
|
|
|
+
|
|
|
+- (CGSize)intrinsicContentSize {
|
|
|
+ if (!_sceneModel) {
|
|
|
+ return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
|
|
|
+ }
|
|
|
+ return _sceneModel.compBounds.size;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)didMoveToSuperview {
|
|
|
+ [super didMoveToSuperview];
|
|
|
+ if (self.superview == nil) {
|
|
|
+ [self _callCompletionIfNecessary:NO];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+- (void)willMoveToWindow:(UIWindow *)newWindow {
|
|
|
+ [self _handleWindowChanges:(newWindow != nil)];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)didMoveToWindow {
|
|
|
+ _compContainer.rasterizationScale = self.window.screen.scale;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setContentMode:(LOTViewContentMode)contentMode {
|
|
|
+ [super setContentMode:contentMode];
|
|
|
+ [self setNeedsLayout];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)layoutSubviews {
|
|
|
+ [super layoutSubviews];
|
|
|
+ [self _layout];
|
|
|
+}
|
|
|
+
|
|
|
+#else
|
|
|
+
|
|
|
+- (void)viewWillMoveToWindow:(NSWindow *)newWindow {
|
|
|
+ [self _handleWindowChanges:(newWindow != nil)];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)viewDidMoveToWindow {
|
|
|
+ _compContainer.rasterizationScale = self.window.screen.backingScaleFactor;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setCompletionBlock:(LOTAnimationCompletionBlock)completionBlock {
|
|
|
+ if (completionBlock) {
|
|
|
+ _completionBlock = ^(BOOL finished) {
|
|
|
+ dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(finished); });
|
|
|
+ };
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ _completionBlock = nil;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setContentMode:(LOTViewContentMode)contentMode {
|
|
|
+ _contentMode = contentMode;
|
|
|
+ [self setNeedsLayout];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setNeedsLayout {
|
|
|
+ self.needsLayout = YES;
|
|
|
+}
|
|
|
+
|
|
|
+- (BOOL)isFlipped {
|
|
|
+ return YES;
|
|
|
+}
|
|
|
+
|
|
|
+- (BOOL)wantsUpdateLayer {
|
|
|
+ return YES;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)layout {
|
|
|
+ [super layout];
|
|
|
+ [self _layout];
|
|
|
+}
|
|
|
+
|
|
|
+#endif
|
|
|
+
|
|
|
+- (void)_layoutAndForceUpdate {
|
|
|
+ [CATransaction begin];
|
|
|
+ [CATransaction setDisableActions:YES];
|
|
|
+ [self _layout];
|
|
|
+ [_compContainer displayWithFrame:_compContainer.currentFrame forceUpdate:YES];
|
|
|
+ [CATransaction commit];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)_layout {
|
|
|
+ CGPoint centerPoint = LOT_RectGetCenterPoint(self.bounds);
|
|
|
+ CATransform3D xform;
|
|
|
+
|
|
|
+ if (self.contentMode == LOTViewContentModeScaleToFill) {
|
|
|
+ CGSize scaleSize = CGSizeMake(self.bounds.size.width / self.sceneModel.compBounds.size.width,
|
|
|
+ self.bounds.size.height / self.sceneModel.compBounds.size.height);
|
|
|
+ xform = CATransform3DMakeScale(scaleSize.width, scaleSize.height, 1);
|
|
|
+ } else if (self.contentMode == LOTViewContentModeScaleAspectFit) {
|
|
|
+ CGFloat compAspect = self.sceneModel.compBounds.size.width / self.sceneModel.compBounds.size.height;
|
|
|
+ CGFloat viewAspect = self.bounds.size.width / self.bounds.size.height;
|
|
|
+ BOOL scaleWidth = compAspect > viewAspect;
|
|
|
+ CGFloat dominantDimension = scaleWidth ? self.bounds.size.width : self.bounds.size.height;
|
|
|
+ CGFloat compDimension = scaleWidth ? self.sceneModel.compBounds.size.width : self.sceneModel.compBounds.size.height;
|
|
|
+ CGFloat scale = dominantDimension / compDimension;
|
|
|
+ xform = CATransform3DMakeScale(scale, scale, 1);
|
|
|
+ } else if (self.contentMode == LOTViewContentModeScaleAspectFill) {
|
|
|
+ CGFloat compAspect = self.sceneModel.compBounds.size.width / self.sceneModel.compBounds.size.height;
|
|
|
+ CGFloat viewAspect = self.bounds.size.width / self.bounds.size.height;
|
|
|
+ BOOL scaleWidth = compAspect < viewAspect;
|
|
|
+ CGFloat dominantDimension = scaleWidth ? self.bounds.size.width : self.bounds.size.height;
|
|
|
+ CGFloat compDimension = scaleWidth ? self.sceneModel.compBounds.size.width : self.sceneModel.compBounds.size.height;
|
|
|
+ CGFloat scale = dominantDimension / compDimension;
|
|
|
+ xform = CATransform3DMakeScale(scale, scale, 1);
|
|
|
+ } else {
|
|
|
+ xform = CATransform3DIdentity;
|
|
|
+ }
|
|
|
+
|
|
|
+ [CATransaction begin];
|
|
|
+ [CATransaction setDisableActions:YES];
|
|
|
+ _compContainer.transform = CATransform3DIdentity;
|
|
|
+ _compContainer.bounds = _sceneModel.compBounds;
|
|
|
+ _compContainer.viewportBounds = _sceneModel.compBounds;
|
|
|
+ _compContainer.transform = xform;
|
|
|
+ _compContainer.position = centerPoint;
|
|
|
+ [CATransaction commit];
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - CAAnimationDelegate
|
|
|
+
|
|
|
+- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)complete {
|
|
|
+ if ([_compContainer animationForKey:kCompContainerAnimationKey] == anim &&
|
|
|
+ [anim isKindOfClass:[CABasicAnimation class]]) {
|
|
|
+ CABasicAnimation *playAnimation = (CABasicAnimation *)anim;
|
|
|
+ NSNumber *frame = _compContainer.presentationLayer.currentFrame;
|
|
|
+ if (complete) {
|
|
|
+ // Set the final frame based on the animation to/from values. If playing forward, use the
|
|
|
+ // toValue otherwise we want to end on the fromValue.
|
|
|
+ frame = [self _isSpeedNegative] ? (NSNumber *)playAnimation.toValue : (NSNumber *)playAnimation.fromValue;
|
|
|
+ }
|
|
|
+ [self _removeCurrentAnimationIfNecessary];
|
|
|
+ [self setProgressWithFrame:frame callCompletionIfNecessary:NO];
|
|
|
+ [self _callCompletionIfNecessary:complete];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# pragma mark - DEPRECATED
|
|
|
+
|
|
|
+- (void)addSubview:(nonnull LOTView *)view
|
|
|
+ toLayerNamed:(nonnull NSString *)layer
|
|
|
+ applyTransform:(BOOL)applyTransform {
|
|
|
+ NSLog(@"%s: Function is DEPRECATED. Please use addSubview:forKeypathLayer:", __PRETTY_FUNCTION__);
|
|
|
+ LOTKeypath *keypath = [LOTKeypath keypathWithString:layer];
|
|
|
+ if (applyTransform) {
|
|
|
+ [self addSubview:view toKeypathLayer:keypath];
|
|
|
+ } else {
|
|
|
+ [self maskSubview:view toKeypathLayer:keypath];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+- (CGRect)convertRect:(CGRect)rect
|
|
|
+ toLayerNamed:(NSString *_Nullable)layerName {
|
|
|
+ NSLog(@"%s: Function is DEPRECATED. Please use convertRect:forKeypathLayer:", __PRETTY_FUNCTION__);
|
|
|
+ LOTKeypath *keypath = [LOTKeypath keypathWithString:layerName];
|
|
|
+ return [self convertRect:rect toKeypathLayer:keypath];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setValue:(nonnull id)value
|
|
|
+ forKeypath:(nonnull NSString *)keypath
|
|
|
+ atFrame:(nullable NSNumber *)frame {
|
|
|
+ NSLog(@"%s: Function is DEPRECATED and no longer functional. Please use setValueCallback:forKeypath:", __PRETTY_FUNCTION__);
|
|
|
+}
|
|
|
+
|
|
|
+- (void)logHierarchyKeypaths {
|
|
|
+ NSArray *keypaths = [self keysForKeyPath:[LOTKeypath keypathWithString:@"**"]];
|
|
|
+ for (NSString *keypath in keypaths) {
|
|
|
+ NSLog(@"%@", keypath);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@end
|