// // 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 _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