From a53bbe99cabb93eed27d34c22fdbad896c3f853d Mon Sep 17 00:00:00 2001 From: Michael Welsh Date: Thu, 17 Aug 2017 10:16:47 -0400 Subject: [PATCH 1/5] Add better support for negative animation speeds - The progress can be calculated basd on negative speed + The end frame would be a progress of 0 and the start frame a progress of 1 + Recalculate progress when the speed changes, otherwise a "jump" will be seen when restarting animation - The frame for the progress can be calculated using the newly inverted progress + Determine the absolute progress based on current progress and determine which frame to use based on animation speed - This fixes issues of animations playing with negative speeds "bouncing" back to their final position which was caused by the final frame being calculated based on a completed progress of 1 which then did not take into account the negative animation speed. --- lottie-ios/Classes/Private/LOTAnimationView.m | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/lottie-ios/Classes/Private/LOTAnimationView.m b/lottie-ios/Classes/Private/LOTAnimationView.m index 312c3a048a..09d9cd05f9 100644 --- a/lottie-ios/Classes/Private/LOTAnimationView.m +++ b/lottie-ios/Classes/Private/LOTAnimationView.m @@ -14,6 +14,8 @@ #import "LOTAnimationCache.h" #import "LOTCompositionContainer.h" +static NSString * const kCompContainerAnimationKey = @"play"; + @implementation LOTAnimationView { LOTCompositionContainer *_compContainer; NSNumber *_playRangeStartFrame; @@ -208,14 +210,32 @@ if (!_sceneModel) { return 0; } - return ((frame.floatValue - _sceneModel.startFrame.floatValue) / (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue)); + CGFloat absoluteProgress = ((frame.floatValue - _sceneModel.startFrame.floatValue) / (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue)); + if ([self _isPlayingForwards]) { + return absoluteProgress; + } else { + // If the animation is playing backwards, the progress is inverted. + return 1 - absoluteProgress; + } } - (NSNumber *)_frameForProgress:(CGFloat)progress { if (!_sceneModel) { return @0; } - return @(((_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue) * progress) + _sceneModel.startFrame.floatValue); + CGFloat absoluteProgress = ((_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue) * progress); + if ([self _isPlayingForwards]) { + // If we're moving forward, then add the absolute progress to the start. + return @(absoluteProgress + _sceneModel.startFrame.floatValue); + } else { + // It the animation is playing backwards, subtract the progress from the end. + return @(_sceneModel.endFrame.floatValue - absoluteProgress); + } +} + +- (BOOL)_isPlayingForwards { + // If the animation speed is negative, then we're moving backwards. + return _animationSpeed >= 0; } # pragma mark - Completion Block @@ -293,11 +313,8 @@ return; } NSNumber *currentFrame = [self _frameForProgress:_animationProgress]; - + currentFrame = @(MAX(MIN(currentFrame.floatValue, toEndFrame.floatValue), fromStartFrame.floatValue)); - if (currentFrame.floatValue == toEndFrame.floatValue) { - currentFrame = fromStartFrame; - } _animationProgress = [self _progressForFrame:currentFrame]; NSTimeInterval offset = MAX(0, (_animationProgress * (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue)) - fromStartFrame.floatValue) / _sceneModel.framerate.floatValue; @@ -313,7 +330,7 @@ animation.delegate = self; animation.removedOnCompletion = NO; animation.beginTime = CACurrentMediaTime() - offset; - [_compContainer addAnimation:animation forKey:@"play"]; + [_compContainer addAnimation:animation forKey:kCompContainerAnimationKey]; _isAnimationPlaying = YES; } @@ -374,11 +391,18 @@ } -(void)setAnimationSpeed:(CGFloat)animationSpeed { + BOOL directionChange = NO; + if ((animationSpeed >= 0 && _animationSpeed < 0) || (animationSpeed < 0 && _animationSpeed >= 0)) { + directionChange = YES; + } _animationSpeed = animationSpeed; + NSNumber *frame = [_compContainer.presentationLayer.currentFrame copy]; if (_isAnimationPlaying && _sceneModel) { - NSNumber *frame = [_compContainer.presentationLayer.currentFrame copy]; [self setProgressWithFrame:frame callCompletionIfNecessary:NO]; [self playFromFrame:_playRangeStartFrame toFrame:_playRangeEndFrame withCompletion:self.completionBlock]; + } else if (directionChange) { + // The progress needs to be re-calculated if the speed direction changes if the animation is not playing. + _animationProgress = [self _progressForFrame:frame]; } } @@ -464,7 +488,7 @@ if (!_sceneModel) { return 0; } - CAAnimation *play = [_compContainer animationForKey:@"play"]; + CAAnimation *play = [_compContainer animationForKey:kCompContainerAnimationKey]; if (play) { return play.duration; } @@ -596,12 +620,14 @@ # pragma mark - CAANimationDelegate - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)complete { - if ([_compContainer animationForKey:@"play"] == anim && + if ([_compContainer animationForKey:kCompContainerAnimationKey] == anim && [anim isKindOfClass:[CABasicAnimation class]]) { CABasicAnimation *playAnimation = (CABasicAnimation *)anim; NSNumber *frame = _compContainer.presentationLayer.currentFrame; if (complete) { - frame = (NSNumber *)playAnimation.toValue; + // 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 _isPlayingForwards] ? (NSNumber *)playAnimation.toValue : (NSNumber *)playAnimation.fromValue; } [self _removeCurrentAnimationIfNecessary]; [self setProgressWithFrame:frame callCompletionIfNecessary:NO]; From 3e0790a41387c963aad29afb58c6a75e6251d33e Mon Sep 17 00:00:00 2001 From: Michael Welsh Date: Thu, 17 Aug 2017 10:16:47 -0400 Subject: [PATCH 2/5] Add better support for negative animation speeds - The progress can be calculated basd on negative speed + The end frame would be a progress of 0 and the start frame a progress of 1 + Recalculate progress when the speed changes, otherwise a "jump" will be seen when restarting animation - The frame for the progress can be calculated using the newly inverted progress + Determine the absolute progress based on current progress and determine which frame to use based on animation speed - This fixes issues of animations playing with negative speeds "bouncing" back to their final position which was caused by the final frame being calculated based on a completed progress of 1 which then did not take into account the negative animation speed. --- lottie-ios/Classes/Private/LOTAnimationView.m | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/lottie-ios/Classes/Private/LOTAnimationView.m b/lottie-ios/Classes/Private/LOTAnimationView.m index 312c3a048a..0fe50627cc 100644 --- a/lottie-ios/Classes/Private/LOTAnimationView.m +++ b/lottie-ios/Classes/Private/LOTAnimationView.m @@ -14,6 +14,8 @@ #import "LOTAnimationCache.h" #import "LOTCompositionContainer.h" +static NSString * const kCompContainerAnimationKey = @"play"; + @implementation LOTAnimationView { LOTCompositionContainer *_compContainer; NSNumber *_playRangeStartFrame; @@ -208,14 +210,32 @@ if (!_sceneModel) { return 0; } - return ((frame.floatValue - _sceneModel.startFrame.floatValue) / (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue)); + CGFloat absoluteProgress = ((frame.floatValue - _sceneModel.startFrame.floatValue) / (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue)); + if ([self _isPlayingForwards]) { + return absoluteProgress; + } else { + // If the animation is playing backwards, the progress is inverted. + return 1 - absoluteProgress; + } } - (NSNumber *)_frameForProgress:(CGFloat)progress { if (!_sceneModel) { return @0; } - return @(((_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue) * progress) + _sceneModel.startFrame.floatValue); + CGFloat absoluteProgress = ((_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue) * progress); + if ([self _isPlayingForwards]) { + // If we're moving forward, then add the absolute progress to the start. + return @(absoluteProgress + _sceneModel.startFrame.floatValue); + } else { + // It the animation is playing backwards, subtract the progress from the end. + return @(_sceneModel.endFrame.floatValue - absoluteProgress); + } +} + +- (BOOL)_isPlayingForwards { + // If the animation speed is negative, then we're moving backwards. + return _animationSpeed >= 0; } # pragma mark - Completion Block @@ -293,10 +313,13 @@ return; } NSNumber *currentFrame = [self _frameForProgress:_animationProgress]; - + currentFrame = @(MAX(MIN(currentFrame.floatValue, toEndFrame.floatValue), fromStartFrame.floatValue)); - if (currentFrame.floatValue == toEndFrame.floatValue) { + BOOL playingForward = [self _isPlayingForwards]; + if (currentFrame.floatValue == toEndFrame.floatValue && playingForward) { currentFrame = fromStartFrame; + } else if (currentFrame.floatValue == fromStartFrame.floatValue && !playingForward) { + currentFrame = toEndFrame; } _animationProgress = [self _progressForFrame:currentFrame]; @@ -313,7 +336,7 @@ animation.delegate = self; animation.removedOnCompletion = NO; animation.beginTime = CACurrentMediaTime() - offset; - [_compContainer addAnimation:animation forKey:@"play"]; + [_compContainer addAnimation:animation forKey:kCompContainerAnimationKey]; _isAnimationPlaying = YES; } @@ -374,11 +397,18 @@ } -(void)setAnimationSpeed:(CGFloat)animationSpeed { + BOOL directionChange = NO; + if ((animationSpeed >= 0 && _animationSpeed < 0) || (animationSpeed < 0 && _animationSpeed >= 0)) { + directionChange = YES; + } _animationSpeed = animationSpeed; + NSNumber *frame = [_compContainer.presentationLayer.currentFrame copy]; if (_isAnimationPlaying && _sceneModel) { - NSNumber *frame = [_compContainer.presentationLayer.currentFrame copy]; [self setProgressWithFrame:frame callCompletionIfNecessary:NO]; [self playFromFrame:_playRangeStartFrame toFrame:_playRangeEndFrame withCompletion:self.completionBlock]; + } else if (directionChange) { + // The progress needs to be re-calculated if the speed direction changes if the animation is not playing. + _animationProgress = [self _progressForFrame:frame]; } } @@ -464,7 +494,7 @@ if (!_sceneModel) { return 0; } - CAAnimation *play = [_compContainer animationForKey:@"play"]; + CAAnimation *play = [_compContainer animationForKey:kCompContainerAnimationKey]; if (play) { return play.duration; } @@ -596,12 +626,14 @@ # pragma mark - CAANimationDelegate - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)complete { - if ([_compContainer animationForKey:@"play"] == anim && + if ([_compContainer animationForKey:kCompContainerAnimationKey] == anim && [anim isKindOfClass:[CABasicAnimation class]]) { CABasicAnimation *playAnimation = (CABasicAnimation *)anim; NSNumber *frame = _compContainer.presentationLayer.currentFrame; if (complete) { - frame = (NSNumber *)playAnimation.toValue; + // 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 _isPlayingForwards] ? (NSNumber *)playAnimation.toValue : (NSNumber *)playAnimation.fromValue; } [self _removeCurrentAnimationIfNecessary]; [self setProgressWithFrame:frame callCompletionIfNecessary:NO]; From ab75236c5f0dcbedfaa6af51b182a75c0413a93f Mon Sep 17 00:00:00 2001 From: Michael Welsh Date: Thu, 17 Aug 2017 17:05:20 -0400 Subject: [PATCH 3/5] Revert some of the animationProgress changes to revert to reflect absolutes - Update header documentation to make it clear how animatinoSpeed and animationProgress interact - Leave fixes in there that stop reversed animations "snapping" back --- lottie-ios/Classes/Private/LOTAnimationView.m | 26 +++---------------- .../Classes/PublicHeaders/LOTAnimationView.h | 5 +++- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/lottie-ios/Classes/Private/LOTAnimationView.m b/lottie-ios/Classes/Private/LOTAnimationView.m index 0fe50627cc..ea1375b3fa 100644 --- a/lottie-ios/Classes/Private/LOTAnimationView.m +++ b/lottie-ios/Classes/Private/LOTAnimationView.m @@ -210,27 +210,14 @@ static NSString * const kCompContainerAnimationKey = @"play"; if (!_sceneModel) { return 0; } - CGFloat absoluteProgress = ((frame.floatValue - _sceneModel.startFrame.floatValue) / (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue)); - if ([self _isPlayingForwards]) { - return absoluteProgress; - } else { - // If the animation is playing backwards, the progress is inverted. - return 1 - absoluteProgress; - } + return ((frame.floatValue - _sceneModel.startFrame.floatValue) / (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue)); } - (NSNumber *)_frameForProgress:(CGFloat)progress { if (!_sceneModel) { return @0; } - CGFloat absoluteProgress = ((_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue) * progress); - if ([self _isPlayingForwards]) { - // If we're moving forward, then add the absolute progress to the start. - return @(absoluteProgress + _sceneModel.startFrame.floatValue); - } else { - // It the animation is playing backwards, subtract the progress from the end. - return @(_sceneModel.endFrame.floatValue - absoluteProgress); - } + return @(((_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue) * progress) + _sceneModel.startFrame.floatValue); } - (BOOL)_isPlayingForwards { @@ -397,18 +384,11 @@ static NSString * const kCompContainerAnimationKey = @"play"; } -(void)setAnimationSpeed:(CGFloat)animationSpeed { - BOOL directionChange = NO; - if ((animationSpeed >= 0 && _animationSpeed < 0) || (animationSpeed < 0 && _animationSpeed >= 0)) { - directionChange = YES; - } _animationSpeed = animationSpeed; - NSNumber *frame = [_compContainer.presentationLayer.currentFrame copy]; if (_isAnimationPlaying && _sceneModel) { + NSNumber *frame = [_compContainer.presentationLayer.currentFrame copy]; [self setProgressWithFrame:frame callCompletionIfNecessary:NO]; [self playFromFrame:_playRangeStartFrame toFrame:_playRangeEndFrame withCompletion:self.completionBlock]; - } else if (directionChange) { - // The progress needs to be re-calculated if the speed direction changes if the animation is not playing. - _animationProgress = [self _progressForFrame:frame]; } } diff --git a/lottie-ios/Classes/PublicHeaders/LOTAnimationView.h b/lottie-ios/Classes/PublicHeaders/LOTAnimationView.h index 0ae5534e86..5bfb9f3c3f 100644 --- a/lottie-ios/Classes/PublicHeaders/LOTAnimationView.h +++ b/lottie-ios/Classes/PublicHeaders/LOTAnimationView.h @@ -46,9 +46,12 @@ typedef void (^LOTAnimationCompletionBlock)(BOOL animationFinished); // TODO /// Sets a progress from 0 - 1 of the animation. If the animation is playing it will stop and the compeltion block will be called. +/// The animation progress is in terms of absolute progress of the defined animation and does not +/// take into account negative speeds. @property (nonatomic, assign) CGFloat animationProgress; -/// Sets the speed of the animation. Accepts a negative value for reversing animation +/// Sets the speed of the animation. Accepts a negative value for reversing animation. +/// Negative speeds do not affect animationProgress @property (nonatomic, assign) CGFloat animationSpeed; /// Read only of the duration in seconds of the animation at speed of 1 From b0b7b7c8cb5ff06d8b9f20109ca70d2839fc3cda Mon Sep 17 00:00:00 2001 From: Michael Welsh Date: Thu, 17 Aug 2017 17:37:15 -0400 Subject: [PATCH 4/5] Rename _isPlayingForward to _isSpeedNegative to be more clear what it's checking --- lottie-ios/Classes/Private/LOTAnimationView.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lottie-ios/Classes/Private/LOTAnimationView.m b/lottie-ios/Classes/Private/LOTAnimationView.m index ea1375b3fa..8aab53ffba 100644 --- a/lottie-ios/Classes/Private/LOTAnimationView.m +++ b/lottie-ios/Classes/Private/LOTAnimationView.m @@ -220,7 +220,7 @@ static NSString * const kCompContainerAnimationKey = @"play"; return @(((_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue) * progress) + _sceneModel.startFrame.floatValue); } -- (BOOL)_isPlayingForwards { +- (BOOL)_isSpeedNegative { // If the animation speed is negative, then we're moving backwards. return _animationSpeed >= 0; } @@ -302,7 +302,7 @@ static NSString * const kCompContainerAnimationKey = @"play"; NSNumber *currentFrame = [self _frameForProgress:_animationProgress]; currentFrame = @(MAX(MIN(currentFrame.floatValue, toEndFrame.floatValue), fromStartFrame.floatValue)); - BOOL playingForward = [self _isPlayingForwards]; + BOOL playingForward = [self _isSpeedNegative]; if (currentFrame.floatValue == toEndFrame.floatValue && playingForward) { currentFrame = fromStartFrame; } else if (currentFrame.floatValue == fromStartFrame.floatValue && !playingForward) { @@ -613,7 +613,7 @@ static NSString * const kCompContainerAnimationKey = @"play"; 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 _isPlayingForwards] ? (NSNumber *)playAnimation.toValue : (NSNumber *)playAnimation.fromValue; + frame = [self _isSpeedNegative] ? (NSNumber *)playAnimation.toValue : (NSNumber *)playAnimation.fromValue; } [self _removeCurrentAnimationIfNecessary]; [self setProgressWithFrame:frame callCompletionIfNecessary:NO]; From 805a67144889a9845905cc32febe2898365230a5 Mon Sep 17 00:00:00 2001 From: Michael Welsh Date: Thu, 17 Aug 2017 17:41:50 -0400 Subject: [PATCH 5/5] Update LOTAnimationView header to have better comments --- lottie-ios/Classes/PublicHeaders/LOTAnimationView.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lottie-ios/Classes/PublicHeaders/LOTAnimationView.h b/lottie-ios/Classes/PublicHeaders/LOTAnimationView.h index 5bfb9f3c3f..aef772c28b 100644 --- a/lottie-ios/Classes/PublicHeaders/LOTAnimationView.h +++ b/lottie-ios/Classes/PublicHeaders/LOTAnimationView.h @@ -46,12 +46,12 @@ typedef void (^LOTAnimationCompletionBlock)(BOOL animationFinished); // TODO /// Sets a progress from 0 - 1 of the animation. If the animation is playing it will stop and the compeltion block will be called. -/// The animation progress is in terms of absolute progress of the defined animation and does not -/// take into account negative speeds. +/// The current progress of the animation in absolute time. +/// e.g. a value of 0.75 always represents the same point in the animation, regardless of positive +/// or negative speed. @property (nonatomic, assign) CGFloat animationProgress; /// Sets the speed of the animation. Accepts a negative value for reversing animation. -/// Negative speeds do not affect animationProgress @property (nonatomic, assign) CGFloat animationSpeed; /// Read only of the duration in seconds of the animation at speed of 1